From 3227fff9e9a5ea44ae48cec3e24144e3e6184789 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Wed, 22 Jan 2025 22:10:29 +0000 Subject: [PATCH 01/47] starting to add WDWD mergers in CI --- .../cosmic_integration/ClassCOMPAS.py | 25 +++++++++++++------ .../FastCosmicIntegration.py | 11 +++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 76ac552b3..9835ff9b7 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -31,12 +31,13 @@ def __init__( self.mass2 = None # Msun self.DCOmask = None self.allTypesMask = None - self.BBHmask = None - self.DNSmask = None + self.BHBHmask = None + self.NSNSmask = None self.BHNSmask = None + self.WDWDmask = None self.CHE_mask = None - self.CHE_BBHmask = None - self.NonCHE_BBHmask = None + self.CHE_BHBHmask = None + self.NonCHE_BHBHmask = None self.initialZ = None self.sw_weights = None self.n_systems = None @@ -91,6 +92,10 @@ def setCOMPASDCOmask( "BBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13), np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14)), "BNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), + + # ## MELANIE CHANGE - defining types of masks for BWDs and COWD systems + # "BWD": np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==11),np.logical_and(stellar_type_1==12,stellar_type_2==12))))))))), + } type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) @@ -124,11 +129,15 @@ def setCOMPASDCOmask( # create a mask for each dco type supplied self.DCOmask = type_masks[types] * hubble_mask * rlof_mask * pessimistic_mask - self.BBHmask = type_masks["BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.BHBHmask = type_masks["BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask - self.DNSmask = type_masks["BNS"] * hubble_mask * rlof_mask * pessimistic_mask - self.CHE_BBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask - self.NonCHE_BBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.NSNSmask = type_masks["BNS"] * hubble_mask * rlof_mask * pessimistic_mask + + # ## MELANIE CHANGE - adding masks for BWD and COWD systems + # self.WDWDmask = type_masks["BWD"] * hubble_mask * rlof_mask * pessimistic_mask + + self.CHE_BHBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.NonCHE_BHBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask self.optimisticmask = pessimistic_mask diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index e3724e5ab..fd36a8655 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -760,6 +760,7 @@ def parse_cli_args(): parser = argparse.ArgumentParser() parser.add_argument("--path", dest='path', help="Path to the COMPAS file that contains the output", type=str, default="COMPAS_Output.h5") + # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS parser.add_argument("--dco_type", dest='dco_type', help="Which DCO type you used to calculate rates, one of: ['all', 'BBH', 'BHNS', 'BNS'] ", @@ -767,7 +768,13 @@ def parse_cli_args(): parser.add_argument("--weight", dest='weight_column', help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", type=str, default=None) - + parser.add_argument("--keep_pessimistic_CEE", dest='remove_pessimistic_CEE', + help="keep_pessimistic_CEE will set remove_pessimistic_CEE to false. The default behaviour (remove_pessimistic_CEE == True), will mask binaries that binaries that experience a CEE while on the HG", + action='store_false', default=True) + parser.add_argument("--keepRLOF_postCE", dest='remove_RLOF_after_CEE', + help="keepRLOF_postCE will set remove_RLOF_after_CEE to false. The default behaviour (remove_RLOF_after_CEE == True), will mask binaries that have immediate RLOF after a CCE", + action='store_false', default=True) + # Options for the redshift evolution and detector sensitivity parser.add_argument("--maxz", dest='max_redshift', help="Maximum redshift to use in array", type=float, default=10) parser.add_argument("--zSF", dest='z_first_SF', help="redshift of first star formation", type=float, default=10) @@ -841,6 +848,8 @@ def main(): args.path, dco_type=args.dco_type, weight_column=args.weight_column, + pessimistic_CEE=args.remove_pessimistic_CEE, + no_RLOF_after_CEE=args.remove_RLOF_after_CEE max_redshift=args.max_redshift, max_redshift_detection=args.max_redshift_detection, redshift_step=args.redshift_step, From 59157f77f81303379688166eca6787a580534c60 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Wed, 22 Jan 2025 22:58:22 +0000 Subject: [PATCH 02/47] more changes to FCI and ClassCOMPAS --- .../cosmic_integration/ClassCOMPAS.py | 30 ++++++++----------- .../FastCosmicIntegration.py | 25 +++++++++------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 9835ff9b7..52307d796 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -2,7 +2,8 @@ import numpy as np import h5py as h5 import os -from . import totalMassEvolvedPerZ as MPZ +# from . import totalMassEvolvedPerZ as MPZ +import totalMassEvolvedPerZ as MPZ class COMPASData(object): @@ -64,7 +65,7 @@ def __init__( print(" and optionally self.setGridAndMassEvolved() if using a metallicity grid") def setCOMPASDCOmask( - self, types="BBH", withinHubbleTime=True, pessimistic=True, noRLOFafterCEE=True + self, types="BHBH", withinHubbleTime=True, pessimistic=True, noRLOFafterCEE=True ): # By default, we mask for BBHs that merge within a Hubble time, assuming # the pessimistic CEE prescription (HG donors cannot survive a CEE) and @@ -89,16 +90,14 @@ def setCOMPASDCOmask( # mask on stellar types (where 14=BH and 13=NS), BHNS can be BHNS or NSBH type_masks = { "all": np.repeat(True, len(dco_seeds)), - "BBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), - "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13), np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14)), - "BNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), - - # ## MELANIE CHANGE - defining types of masks for BWDs and COWD systems - # "BWD": np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==11),np.logical_and(stellar_type_1==12,stellar_type_2==12))))))))), - + "BHBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), + "BHNS": np.logical_and(np.isin(stellar_type_1,[13,14]),np.isin(stellar_type_2,[13,14])), + "NSNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), + "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])) } - type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) - type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) + + type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BHBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) + type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BHBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) # if the user wants to make RLOF or optimistic CEs if noRLOFafterCEE or pessimistic: @@ -129,13 +128,10 @@ def setCOMPASDCOmask( # create a mask for each dco type supplied self.DCOmask = type_masks[types] * hubble_mask * rlof_mask * pessimistic_mask - self.BHBHmask = type_masks["BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.BHBHmask = type_masks["BHBH"] * hubble_mask * rlof_mask * pessimistic_mask self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask - self.NSNSmask = type_masks["BNS"] * hubble_mask * rlof_mask * pessimistic_mask - - # ## MELANIE CHANGE - adding masks for BWD and COWD systems - # self.WDWDmask = type_masks["BWD"] * hubble_mask * rlof_mask * pessimistic_mask - + self.NSNSmask = type_masks["NSNS"] * hubble_mask * rlof_mask * pessimistic_mask + self.WDWDmask = type_masks["WDWD"] * hubble_mask * rlof_mask * pessimistic_mask self.CHE_BHBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.NonCHE_BHBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index fd36a8655..8c47a35c8 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -6,13 +6,16 @@ import scipy from scipy.interpolate import interp1d from scipy.stats import norm as NormDist -from compas_python_utils.cosmic_integration import ClassCOMPAS -from compas_python_utils.cosmic_integration import selection_effects +# from compas_python_utils.cosmic_integration import ClassCOMPAS +import ClassCOMPAS +# from compas_python_utils.cosmic_integration import selection_effects +import selection_effects import warnings import astropy.units as u import argparse import importlib -from compas_python_utils.cosmic_integration.cosmology import get_cosmology +# from compas_python_utils.cosmic_integration.cosmology import get_cosmology +from cosmology import get_cosmology def calculate_redshift_related_params(max_redshift=10.0, max_redshift_detection=1.0, redshift_step=0.001, z_first_SF = 10.0, cosmology=None): """ @@ -310,7 +313,7 @@ def find_detection_probability(Mc, eta, redshifts, distances, n_redshifts_detect return detection_probability -def find_detection_rate(path, dco_type="BBH", merger_output_filename=None, weight_column=None, +def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weight_column=None, merges_hubble_time=True, pessimistic_CEE=True, no_RLOF_after_CEE=True, max_redshift=10.0, max_redshift_detection=1.0, redshift_step=0.001, z_first_SF = 10, use_sampled_mass_ranges=True, m1_min=5 * u.Msun, m1_max=150 * u.Msun, m2_min=0.1 * u.Msun, fbin=0.7, @@ -332,7 +335,7 @@ def find_detection_rate(path, dco_type="BBH", merger_output_filename=None, weigh == Arguments for finding and masking COMPAS file == =================================================== path --> [string] Path to the COMPAS data file that contains the output - dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BBH", "BHNS", "BNS"] + dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BHBH", "BHNS", "NSNS", "WDWD"] merger_output_filename --> [string] Optional name of output file to store merging DCOs (do not create the extra output if None) weight_column --> [string] Name of column in "DoubleCompactObjects" file that contains adaptive sampling weights (Leave this as None if you have unweighted samples) @@ -529,6 +532,8 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C print('shape redshifts', np.shape(redshifts)) print('shape COMPAS.sw_weights', np.shape(COMPAS.sw_weights) ) print('COMPAS.DCOmask', COMPAS.DCOmask, ' was set for dco_type', dco_type) + if dco_type=='all': + print('Note that rates are calculated for ALL systems in the DCO table, this could include WDWD') print('shape COMPAS COMPAS.DCOmask', np.shape(COMPAS.DCOmask) ) ################################################# @@ -579,7 +584,7 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C N_dco_in_z_bin = (merger_rate[:,:] * fine_shell_volumes[:]) print('fine_shell_volumes', fine_shell_volumes) - # The number of merging BBHs that need a weight + # The number of merging BHBHs that need a weight N_dco = len(merger_rate[:,0]) #################### @@ -761,10 +766,10 @@ def parse_cli_args(): parser.add_argument("--path", dest='path', help="Path to the COMPAS file that contains the output", type=str, default="COMPAS_Output.h5") - # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS + # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS, WDWD parser.add_argument("--dco_type", dest='dco_type', - help="Which DCO type you used to calculate rates, one of: ['all', 'BBH', 'BHNS', 'BNS'] ", - type=str, default="BBH") + help="Which DCO type you used to calculate rates, one of: ['all', 'BHBH', 'BHNS', 'NSNS', 'WDWD'] ", + type=str, default="BHBH") parser.add_argument("--weight", dest='weight_column', help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", type=str, default=None) @@ -849,7 +854,7 @@ def main(): dco_type=args.dco_type, weight_column=args.weight_column, pessimistic_CEE=args.remove_pessimistic_CEE, - no_RLOF_after_CEE=args.remove_RLOF_after_CEE + no_RLOF_after_CEE=args.remove_RLOF_after_CEE, max_redshift=args.max_redshift, max_redshift_detection=args.max_redshift_detection, redshift_step=args.redshift_step, From 300070891d5ded918e38b1f4817f22c90ae6ff9d Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Thu, 23 Jan 2025 17:41:34 +0000 Subject: [PATCH 03/47] fixed redhsift and rates to match maxz --- compas_python_utils/cosmic_integration/FastCosmicIntegration.py | 2 +- compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 8c47a35c8..fed333c35 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -614,7 +614,7 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C detection_index = z_index if z_index < n_redshifts_detection else n_redshifts_detection print('You will only save data up to redshift ', maxz, ', i.e. index', z_index) - save_redshifts = redshifts + save_redshifts = redshifts[:z_index] save_merger_rate = merger_rate[:,:z_index] save_detection_rate = detection_rate[:,:detection_index] diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 8d1317d9c..fd34bd641 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -77,6 +77,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin, mass_ratio_pdf_function=l fraction The fraction of mass in a COMPAS population relative to the total Universal population """ + # first, for normalisation purposes, we can find the integral with no COMPAS cuts def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass From 1a084fae084d80317029826c27c73834a9736bb0 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Thu, 23 Jan 2025 17:49:55 +0000 Subject: [PATCH 04/47] making the binary fraction mass dependent --- .../totalMassEvolvedPerZ.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index fd34bd641..91bbc43cf 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -52,7 +52,7 @@ def IMF(m, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): -def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin, mass_ratio_pdf_function=lambda q: 1, +def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_function=lambda q: 1, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): """Calculate the fraction of mass in a COMPAS population relative to the total Universal population. This can be used to normalise the rates of objects from COMPAS simulations. @@ -77,24 +77,40 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin, mass_ratio_pdf_function=l fraction The fraction of mass in a COMPAS population relative to the total Universal population """ - + # Step 0: define mass bins and corresponding binary fractions + # Values chosen to approximately follow Figure 1 from Offner et al. (2023) + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + binaryFractions = [0.1, 0.25, 0.5, 0.75, 1] + def get_binary_fraction(mass): + for i in range(len(binary_bin_edges) - 1): + if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: + return binaryFractions[i] + return 0 # Default value if mass is out of range + # first, for normalisation purposes, we can find the integral with no COMPAS cuts def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass + if f_bin == None: + f_bin = get_binary_fraction(mass) + # find the expected companion mass given the mass ratio pdf function expected_secondary_mass = quad(lambda q: q * mass_ratio_pdf_function(q), 0, 1)[0] * primary_mass single_stars = (1 - f_bin) * primary_mass binary_stars = f_bin * (primary_mass + expected_secondary_mass) return single_stars + binary_stars + full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34))[0] # now we do a similar integral but for the COMPAS regime def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # define the primary mass in the same way primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - + + if f_bin == None: + f_bin = get_binary_fraction(mass) + # find the fraction that are below the m2 mass cut f_below_m2low = quad(mass_ratio_pdf_function, 0, m2_low / mass)[0] @@ -103,7 +119,9 @@ def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # return total mass of binary stars that have m2 above the cut return f_bin * (1 - f_below_m2low) * (primary_mass + expected_secondary_mass) + compas_mass = quad(compas_integral, m1_low, m1_upp, args=(m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34))[0] + return compas_mass / full_mass From ac256f71bf33504e446e32590e79b5c7695ea065 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Thu, 23 Jan 2025 21:51:13 +0000 Subject: [PATCH 05/47] changing fbin to be mass-dependent --- .../cosmic_integration/FastCosmicIntegration.py | 8 ++++---- .../cosmic_integration/totalMassEvolvedPerZ.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index fed333c35..4d09c266d 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -396,7 +396,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig # assert that input will not produce errors assert max_redshift_detection <= max_redshift, "Maximum detection redshift cannot be below maximum redshift" assert m1_min <= m1_max, "Minimum sampled primary mass cannot be above maximum sampled primary mass" - assert np.logical_and(fbin >= 0.0, fbin <= 1.0), "Binary fraction must be between 0 and 1" + # assert np.logical_and(fbin >= 0.0, fbin <= 1.0), "Binary fraction must be between 0 and 1" assert Mc_step < Mc_max, "Chirp mass step size must be less than maximum chirp mass" assert eta_step < eta_max, "Symmetric mass ratio step size must be less than maximum symmetric mass ratio" assert snr_step < snr_max, "SNR step size must be less than maximum SNR" @@ -555,7 +555,6 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C else: print(new_rate_group, 'exists, we will overrwrite the data') - ################################################# # Bin rates by redshifts ################################################# @@ -624,7 +623,8 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C # Write the rates as a separate dataset # re-arrange your list of rate parameters DCO_to_rate_mask = COMPAS.DCOmask #save this bool for easy conversion between BSE_Double_Compact_Objects, and CI weights - rate_data_list = [DCO['SEED'][DCO_to_rate_mask], DCO_to_rate_mask , save_redshifts, save_merger_rate, merger_rate[:,0], save_detection_rate] + DCO_seeds = h_new['BSE_Double_Compact_Objects']['SEED'][DCO_to_rate_mask] # Get DCO seed + rate_data_list = [DCO_seeds, DCO_to_rate_mask , save_redshifts, save_merger_rate, merger_rate[:,0], save_detection_rate] rate_list_names = ['SEED', 'DCOmask', 'redshifts', 'merger_rate','merger_rate_z0', 'detection_rate'+sensitivity] for i, data in enumerate(rate_data_list): print('Adding rate info of shape', np.shape(data)) @@ -798,7 +798,7 @@ def parse_cli_args(): default=150.) parser.add_argument("--m2min", dest='m2_min', help="Minimum secondary mass sampled by COMPAS", type=float, default=0.1) - parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS", type=float, default=0.7) + parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if -1 a f_bin will be changing with mass", type=float, default=0.7) # Parameters determining dP/dZ and SFR(z), default options from Neijssel 2019 parser.add_argument("--mu0", dest='mu0', help="mean metallicity at redshhift 0", type=float, default=0.035) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 91bbc43cf..eddb753cf 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -66,7 +66,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct m2_low : `float` Lower limit on the sampled secondary mass f_bin : `float` - Binary fraction + Binary fraction, if set to -1, you will use a mass-dependent binary fraction mass_ratio_pdf_function : `function`, optional Function to calculate the mass ratio PDF, by default a uniform mass ratio distribution mi, aij : `float` @@ -91,7 +91,7 @@ def get_binary_fraction(mass): def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == None: + if f_bin == -1: f_bin = get_binary_fraction(mass) # find the expected companion mass given the mass ratio pdf function @@ -108,7 +108,7 @@ def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # define the primary mass in the same way primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == None: + if f_bin == -1: f_bin = get_binary_fraction(mass) # find the fraction that are below the m2 mass cut From 7ce37366784d6f610e5bfe57f55f17ffbe86757f Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Wed, 26 Feb 2025 04:17:59 +0000 Subject: [PATCH 06/47] updating binary fraction calculation --- .../totalMassEvolvedPerZ.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index eddb753cf..d34e21088 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -222,16 +222,34 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( p(M) \propto M^-1.3 for M between m2 and m3; p(M) = alpha * M^-2.3 for M between m3 and m4; + m1_min, m1_max are the min and max sampled primary masses + m2_min is the min sampled secondary mass + @Ilya Mandel's derivation """ m1, m2, m3, m4 = imf_mass_bounds if m1_min < m3: raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") + if m1_min > m1_max: + raise ValueError(f"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})") + if m1_max > m4: + raise ValueError(f"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})") + + # normalize IMF over the complete mass range: alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + # average mass of stars (average mass of all binaries is a factor of 1.5 larger) m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) - # fraction of binaries that COMPAS simulates + + # fraction of binaries that COMPAS simulates (N_binaries_in_COMPAS/N_binaries_in_universe) + # i.e., p(m1)p(m2|m1) dm1dm2, which can be rewritten as p(m1)p(q|m1) dm1dq, assuming a flat mass q dist with m2_max = m1_max fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) + + # Average mass of systems (M_rep_by_all_binary_systems/N_binaries_in_universe) + # 1.5 = Average number of stars in single and binary systems, (1-fbin)/fbin) = ratio of single/binary systems + average_mass_per_binary = m_avg * (1.5 + (1-fbin)/fbin) + # mass represented by each binary simulated by COMPAS - m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) + # N_binaries_in_universe/N_binaries_in_COMPAS * M_rep_by_all_binary_systems/N_binaries_in_universe + m_rep = (1/fint) * average_mass_per_binary return m_rep \ No newline at end of file From 868b12c1f4d99176e7245f664d3f88ca47337f47 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Mon, 7 Apr 2025 13:44:18 +0000 Subject: [PATCH 07/47] changed the analytical function for star forming mass per binary --- .../totalMassEvolvedPerZ.py | 78 +++++++++++++++---- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index d34e21088..2aa5d1620 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -80,7 +80,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct # Step 0: define mass bins and corresponding binary fractions # Values chosen to approximately follow Figure 1 from Offner et al. (2023) binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] - binaryFractions = [0.1, 0.25, 0.5, 0.75, 1] + binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] def get_binary_fraction(mass): for i in range(len(binary_bin_edges) - 1): if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: @@ -88,7 +88,7 @@ def get_binary_fraction(mass): return 0 # Default value if mass is out of range # first, for normalisation purposes, we can find the integral with no COMPAS cuts - def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): + def full_integral(mass, m1, m2, m3, m4, a12, a23, a34, f_bin): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass if f_bin == -1: @@ -101,7 +101,7 @@ def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): binary_stars = f_bin * (primary_mass + expected_secondary_mass) return single_stars + binary_stars - full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34))[0] + full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34, f_bin))[0] # now we do a similar integral but for the COMPAS regime def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): @@ -210,6 +210,9 @@ def draw_samples_from_kroupa_imf( return m1_samples[mask] , m2_samples[mask] +################################################### +# New version of analytical calculation +################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] ): @@ -225,9 +228,14 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max are the min and max sampled primary masses m2_min is the min sampled secondary mass - @Ilya Mandel's derivation + This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max + Lieke base on Ilya Mandel's derivation """ + # Kroupa IMF m1, m2, m3, m4 = imf_mass_bounds + continuity_constants = [1./(m2*m3), 1./(m3), 1.0] + IMF_powers = [-0.3, -1.3, -2.3] + if m1_min < m3: raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") if m1_min > m1_max: @@ -237,19 +245,59 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # normalize IMF over the complete mass range: alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + # print('alpha', alpha) - # average mass of stars (average mass of all binaries is a factor of 1.5 larger) - m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) + # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS + # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe + # = 1/fint * 1/fbin * average mass of a stellar system in the Universe - # fraction of binaries that COMPAS simulates (N_binaries_in_COMPAS/N_binaries_in_universe) - # i.e., p(m1)p(m2|m1) dm1dm2, which can be rewritten as p(m1)p(q|m1) dm1dq, assuming a flat mass q dist with m2_max = m1_max + # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) - # Average mass of systems (M_rep_by_all_binary_systems/N_binaries_in_universe) - # 1.5 = Average number of stars in single and binary systems, (1-fbin)/fbin) = ratio of single/binary systems - average_mass_per_binary = m_avg * (1.5 + (1-fbin)/fbin) + # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe + # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction + # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + if fbin == None: + # use a binary fraction that varies with mass + binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] + else: + # otherwise use a constant binary fraction + binaryFractions = [fbin] * 5 + + # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe, + # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to: + # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. + # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins. + piece_wise_integral = 0 + + # For every binary fraction bin + for i in range(len(binary_bin_edges) - 1): + fbin = binaryFractions[i] # Binary fraction for this range + + # And every piece of the Kroupa IMF + for j in range(len(imf_mass_bounds) - 1): + exponent = IMF_powers[j] # IMF exponent for these masses + + # Check if the binary fraction bin overlaps with the IMF mass bin + if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]: + continue # No overlap + + # Integrate from the most narrow range + m_start = max(binary_bin_edges[i], imf_mass_bounds[j]) + m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1]) + + # Compute the definite integral: + integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j] + + # Compute the sum term + sum_term = (1 /fbin + 0.5) * integral + piece_wise_integral += sum_term + + # combining them: + Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral + + # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS + M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin - # mass represented by each binary simulated by COMPAS - # N_binaries_in_universe/N_binaries_in_COMPAS * M_rep_by_all_binary_systems/N_binaries_in_universe - m_rep = (1/fint) * average_mass_per_binary - return m_rep \ No newline at end of file + return M_sf_Univ_per_N_binary_COMPAS \ No newline at end of file From 3f5cad0c717ab2b8d3621487f184bd24e1713036 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Mon, 7 Apr 2025 14:01:45 +0000 Subject: [PATCH 08/47] changed assert to reflect fbin changing with mass --- .../cosmic_integration/FastCosmicIntegration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 808515699..722df345b 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -396,7 +396,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig # assert that input will not produce errors assert max_redshift_detection <= max_redshift, "Maximum detection redshift cannot be below maximum redshift" assert m1_min <= m1_max, "Minimum sampled primary mass cannot be above maximum sampled primary mass" - # assert np.logical_and(fbin >= 0.0, fbin <= 1.0), "Binary fraction must be between 0 and 1" + assert fbin is None or (0.0 <= fbin <= 1.0), "Binary fraction must be between 0 and 1, or if None will vary with mass" assert Mc_step < Mc_max, "Chirp mass step size must be less than maximum chirp mass" assert eta_step < eta_max, "Symmetric mass ratio step size must be less than maximum symmetric mass ratio" assert snr_step < snr_max, "SNR step size must be less than maximum SNR" @@ -804,7 +804,7 @@ def parse_cli_args(): default=150.) parser.add_argument("--m2min", dest='m2_min', help="Minimum secondary mass sampled by COMPAS", type=float, default=0.1) - parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if -1 a f_bin will be changing with mass", type=float, default=0.7) + parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if None f_bin will be changing with mass", type=float, default=0.7) # Parameters determining dP/dZ and SFR(z), default options from Neijssel 2019 parser.add_argument("--mu0", dest='mu0', help="mean metallicity at redshhift 0", type=float, default=0.035) From db569bca5b83d6407e0a62dca66092f74fccec96 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Mon, 7 Apr 2025 14:27:34 +0000 Subject: [PATCH 09/47] Allowed the argument f_bin to be None --- .../cosmic_integration/FastCosmicIntegration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 722df345b..6069eff8c 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -765,7 +765,9 @@ def plot_rates(save_dir, formation_rate, merger_rate, detection_rate, redshifts, else: plt.close() - +# To allow f_binary to be None or Float +def none_or_float(value): + return None if value.lower() == "none" else float(value) def parse_cli_args(): parser = argparse.ArgumentParser() @@ -804,7 +806,7 @@ def parse_cli_args(): default=150.) parser.add_argument("--m2min", dest='m2_min', help="Minimum secondary mass sampled by COMPAS", type=float, default=0.1) - parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if None f_bin will be changing with mass", type=float, default=0.7) + parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if None f_bin will be changing with mass", type=none_or_float, default=0.7) # Parameters determining dP/dZ and SFR(z), default options from Neijssel 2019 parser.add_argument("--mu0", dest='mu0', help="mean metallicity at redshhift 0", type=float, default=0.035) From 27c1b5a95446ee98ead9fa1522af4c45d5af4cee Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 14:24:32 -0400 Subject: [PATCH 10/47] fixing inconsistent naming BBH, and adding NSWD, WDBH options --- .../cosmic_integration/ClassCOMPAS.py | 26 ++++++++++++------- .../FastCosmicIntegration.py | 2 +- .../totalMassEvolvedPerZ.py | 5 +--- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 52307d796..9796a1aef 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -67,7 +67,7 @@ def __init__( def setCOMPASDCOmask( self, types="BHBH", withinHubbleTime=True, pessimistic=True, noRLOFafterCEE=True ): - # By default, we mask for BBHs that merge within a Hubble time, assuming + # By default, we mask for BHBHs that merge within a Hubble time, assuming # the pessimistic CEE prescription (HG donors cannot survive a CEE) and # not allowing immediate RLOF post-CEE @@ -75,14 +75,14 @@ def setCOMPASDCOmask( self.get_COMPAS_variables("BSE_Double_Compact_Objects", ["Stellar_Type(1)", "Stellar_Type(2)", "Merges_Hubble_Time", "SEED"]) dco_seeds = dco_seeds.flatten() - if types == "CHE_BBH" or types == "NON_CHE_BBH": + if types == "CHE_BHBH" or types == "NON_CHE_BHBH": stellar_type_1_zams, stellar_type_2_zams, che_ms_1, che_ms_2, sys_seeds = \ self.get_COMPAS_variables("BSE_System_Parameters", ["Stellar_Type@ZAMS(1)", "Stellar_Type@ZAMS(2)", "CH_on_MS(1)", "CH_on_MS(2)", "SEED"]) che_mask = np.logical_and.reduce((stellar_type_1_zams == 16, stellar_type_2_zams == 16, che_ms_1 == True, che_ms_2 == True)) che_seeds = sys_seeds[()][che_mask] - self.CHE_mask = np.in1d(dco_seeds, che_seeds) if types == "CHE_BBH" or types == "NON_CHE_BBH" else np.repeat(False, len(dco_seeds)) + self.CHE_mask = np.in1d(dco_seeds, che_seeds) if types == "CHE_BHBH" or types == "NON_CHE_BHBH" else np.repeat(False, len(dco_seeds)) # if user wants to mask on Hubble time use the flag, otherwise just set all to True, use astype(bool) to set masks to bool type hubble_mask = hubble_flag.astype(bool) if withinHubbleTime else np.repeat(True, len(dco_seeds)) @@ -91,13 +91,17 @@ def setCOMPASDCOmask( type_masks = { "all": np.repeat(True, len(dco_seeds)), "BHBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), - "BHNS": np.logical_and(np.isin(stellar_type_1,[13,14]),np.isin(stellar_type_2,[13,14])), "NSNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), - "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])) + "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])), + "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14),np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13)) + "NSWD": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 13), + np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 13)), + "WDBH": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 14), + np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 14)), } - type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BHBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) - type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BHBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) + type_masks["CHE_BHBH"] = np.logical_and(self.CHE_mask, type_masks["BHBH"]) if types == "CHE_BHBH" else np.repeat(False, len(dco_seeds)) + type_masks["NON_CHE_BHBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BHBH"]) if types == "NON_CHE_BHBH" else np.repeat(True, len(dco_seeds)) # if the user wants to make RLOF or optimistic CEs if noRLOFafterCEE or pessimistic: @@ -129,11 +133,13 @@ def setCOMPASDCOmask( # create a mask for each dco type supplied self.DCOmask = type_masks[types] * hubble_mask * rlof_mask * pessimistic_mask self.BHBHmask = type_masks["BHBH"] * hubble_mask * rlof_mask * pessimistic_mask - self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask self.NSNSmask = type_masks["NSNS"] * hubble_mask * rlof_mask * pessimistic_mask self.WDWDmask = type_masks["WDWD"] * hubble_mask * rlof_mask * pessimistic_mask - self.CHE_BHBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask - self.NonCHE_BHBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask + self.WDWDmask = type_masks["NSWD"] * hubble_mask * rlof_mask * pessimistic_mask + self.WDWDmask = type_masks["WDBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.CHE_BHBHmask = type_masks["CHE_BHBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.NonCHE_BHBHmask = type_masks["NON_CHE_BHBH"] * hubble_mask * rlof_mask * pessimistic_mask self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask self.optimisticmask = pessimistic_mask diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 6069eff8c..d25f2de32 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -335,7 +335,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig == Arguments for finding and masking COMPAS file == =================================================== path --> [string] Path to the COMPAS data file that contains the output - dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BHBH", "BHNS", "NSNS", "WDWD"] + dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BHBH", "NSNS", "WDWD", "BHNS", "NSWD", "WDBH"] merger_output_filename --> [string] Optional name of output file to store merging DCOs (do not create the extra output if None) weight_column --> [string] Name of column in "DoubleCompactObjects" file that contains adaptive sampling weights (Leave this as None if you have unweighted samples) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 2aa5d1620..6a6b4ee8a 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -210,16 +210,13 @@ def draw_samples_from_kroupa_imf( return m1_samples[mask] , m2_samples[mask] -################################################### -# New version of analytical calculation ################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] ): """ Analytical computation of the mass of stars formed per binary star formed within the - [m1 min, m1 max] and [m2 min, ..] rage, - using the Kroupa IMF: + [m1 min, m1 max] and [m2 min, ..] rage, using the Kroupa IMF: p(M) \propto M^-0.3 for M between m1 and m2 p(M) \propto M^-1.3 for M between m2 and m3; From d129df53bd04971fb9ac43fb3d25c12cb1d21828 Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 14:26:01 -0400 Subject: [PATCH 11/47] fix typo in arg parser help --- compas_python_utils/cosmic_integration/FastCosmicIntegration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index d25f2de32..824394725 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -782,7 +782,7 @@ def parse_cli_args(): help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", type=str, default=None) parser.add_argument("--keep_pessimistic_CEE", dest='remove_pessimistic_CEE', - help="keep_pessimistic_CEE will set remove_pessimistic_CEE to false. The default behaviour (remove_pessimistic_CEE == True), will mask binaries that binaries that experience a CEE while on the HG", + help="keep_pessimistic_CEE will set remove_pessimistic_CEE to false. The default behaviour (remove_pessimistic_CEE == True), will mask binaries that experience a CEE while on the HG", action='store_false', default=True) parser.add_argument("--keepRLOF_postCE", dest='remove_RLOF_after_CEE', help="keepRLOF_postCE will set remove_RLOF_after_CEE to false. The default behaviour (remove_RLOF_after_CEE == True), will mask binaries that have immediate RLOF after a CCE", From 2df0da32721e1994ec2ded968294ee3c820975c7 Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 15:14:47 -0400 Subject: [PATCH 12/47] fixed the commented out imports and few typos --- .../cosmic_integration/ClassCOMPAS.py | 7 +++++-- .../cosmic_integration/FastCosmicIntegration.py | 17 ++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 9796a1aef..c77283412 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -2,7 +2,10 @@ import numpy as np import h5py as h5 import os -# from . import totalMassEvolvedPerZ as MPZ +import sys +# Get the COMPAS_ROOT_DIR var, and add the cosmic_integration directory to the path +compas_root_dir = os.getenv('COMPAS_ROOT_DIR') +sys.path.append(os.path.join(compas_root_dir, 'compas_python_utils/cosmic_integration')) import totalMassEvolvedPerZ as MPZ @@ -93,7 +96,7 @@ def setCOMPASDCOmask( "BHBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), "NSNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])), - "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14),np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13)) + "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14),np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13)), "NSWD": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 13), np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 13)), "WDBH": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 14), diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 824394725..a102d97cb 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -1,21 +1,24 @@ import numpy as np import h5py as h5 import os +import sys import time import matplotlib.pyplot as plt import scipy from scipy.interpolate import interp1d from scipy.stats import norm as NormDist -# from compas_python_utils.cosmic_integration import ClassCOMPAS -import ClassCOMPAS -# from compas_python_utils.cosmic_integration import selection_effects -import selection_effects import warnings import astropy.units as u import argparse import importlib -# from compas_python_utils.cosmic_integration.cosmology import get_cosmology + +# Get the COMPAS_ROOT_DIR var, and add the cosmic_integration directory to the path +compas_root_dir = os.getenv('COMPAS_ROOT_DIR') +sys.path.append(os.path.join(compas_root_dir, 'compas_python_utils/cosmic_integration')) +import ClassCOMPAS from cosmology import get_cosmology +import selection_effects + def calculate_redshift_related_params(max_redshift=10.0, max_redshift_detection=1.0, redshift_step=0.001, z_first_SF = 10.0, cosmology=None): """ @@ -583,7 +586,7 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C N_dco_in_z_bin = (merger_rate[:,:] * fine_shell_volumes[:]) print('fine_shell_volumes', fine_shell_volumes) - # The number of merging BHBHs that need a weight + # The number of merging DCO systems that need a weight N_dco = len(merger_rate[:,0]) #################### @@ -776,7 +779,7 @@ def parse_cli_args(): # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS, WDWD parser.add_argument("--dco_type", dest='dco_type', - help="Which DCO type you used to calculate rates, one of: ['all', 'BHBH', 'BHNS', 'NSNS', 'WDWD'] ", + help="Which DCO type you used to calculate rates, one of: ['all', 'BHBH', 'NSNS', 'WDWD', 'BHNS', 'NSWD', 'WDBH'] ", type=str, default="BHBH") parser.add_argument("--weight", dest='weight_column', help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", From 77bbb99919f1776f713d8738dfbbf123a4559477 Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 15:23:40 -0400 Subject: [PATCH 13/47] Added chekcs for empty DCO, or no systems left after masking --- compas_python_utils/cosmic_integration/ClassCOMPAS.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index c77283412..ef15200f4 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -172,6 +172,9 @@ def setCOMPASData(self): primary_masses, secondary_masses, formation_times, coalescence_times, dco_seeds = \ self.get_COMPAS_variables("BSE_Double_Compact_Objects", ["Mass(1)", "Mass(2)", "Time", "Coalescence_Time", "SEED"]) + # Raise an error if DCO table is empty + if len(primary_masses) == 0: + raise ValueError("BSE_Double_Compact_Objects is empty!") initial_seeds, initial_Z = self.get_COMPAS_variables("BSE_System_Parameters", ["SEED", "Metallicity@ZAMS(1)"]) @@ -187,6 +190,10 @@ def setCOMPASData(self): self.mass1 = primary_masses[self.DCOmask] self.mass2 = secondary_masses[self.DCOmask] + #Check that you have some systems of interest in your DCO table (i.e. len(primary_masses[self.DCOmask])>0 ) + if len(self.mass1) == 0: + raise ValueError("No DCOs found with the current mask. Please check your DCO table, or change your mask settings.") + # Stuff of data I dont need for integral # but I might be to laze to read in myself # and often use. Might turn it of for memory efficiency From 1475fc043621259f42f17425969d3ef9e79e5d6a Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 15:39:53 -0400 Subject: [PATCH 14/47] added dco_type warning to compute_snr_and_detection_grids --- .../cosmic_integration/FastCosmicIntegration.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index a102d97cb..8da39e2c5 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -218,7 +218,7 @@ def find_formation_and_merger_rates(n_binaries, redshifts, times, time_first_SF, merger_rate[i, :first_too_early_index - 1] = formation_rate[i, z_of_formation_index] return formation_rate, merger_rate -def compute_snr_and_detection_grids(sensitivity="O1", snr_threshold=8.0, Mc_max=300.0, Mc_step=0.1, +def compute_snr_and_detection_grids(dco_type, sensitivity="O1", snr_threshold=8.0, Mc_max=300.0, Mc_step=0.1, eta_max=0.25, eta_step=0.01, snr_max=1000.0, snr_step=0.1): """ Compute a grid of SNRs and detection probabilities for a range of masses and SNRs @@ -244,6 +244,10 @@ def compute_snr_and_detection_grids(sensitivity="O1", snr_threshold=8.0, Mc_max= snr_grid_at_1Mpc --> [2D float array] The snr of a binary with masses (Mc, eta) at a distance of 1 Mpc detection_probability_from_snr --> [list of floats] A list of detection probabilities for different SNRs """ + # If DCO type includes a WD, return empty arrays since we currently only support LVK sensitivity + if dco_type in ["WDWD", "NSWD", "WDBH"]: + warnings.warn("!! Detected rate is not computed since DCO type {} doesnt work with LVK sensitivity {}".format(dco_type, sensitivity)) + # get interpolator given sensitivity interpolator = selection_effects.SNRinterpolator(sensitivity) @@ -475,7 +479,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig COMPAS.delayTimes, COMPAS.sw_weights) # create lookup tables for the SNR at 1Mpc as a function of the masses and the probability of detection as a function of SNR - snr_grid_at_1Mpc, detection_probability_from_snr = compute_snr_and_detection_grids(sensitivity, snr_threshold, Mc_max, Mc_step, + snr_grid_at_1Mpc, detection_probability_from_snr = compute_snr_and_detection_grids(dco_type, sensitivity, snr_threshold, Mc_max, Mc_step, eta_max, eta_step, snr_max, snr_step) # use lookup tables to find the probability of detecting each binary at each redshift From fe383dfe187feb207cf6260b6d1eaaff6ba9e425 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Fri, 8 Aug 2025 16:50:08 +0200 Subject: [PATCH 15/47] don't use totalMassEvolvedPerZ in star_forming_mass_per_bin, this is slow and error prone, just directly call get_compas_fraction --- .../cosmic_integration/totalMassEvolvedPerZ.py | 11 ++++++++--- py_tests/test_total_mass_evolved_per_z.py | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 6a6b4ee8a..83046988f 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -144,17 +144,17 @@ def totalMassEvolvedPerZ(path, Mlower, Mupper, m2_low, binaryFraction, mass_rati """ Calculate the total mass evolved per metallicity as a function of redshift in a COMPAS simulation. """ - # calculate the fraction of mass in the COMPAS simulation vs. the real population without sample cuts fraction = get_COMPAS_fraction(m1_low=Mlower, m1_upp=Mupper, m2_low=m2_low, f_bin=binaryFraction, mass_ratio_pdf_function=mass_ratio_pdf_function, m1=m1, m2=m2, m3=m3, m4=m4, a12=a12, a23=a23, a34=a34) multiplicationFactor = 1 / fraction + # LvS: This is slow and buggy! (esp if you sample metallicities smoothly) # get the mass evolved for each metallicity bin and convert to a total mass using the fraction MassEvolvedPerZ = retrieveMassEvolvedPerZ(path) - totalMassEvolvedPerMetallicity = MassEvolvedPerZ / fraction + totalMassEvolvedPerMetallicity = MassEvolvedPerZ / fraction return multiplicationFactor, totalMassEvolvedPerMetallicity @@ -166,7 +166,12 @@ def star_forming_mass_per_binary( """ Calculate the total mass of stars formed per binary star formed within the COMPAS simulation. """ - multiplicationFactor, _ = totalMassEvolvedPerZ(**locals()) + fraction = get_COMPAS_fraction(m1_low=Mlower,m1_upp=Mupper,m2_low=m2_low, + f_bin=binaryFraction,mass_ratio_pdf_function=mass_ratio_pdf_function, + m1=m1, m2=m2, m3=m3, m4=m4, + a12=a12, a23=a23, a34=a34) + + multiplicationFactor = 1 / fraction # get the total mass in COMPAS and number of binaries with h5.File(path, 'r') as f: diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index 8f77ec4ed..bad1ac5da 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -5,12 +5,15 @@ from compas_python_utils.cosmic_integration.binned_cosmic_integrator.bbh_population import \ generate_mock_bbh_population_file import numpy as np +import matplotlib +matplotlib.use("Agg") # Use non-interactive backend + import matplotlib.pyplot as plt import h5py as h5 import pytest -MAKE_PLOTS = False +MAKE_PLOTS = True M1_MIN = 5 M1_MAX = 150 @@ -61,20 +64,23 @@ def test_analytical_function(): def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output, tmpdir, test_archive_dir): + fake_compas_output = '/Users/lvanson/CompasOutput/v02.35.02/FiducialN1e6/MainRun/COMPAS_Output.h5' np.random.seed(42) m1_max = M1_MAX m1_min = M1_MIN m2_min = M2_MIN fbin = 1 - numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(m1_min, m1_max, m2_min, fbin) - + numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) + assert numerical > 0 assert analytical > 0 assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: + tmpdir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' + test_archive_dir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) fig.savefig(f"{test_archive_dir}/analytical_vs_numerical.png") @@ -90,11 +96,11 @@ def plot_star_forming_mass_per_binary_comparison( vals = np.zeros(len(n_samps)) for i, n in enumerate(n_samps): fname = f"{tmpdir}/test_{i}.h5" - generate_mock_bbh_population_file(tmpdir, n_systems=int(n)) + generate_mock_bbh_population_file(fname, n_systems=int(n)) vals[i] = (star_forming_mass_per_binary(fname, m1_min, m1_max, m2_min, fbin)) numerical_vals.append(vals) - # plot the upper and lower bounds of the numerical values + # plot the upper and lower bounds of the numerical values numerical_vals = np.array(numerical_vals) lower = np.percentile(numerical_vals, 5, axis=0) upper = np.percentile(numerical_vals, 95, axis=0) @@ -108,6 +114,7 @@ def plot_star_forming_mass_per_binary_comparison( plt.xlabel("Number of samples") plt.xlim(min(n_samps), max(n_samps)) plt.legend() + plt.savefig(f"{tmpdir}/analytical_vs_numerical.png") return plt.gcf() From 521f7c2cd3f6060cd18fdfe3a124efad41f9347c Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Fri, 8 Aug 2025 23:02:51 +0200 Subject: [PATCH 16/47] cleaned up some print statements --- .../totalMassEvolvedPerZ.py | 19 ++++++++----------- py_tests/test_total_mass_evolved_per_z.py | 12 +++++------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 83046988f..d2abf00cc 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -51,7 +51,6 @@ def IMF(m, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): return 0.0 - def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_function=lambda q: 1, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): """Calculate the fraction of mass in a COMPAS population relative to the total Universal population. This @@ -78,10 +77,10 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct The fraction of mass in a COMPAS population relative to the total Universal population """ # Step 0: define mass bins and corresponding binary fractions - # Values chosen to approximately follow Figure 1 from Offner et al. (2023) - binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] - binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] def get_binary_fraction(mass): + # Values chosen to approximately follow Figure 1 from Offner et al. (2023) + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] for i in range(len(binary_bin_edges) - 1): if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: return binaryFractions[i] @@ -91,7 +90,7 @@ def get_binary_fraction(mass): def full_integral(mass, m1, m2, m3, m4, a12, a23, a34, f_bin): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == -1: + if f_bin == None: f_bin = get_binary_fraction(mass) # find the expected companion mass given the mass ratio pdf function @@ -108,7 +107,7 @@ def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # define the primary mass in the same way primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == -1: + if f_bin == None: f_bin = get_binary_fraction(mass) # find the fraction that are below the m2 mass cut @@ -150,7 +149,7 @@ def totalMassEvolvedPerZ(path, Mlower, Mupper, m2_low, binaryFraction, mass_rati m1=m1, m2=m2, m3=m3, m4=m4, a12=a12, a23=a23, a34=a34) multiplicationFactor = 1 / fraction - # LvS: This is slow and buggy! (esp if you sample metallicities smoothly) + # Warning: This is slow and error prone! esp if you sample metallicities smoothly # get the mass evolved for each metallicity bin and convert to a total mass using the fraction MassEvolvedPerZ = retrieveMassEvolvedPerZ(path) @@ -171,8 +170,6 @@ def star_forming_mass_per_binary( m1=m1, m2=m2, m3=m3, m4=m4, a12=a12, a23=a23, a34=a34) - multiplicationFactor = 1 / fraction - # get the total mass in COMPAS and number of binaries with h5.File(path, 'r') as f: allSystems = f['BSE_System_Parameters'] @@ -181,7 +178,7 @@ def star_forming_mass_per_binary( n_binaries = len(m1s) total_star_forming_mass_in_COMPAS = sum(m1s) + sum(m2s) - total_star_forming_mass = total_star_forming_mass_in_COMPAS * multiplicationFactor + total_star_forming_mass = total_star_forming_mass_in_COMPAS / fraction return total_star_forming_mass / n_binaries @@ -247,7 +244,6 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # normalize IMF over the complete mass range: alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) - # print('alpha', alpha) # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe @@ -256,6 +252,7 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) + print('Analytical fint', fint, ' = N_binaries_in_COMPAS/N_binaries_in_universe') # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index bad1ac5da..a4cae7353 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -5,8 +5,8 @@ from compas_python_utils.cosmic_integration.binned_cosmic_integrator.bbh_population import \ generate_mock_bbh_population_file import numpy as np -import matplotlib -matplotlib.use("Agg") # Use non-interactive backend + +from py_tests.conftest import test_archive_dir, fake_compas_output import matplotlib.pyplot as plt import h5py as h5 @@ -15,7 +15,7 @@ MAKE_PLOTS = True -M1_MIN = 5 +M1_MIN = 10 M1_MAX = 150 M2_MIN = 0.1 @@ -64,12 +64,11 @@ def test_analytical_function(): def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output, tmpdir, test_archive_dir): - fake_compas_output = '/Users/lvanson/CompasOutput/v02.35.02/FiducialN1e6/MainRun/COMPAS_Output.h5' np.random.seed(42) m1_max = M1_MAX m1_min = M1_MIN m2_min = M2_MIN - fbin = 1 + fbin = None analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(m1_min, m1_max, m2_min, fbin) numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) @@ -79,8 +78,6 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: - tmpdir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' - test_archive_dir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) fig.savefig(f"{test_archive_dir}/analytical_vs_numerical.png") @@ -112,6 +109,7 @@ def plot_star_forming_mass_per_binary_comparison( plt.xscale("log") plt.ylabel("Star forming mass per binary [M$_{\odot}$]") plt.xlabel("Number of samples") + plt.ylim(bottom=10) plt.xlim(min(n_samps), max(n_samps)) plt.legend() plt.savefig(f"{tmpdir}/analytical_vs_numerical.png") From e2f36f8e60fe1fdbf365834fd5917e5f1f5f2875 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Sat, 9 Aug 2025 18:05:25 +0200 Subject: [PATCH 17/47] it matters what m_min1 is? --- .../totalMassEvolvedPerZ.py | 35 ++++++++++++++++++- py_tests/test_total_mass_evolved_per_z.py | 8 ++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index d2abf00cc..439577f5f 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -171,6 +171,7 @@ def star_forming_mass_per_binary( a12=a12, a23=a23, a34=a34) # get the total mass in COMPAS and number of binaries + print("Reading COMPAS data from", path) with h5.File(path, 'r') as f: allSystems = f['BSE_System_Parameters'] m1s = (allSystems['Mass@ZAMS(1)'])[()] @@ -212,6 +213,39 @@ def draw_samples_from_kroupa_imf( return m1_samples[mask] , m2_samples[mask] + + + +################################################### +# Old version of analytical calculation +################################################### +# def analytical_star_forming_mass_per_binary_using_kroupa_imf( +# m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] +# ): +# """ +# Analytical computation of the mass of stars formed per binary star formed within the +# [m1 min, m1 max] and [m2 min, ..] rage, +# using the Kroupa IMF: + +# p(M) \propto M^-0.3 for M between m1 and m2 +# p(M) \propto M^-1.3 for M between m2 and m3; +# p(M) = alpha * M^-2.3 for M between m3 and m4; + +# @Ilya Mandel's derivation +# """ +# m1, m2, m3, m4 = imf_mass_bounds +# if m1_min < m3: +# raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") +# alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) +# # average mass of stars (average mass of all binaries is a factor of 1.5 larger) +# m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) +# # fraction of binaries that COMPAS simulates +# fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) +# # mass represented by each binary simulated by COMPAS +# m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) +# return m_rep + + ################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] @@ -252,7 +286,6 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) - print('Analytical fint', fint, ' = N_binaries_in_COMPAS/N_binaries_in_universe') # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index a4cae7353..c4c0aab51 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -15,9 +15,10 @@ MAKE_PLOTS = True -M1_MIN = 10 +M1_MIN = 5 M1_MAX = 150 M2_MIN = 0.1 +F_BIN = 0.5 #None def test_imf(test_archive_dir): @@ -68,7 +69,7 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output m1_max = M1_MAX m1_min = M1_MIN m2_min = M2_MIN - fbin = None + fbin = F_BIN analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(m1_min, m1_max, m2_min, fbin) numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) @@ -79,7 +80,7 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) - fig.savefig(f"{test_archive_dir}/analytical_vs_numerical.png") + fig.savefig(f"{test_archive_dir}/analytical_vs_numerical_const.png") def plot_star_forming_mass_per_binary_comparison( @@ -112,7 +113,6 @@ def plot_star_forming_mass_per_binary_comparison( plt.ylim(bottom=10) plt.xlim(min(n_samps), max(n_samps)) plt.legend() - plt.savefig(f"{tmpdir}/analytical_vs_numerical.png") return plt.gcf() From 0d736174e1366bed9a9e53fc5f1dee08741ac002 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 11 Aug 2025 11:16:43 +0200 Subject: [PATCH 18/47] move test values to separate so generate_mock_bbh_population_file is called consistently with same m1_min and max etc --- .../totalMassEvolvedPerZ.py | 57 +++++++++---------- py_tests/conftest.py | 7 +++ py_tests/test_total_mass_evolved_per_z.py | 22 +++---- py_tests/test_values.py | 6 ++ 4 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 py_tests/test_values.py diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 439577f5f..4cf4b3ed6 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -65,7 +65,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct m2_low : `float` Lower limit on the sampled secondary mass f_bin : `float` - Binary fraction, if set to -1, you will use a mass-dependent binary fraction + Binary fraction, if set to None, you will use a mass-dependent binary fraction mass_ratio_pdf_function : `function`, optional Function to calculate the mass ratio PDF, by default a uniform mass ratio distribution mi, aij : `float` @@ -171,7 +171,6 @@ def star_forming_mass_per_binary( a12=a12, a23=a23, a34=a34) # get the total mass in COMPAS and number of binaries - print("Reading COMPAS data from", path) with h5.File(path, 'r') as f: allSystems = f['BSE_System_Parameters'] m1s = (allSystems['Mass@ZAMS(1)'])[()] @@ -216,34 +215,34 @@ def draw_samples_from_kroupa_imf( -################################################### +################################################## # Old version of analytical calculation -################################################### -# def analytical_star_forming_mass_per_binary_using_kroupa_imf( -# m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] -# ): -# """ -# Analytical computation of the mass of stars formed per binary star formed within the -# [m1 min, m1 max] and [m2 min, ..] rage, -# using the Kroupa IMF: - -# p(M) \propto M^-0.3 for M between m1 and m2 -# p(M) \propto M^-1.3 for M between m2 and m3; -# p(M) = alpha * M^-2.3 for M between m3 and m4; - -# @Ilya Mandel's derivation -# """ -# m1, m2, m3, m4 = imf_mass_bounds -# if m1_min < m3: -# raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") -# alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) -# # average mass of stars (average mass of all binaries is a factor of 1.5 larger) -# m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) -# # fraction of binaries that COMPAS simulates -# fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) -# # mass represented by each binary simulated by COMPAS -# m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) -# return m_rep +################################################## +def old_analytical_star_forming_mass_per_binary_using_kroupa_imf( + m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] +): + """ + Analytical computation of the mass of stars formed per binary star formed within the + [m1 min, m1 max] and [m2 min, ..] rage, + using the Kroupa IMF: + + p(M) \propto M^-0.3 for M between m1 and m2 + p(M) \propto M^-1.3 for M between m2 and m3; + p(M) = alpha * M^-2.3 for M between m3 and m4; + + @Ilya Mandel's derivation + """ + m1, m2, m3, m4 = imf_mass_bounds + if m1_min < m3: + raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") + alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + # average mass of stars (average mass of all binaries is a factor of 1.5 larger) + m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) + # fraction of binaries that COMPAS simulates + fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) + # mass represented by each binary simulated by COMPAS + m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) + return m_rep ################################################### diff --git a/py_tests/conftest.py b/py_tests/conftest.py index 53053e2ed..956f97f78 100644 --- a/py_tests/conftest.py +++ b/py_tests/conftest.py @@ -7,6 +7,10 @@ from compas_python_utils.cosmic_integration.binned_cosmic_integrator.bbh_population import \ generate_mock_bbh_population_file +# Testvalues used in test_total_mass_evolved_per_z defined in py_tests/test_values.py +from py_tests.test_values import MAKE_PLOTS, M1_MIN, M1_MAX, M2_MIN, F_BIN + + HERE = os.path.dirname(__file__) TEST_CONFIG_DIR = os.path.join(HERE, "test_data") TEST_BASH = os.path.join(TEST_CONFIG_DIR, "run.sh") @@ -60,5 +64,8 @@ def fake_compas_output(tmpdir) -> str: fname = f"{tmpdir}/COMPAS_mock_output.h5" generate_mock_bbh_population_file( filename=fname, + m1_min=M1_MIN, + m1_max=M1_MAX, + m2_min=M2_MIN ) return fname \ No newline at end of file diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index c4c0aab51..c27fc831b 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -13,12 +13,8 @@ import pytest -MAKE_PLOTS = True - -M1_MIN = 5 -M1_MAX = 150 -M2_MIN = 0.1 -F_BIN = 0.5 #None +# Testvalues defined in py_tests/test_values.py +from py_tests.test_values import MAKE_PLOTS, M1_MIN, M1_MAX, M2_MIN, F_BIN def test_imf(test_archive_dir): @@ -80,21 +76,27 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) - fig.savefig(f"{test_archive_dir}/analytical_vs_numerical_const.png") + fig.savefig(f"{test_archive_dir}/analytical_vs_numerical_var.png") def plot_star_forming_mass_per_binary_comparison( tmpdir, analytical, m1_min, m1_max, m2_min, fbin, nreps=5, nsamps=5 ): - plt.axhline(analytical, color='tab:blue', label="analytical", ls='--') + # Analytical values + plt.axhline(analytical, color='tab:blue', label=f"analytical fbin = {fbin}", ls='--') + + # Compute numerical values n_samps = np.geomspace(1e3, 5e4, nsamps) numerical_vals = [] for _ in range(nreps): vals = np.zeros(len(n_samps)) for i, n in enumerate(n_samps): fname = f"{tmpdir}/test_{i}.h5" - generate_mock_bbh_population_file(fname, n_systems=int(n)) + + generate_mock_bbh_population_file(filename=fname, n_systems=int(n), + m1_min=m1_min, m1_max=m1_max, m2_min=m2_min) + # generate_mock_bbh_population_file(fname, n_systems=int(n)) vals[i] = (star_forming_mass_per_binary(fname, m1_min, m1_max, m2_min, fbin)) numerical_vals.append(vals) @@ -108,7 +110,7 @@ def plot_star_forming_mass_per_binary_comparison( ) plt.plot(n_samps, np.median(numerical_vals, axis=0), color='tab:orange', label="numerical") plt.xscale("log") - plt.ylabel("Star forming mass per binary [M$_{\odot}$]") + plt.ylabel(r"Star forming mass per binary [M$_{\odot}$]") plt.xlabel("Number of samples") plt.ylim(bottom=10) plt.xlim(min(n_samps), max(n_samps)) diff --git a/py_tests/test_values.py b/py_tests/test_values.py new file mode 100644 index 000000000..f38ef0c28 --- /dev/null +++ b/py_tests/test_values.py @@ -0,0 +1,6 @@ +# Testvalues used in test_total_mass_evolved_per_z.py +MAKE_PLOTS = True +M1_MIN = 5 +M1_MAX = 150 +M2_MIN = 0.1 +F_BIN = None # None = variable f_bin, otherwise fixed value \ No newline at end of file From 3660ba16fc0b214c1e1b205c1ab4685cd2ff9fbb Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 11 Aug 2025 11:35:02 +0200 Subject: [PATCH 19/47] enabled the get_COMPAS_fraction to handle sharp edges of binary bin edges --- .../totalMassEvolvedPerZ.py | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 4cf4b3ed6..47e1a59b9 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -51,75 +51,71 @@ def IMF(m, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): return 0.0 -def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_function=lambda q: 1, - m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): - """Calculate the fraction of mass in a COMPAS population relative to the total Universal population. This - can be used to normalise the rates of objects from COMPAS simulations. + +def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, + mass_ratio_pdf_function=lambda q: 1, + m1=0.01, m2=0.08, m3=0.5, m4=200.0, + a12=0.3, a23=1.3, a34=2.3): + """ + Calculate the fraction of mass in a COMPAS population relative to the total Universal population. + Can be used to normalise the rates of objects from COMPAS simulations. Parameters ---------- - m1_low : `float` - Lower limit on the sampled primary mass - m1_upp : `float` - Upper limit on the sampled primary mass - m2_low : `float` - Lower limit on the sampled secondary mass - f_bin : `float` - Binary fraction, if set to None, you will use a mass-dependent binary fraction - mass_ratio_pdf_function : `function`, optional - Function to calculate the mass ratio PDF, by default a uniform mass ratio distribution - mi, aij : `float` - Settings for the IMF choice, see `IMF` for details, by default follows Kroupa (2001) - - Returns - ------- - fraction - The fraction of mass in a COMPAS population relative to the total Universal population - """ - # Step 0: define mass bins and corresponding binary fractions + m1_low, m1_upp : float + Primary mass cuts in COMPAS simulation + m2_low : float + Secondary mass cutoff + f_bin : float or None + Binary fraction. If None, use a stepwise mass-dependent binary fraction. + mass_ratio_pdf_function : function + PDF of mass ratio q + mi, aij : float + IMF breakpoints and slopes + """ + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + def get_binary_fraction(mass): - # Values chosen to approximately follow Figure 1 from Offner et al. (2023) - binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] for i in range(len(binary_bin_edges) - 1): if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: return binaryFractions[i] - return 0 # Default value if mass is out of range - - # first, for normalisation purposes, we can find the integral with no COMPAS cuts - def full_integral(mass, m1, m2, m3, m4, a12, a23, a34, f_bin): - primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - - if f_bin == None: - f_bin = get_binary_fraction(mass) - - # find the expected companion mass given the mass ratio pdf function - expected_secondary_mass = quad(lambda q: q * mass_ratio_pdf_function(q), 0, 1)[0] * primary_mass - - single_stars = (1 - f_bin) * primary_mass - binary_stars = f_bin * (primary_mass + expected_secondary_mass) - return single_stars + binary_stars - - full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34, f_bin))[0] - - # now we do a similar integral but for the COMPAS regime - def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): - # define the primary mass in the same way - primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - - if f_bin == None: - f_bin = get_binary_fraction(mass) - - # find the fraction that are below the m2 mass cut - f_below_m2low = quad(mass_ratio_pdf_function, 0, m2_low / mass)[0] - - # expectation value of the secondary mass given the m2 cut and mass ratio pdf function - expected_secondary_mass = quad(lambda q: q * mass_ratio_pdf_function(q), m2_low / mass, 1)[0] * primary_mass - - # return total mass of binary stars that have m2 above the cut - return f_bin * (1 - f_below_m2low) * (primary_mass + expected_secondary_mass) - - compas_mass = quad(compas_integral, m1_low, m1_upp, args=(m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34))[0] + return 0 # catch-all + + def IMF_mass(mass): + return IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass + + def integrand_full(mass, f_bin): + local_f_bin = get_binary_fraction(mass) if f_bin is None else f_bin + expected_q = quad(lambda q: q * mass_ratio_pdf_function(q), 0, 1)[0] + return (1 - local_f_bin + local_f_bin * (1 + expected_q)) * IMF_mass(mass) + + def integrand_compas(mass, f_bin): + local_f_bin = get_binary_fraction(mass) if f_bin is None else f_bin + q_min = m2_low / mass + if q_min >= 1: + return 0 # No valid secondaries + f_q = quad(mass_ratio_pdf_function, q_min, 1)[0] + expected_q = quad(lambda q: q * mass_ratio_pdf_function(q), q_min, 1)[0] + return local_f_bin * f_q * (1 + expected_q) * IMF_mass(mass) + + # split integral at binary fraction steps if f_bin is None (i.e. variable and like a step function) + def split_integral(func, a, b, f_bin): + total = 0 + for edge_start, edge_end in zip(binary_bin_edges[:-1], binary_bin_edges[1:]): + left = max(a, edge_start) + right = min(b, edge_end) + if left < right: + result, _ = quad(func, left, right, args=(f_bin,)) + total += result + return total + + if f_bin is None: + full_mass = split_integral(integrand_full, m1, m4, f_bin) + compas_mass = split_integral(integrand_compas, m1_low, m1_upp, f_bin) + else: + full_mass = quad(integrand_full, m1, m4, args=(f_bin,))[0] + compas_mass = quad(integrand_compas, m1_low, m1_upp, args=(f_bin,))[0] return compas_mass / full_mass From 2d64b1af2c8da09ecf9322e3315df2174f2990f9 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Wed, 22 Jan 2025 22:10:29 +0000 Subject: [PATCH 20/47] starting to add WDWD mergers in CI --- .../cosmic_integration/ClassCOMPAS.py | 25 +++++++++++++------ .../FastCosmicIntegration.py | 11 +++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 76ac552b3..9835ff9b7 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -31,12 +31,13 @@ def __init__( self.mass2 = None # Msun self.DCOmask = None self.allTypesMask = None - self.BBHmask = None - self.DNSmask = None + self.BHBHmask = None + self.NSNSmask = None self.BHNSmask = None + self.WDWDmask = None self.CHE_mask = None - self.CHE_BBHmask = None - self.NonCHE_BBHmask = None + self.CHE_BHBHmask = None + self.NonCHE_BHBHmask = None self.initialZ = None self.sw_weights = None self.n_systems = None @@ -91,6 +92,10 @@ def setCOMPASDCOmask( "BBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13), np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14)), "BNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), + + # ## MELANIE CHANGE - defining types of masks for BWDs and COWD systems + # "BWD": np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==11),np.logical_and(stellar_type_1==12,stellar_type_2==12))))))))), + } type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) @@ -124,11 +129,15 @@ def setCOMPASDCOmask( # create a mask for each dco type supplied self.DCOmask = type_masks[types] * hubble_mask * rlof_mask * pessimistic_mask - self.BBHmask = type_masks["BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.BHBHmask = type_masks["BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask - self.DNSmask = type_masks["BNS"] * hubble_mask * rlof_mask * pessimistic_mask - self.CHE_BBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask - self.NonCHE_BBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.NSNSmask = type_masks["BNS"] * hubble_mask * rlof_mask * pessimistic_mask + + # ## MELANIE CHANGE - adding masks for BWD and COWD systems + # self.WDWDmask = type_masks["BWD"] * hubble_mask * rlof_mask * pessimistic_mask + + self.CHE_BHBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.NonCHE_BHBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask self.optimisticmask = pessimistic_mask diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 85c6a30d8..58a9a7e0c 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -766,6 +766,7 @@ def parse_cli_args(): parser = argparse.ArgumentParser() parser.add_argument("--path", dest='path', help="Path to the COMPAS file that contains the output", type=str, default="COMPAS_Output.h5") + # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS parser.add_argument("--dco_type", dest='dco_type', help="Which DCO type you used to calculate rates, one of: ['all', 'BBH', 'BHNS', 'BNS'] ", @@ -773,7 +774,13 @@ def parse_cli_args(): parser.add_argument("--weight", dest='weight_column', help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", type=str, default=None) - + parser.add_argument("--keep_pessimistic_CEE", dest='remove_pessimistic_CEE', + help="keep_pessimistic_CEE will set remove_pessimistic_CEE to false. The default behaviour (remove_pessimistic_CEE == True), will mask binaries that binaries that experience a CEE while on the HG", + action='store_false', default=True) + parser.add_argument("--keepRLOF_postCE", dest='remove_RLOF_after_CEE', + help="keepRLOF_postCE will set remove_RLOF_after_CEE to false. The default behaviour (remove_RLOF_after_CEE == True), will mask binaries that have immediate RLOF after a CCE", + action='store_false', default=True) + # Options for the redshift evolution and detector sensitivity parser.add_argument("--maxz", dest='max_redshift', help="Maximum redshift to use in array", type=float, default=10) parser.add_argument("--zSF", dest='z_first_SF', help="redshift of first star formation", type=float, default=10) @@ -847,6 +854,8 @@ def main(): args.path, dco_type=args.dco_type, weight_column=args.weight_column, + pessimistic_CEE=args.remove_pessimistic_CEE, + no_RLOF_after_CEE=args.remove_RLOF_after_CEE max_redshift=args.max_redshift, max_redshift_detection=args.max_redshift_detection, redshift_step=args.redshift_step, From 2a8082f6786b56f53d3223e579217ca5ba5ac068 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Wed, 22 Jan 2025 22:58:22 +0000 Subject: [PATCH 21/47] more changes to FCI and ClassCOMPAS --- .../cosmic_integration/ClassCOMPAS.py | 30 ++++++++----------- .../FastCosmicIntegration.py | 25 +++++++++------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 9835ff9b7..52307d796 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -2,7 +2,8 @@ import numpy as np import h5py as h5 import os -from . import totalMassEvolvedPerZ as MPZ +# from . import totalMassEvolvedPerZ as MPZ +import totalMassEvolvedPerZ as MPZ class COMPASData(object): @@ -64,7 +65,7 @@ def __init__( print(" and optionally self.setGridAndMassEvolved() if using a metallicity grid") def setCOMPASDCOmask( - self, types="BBH", withinHubbleTime=True, pessimistic=True, noRLOFafterCEE=True + self, types="BHBH", withinHubbleTime=True, pessimistic=True, noRLOFafterCEE=True ): # By default, we mask for BBHs that merge within a Hubble time, assuming # the pessimistic CEE prescription (HG donors cannot survive a CEE) and @@ -89,16 +90,14 @@ def setCOMPASDCOmask( # mask on stellar types (where 14=BH and 13=NS), BHNS can be BHNS or NSBH type_masks = { "all": np.repeat(True, len(dco_seeds)), - "BBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), - "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13), np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14)), - "BNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), - - # ## MELANIE CHANGE - defining types of masks for BWDs and COWD systems - # "BWD": np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==12,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==12),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==11),np.logical_or(np.logical_and(stellar_type_1==10,stellar_type_2==10),np.logical_or(np.logical_and(stellar_type_1==11,stellar_type_2==11),np.logical_and(stellar_type_1==12,stellar_type_2==12))))))))), - + "BHBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), + "BHNS": np.logical_and(np.isin(stellar_type_1,[13,14]),np.isin(stellar_type_2,[13,14])), + "NSNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), + "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])) } - type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) - type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) + + type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BHBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) + type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BHBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) # if the user wants to make RLOF or optimistic CEs if noRLOFafterCEE or pessimistic: @@ -129,13 +128,10 @@ def setCOMPASDCOmask( # create a mask for each dco type supplied self.DCOmask = type_masks[types] * hubble_mask * rlof_mask * pessimistic_mask - self.BHBHmask = type_masks["BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.BHBHmask = type_masks["BHBH"] * hubble_mask * rlof_mask * pessimistic_mask self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask - self.NSNSmask = type_masks["BNS"] * hubble_mask * rlof_mask * pessimistic_mask - - # ## MELANIE CHANGE - adding masks for BWD and COWD systems - # self.WDWDmask = type_masks["BWD"] * hubble_mask * rlof_mask * pessimistic_mask - + self.NSNSmask = type_masks["NSNS"] * hubble_mask * rlof_mask * pessimistic_mask + self.WDWDmask = type_masks["WDWD"] * hubble_mask * rlof_mask * pessimistic_mask self.CHE_BHBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.NonCHE_BHBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 58a9a7e0c..469d29556 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -6,13 +6,16 @@ import scipy from scipy.interpolate import interp1d from scipy.stats import norm as NormDist -from compas_python_utils.cosmic_integration import ClassCOMPAS -from compas_python_utils.cosmic_integration import selection_effects +# from compas_python_utils.cosmic_integration import ClassCOMPAS +import ClassCOMPAS +# from compas_python_utils.cosmic_integration import selection_effects +import selection_effects import warnings import astropy.units as u import argparse import importlib -from compas_python_utils.cosmic_integration.cosmology import get_cosmology +# from compas_python_utils.cosmic_integration.cosmology import get_cosmology +from cosmology import get_cosmology def calculate_redshift_related_params(max_redshift=10.0, max_redshift_detection=1.0, redshift_step=0.001, z_first_SF = 10.0, cosmology=None): """ @@ -310,7 +313,7 @@ def find_detection_probability(Mc, eta, redshifts, distances, n_redshifts_detect return detection_probability -def find_detection_rate(path, dco_type="BBH", merger_output_filename=None, weight_column=None, +def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weight_column=None, merges_hubble_time=True, pessimistic_CEE=True, no_RLOF_after_CEE=True, max_redshift=10.0, max_redshift_detection=1.0, redshift_step=0.001, z_first_SF = 10, use_sampled_mass_ranges=True, m1_min=5 * u.Msun, m1_max=150 * u.Msun, m2_min=0.1 * u.Msun, fbin=0.7, @@ -332,7 +335,7 @@ def find_detection_rate(path, dco_type="BBH", merger_output_filename=None, weigh == Arguments for finding and masking COMPAS file == =================================================== path --> [string] Path to the COMPAS data file that contains the output - dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BBH", "BHNS", "BNS"] + dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BHBH", "BHNS", "NSNS", "WDWD"] merger_output_filename --> [string] Optional name of output file to store merging DCOs (do not create the extra output if None) weight_column --> [string] Name of column in "DoubleCompactObjects" file that contains adaptive sampling weights (Leave this as None if you have unweighted samples) @@ -529,6 +532,8 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C print('shape redshifts', np.shape(redshifts)) print('shape COMPAS.sw_weights', np.shape(COMPAS.sw_weights) ) print('COMPAS.DCOmask', COMPAS.DCOmask, ' was set for dco_type', dco_type) + if dco_type=='all': + print('Note that rates are calculated for ALL systems in the DCO table, this could include WDWD') print('shape COMPAS COMPAS.DCOmask', np.shape(COMPAS.DCOmask) ) ################################################# @@ -579,7 +584,7 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C N_dco_in_z_bin = (merger_rate[:,:] * fine_shell_volumes[:]) print('fine_shell_volumes', fine_shell_volumes) - # The number of merging BBHs that need a weight + # The number of merging BHBHs that need a weight N_dco = len(merger_rate[:,0]) #################### @@ -767,10 +772,10 @@ def parse_cli_args(): parser.add_argument("--path", dest='path', help="Path to the COMPAS file that contains the output", type=str, default="COMPAS_Output.h5") - # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS + # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS, WDWD parser.add_argument("--dco_type", dest='dco_type', - help="Which DCO type you used to calculate rates, one of: ['all', 'BBH', 'BHNS', 'BNS'] ", - type=str, default="BBH") + help="Which DCO type you used to calculate rates, one of: ['all', 'BHBH', 'BHNS', 'NSNS', 'WDWD'] ", + type=str, default="BHBH") parser.add_argument("--weight", dest='weight_column', help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", type=str, default=None) @@ -855,7 +860,7 @@ def main(): dco_type=args.dco_type, weight_column=args.weight_column, pessimistic_CEE=args.remove_pessimistic_CEE, - no_RLOF_after_CEE=args.remove_RLOF_after_CEE + no_RLOF_after_CEE=args.remove_RLOF_after_CEE, max_redshift=args.max_redshift, max_redshift_detection=args.max_redshift_detection, redshift_step=args.redshift_step, From e856195f9e1688c9c742c7cba6616ad346617c4c Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Thu, 23 Jan 2025 17:41:34 +0000 Subject: [PATCH 22/47] fixed redhsift and rates to match maxz --- compas_python_utils/cosmic_integration/FastCosmicIntegration.py | 2 +- compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 469d29556..581f284dc 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -614,7 +614,7 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C detection_index = z_index if z_index < n_redshifts_detection else n_redshifts_detection print('You will only save data up to redshift ', maxz, ', i.e. index', z_index) - save_redshifts = redshifts + save_redshifts = redshifts[:z_index] save_merger_rate = merger_rate[:,:z_index] save_detection_rate = detection_rate[:,:detection_index] diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 8d1317d9c..fd34bd641 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -77,6 +77,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin, mass_ratio_pdf_function=l fraction The fraction of mass in a COMPAS population relative to the total Universal population """ + # first, for normalisation purposes, we can find the integral with no COMPAS cuts def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass From bab93b360fc578ea7798bf154068ac05b2e99f4a Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Thu, 23 Jan 2025 17:49:55 +0000 Subject: [PATCH 23/47] making the binary fraction mass dependent --- .../totalMassEvolvedPerZ.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index fd34bd641..91bbc43cf 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -52,7 +52,7 @@ def IMF(m, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): -def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin, mass_ratio_pdf_function=lambda q: 1, +def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_function=lambda q: 1, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): """Calculate the fraction of mass in a COMPAS population relative to the total Universal population. This can be used to normalise the rates of objects from COMPAS simulations. @@ -77,24 +77,40 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin, mass_ratio_pdf_function=l fraction The fraction of mass in a COMPAS population relative to the total Universal population """ - + # Step 0: define mass bins and corresponding binary fractions + # Values chosen to approximately follow Figure 1 from Offner et al. (2023) + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + binaryFractions = [0.1, 0.25, 0.5, 0.75, 1] + def get_binary_fraction(mass): + for i in range(len(binary_bin_edges) - 1): + if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: + return binaryFractions[i] + return 0 # Default value if mass is out of range + # first, for normalisation purposes, we can find the integral with no COMPAS cuts def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass + if f_bin == None: + f_bin = get_binary_fraction(mass) + # find the expected companion mass given the mass ratio pdf function expected_secondary_mass = quad(lambda q: q * mass_ratio_pdf_function(q), 0, 1)[0] * primary_mass single_stars = (1 - f_bin) * primary_mass binary_stars = f_bin * (primary_mass + expected_secondary_mass) return single_stars + binary_stars + full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34))[0] # now we do a similar integral but for the COMPAS regime def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # define the primary mass in the same way primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - + + if f_bin == None: + f_bin = get_binary_fraction(mass) + # find the fraction that are below the m2 mass cut f_below_m2low = quad(mass_ratio_pdf_function, 0, m2_low / mass)[0] @@ -103,7 +119,9 @@ def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # return total mass of binary stars that have m2 above the cut return f_bin * (1 - f_below_m2low) * (primary_mass + expected_secondary_mass) + compas_mass = quad(compas_integral, m1_low, m1_upp, args=(m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34))[0] + return compas_mass / full_mass From c756e2e12e91780344eca5ab26a40a0455387159 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Thu, 23 Jan 2025 21:51:13 +0000 Subject: [PATCH 24/47] changing fbin to be mass-dependent --- .../cosmic_integration/FastCosmicIntegration.py | 8 ++++---- .../cosmic_integration/totalMassEvolvedPerZ.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 581f284dc..808515699 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -396,7 +396,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig # assert that input will not produce errors assert max_redshift_detection <= max_redshift, "Maximum detection redshift cannot be below maximum redshift" assert m1_min <= m1_max, "Minimum sampled primary mass cannot be above maximum sampled primary mass" - assert np.logical_and(fbin >= 0.0, fbin <= 1.0), "Binary fraction must be between 0 and 1" + # assert np.logical_and(fbin >= 0.0, fbin <= 1.0), "Binary fraction must be between 0 and 1" assert Mc_step < Mc_max, "Chirp mass step size must be less than maximum chirp mass" assert eta_step < eta_max, "Symmetric mass ratio step size must be less than maximum symmetric mass ratio" assert snr_step < snr_max, "SNR step size must be less than maximum SNR" @@ -555,7 +555,6 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C else: print(new_rate_group, 'exists, we will overrwrite the data') - ################################################# # Bin rates by redshifts ################################################# @@ -624,7 +623,8 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C # Write the rates as a separate dataset # re-arrange your list of rate parameters DCO_to_rate_mask = COMPAS.DCOmask #save this bool for easy conversion between BSE_Double_Compact_Objects, and CI weights - rate_data_list = [DCO['SEED'][DCO_to_rate_mask], DCO_to_rate_mask , save_redshifts, save_merger_rate, merger_rate[:,0], save_detection_rate] + DCO_seeds = h_new['BSE_Double_Compact_Objects']['SEED'][DCO_to_rate_mask] # Get DCO seed + rate_data_list = [DCO_seeds, DCO_to_rate_mask , save_redshifts, save_merger_rate, merger_rate[:,0], save_detection_rate] rate_list_names = ['SEED', 'DCOmask', 'redshifts', 'merger_rate','merger_rate_z0', 'detection_rate'+sensitivity] for i, data in enumerate(rate_data_list): print('Adding rate info of shape', np.shape(data)) @@ -804,7 +804,7 @@ def parse_cli_args(): default=150.) parser.add_argument("--m2min", dest='m2_min', help="Minimum secondary mass sampled by COMPAS", type=float, default=0.1) - parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS", type=float, default=0.7) + parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if -1 a f_bin will be changing with mass", type=float, default=0.7) # Parameters determining dP/dZ and SFR(z), default options from Neijssel 2019 parser.add_argument("--mu0", dest='mu0', help="mean metallicity at redshhift 0", type=float, default=0.035) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 91bbc43cf..eddb753cf 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -66,7 +66,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct m2_low : `float` Lower limit on the sampled secondary mass f_bin : `float` - Binary fraction + Binary fraction, if set to -1, you will use a mass-dependent binary fraction mass_ratio_pdf_function : `function`, optional Function to calculate the mass ratio PDF, by default a uniform mass ratio distribution mi, aij : `float` @@ -91,7 +91,7 @@ def get_binary_fraction(mass): def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == None: + if f_bin == -1: f_bin = get_binary_fraction(mass) # find the expected companion mass given the mass ratio pdf function @@ -108,7 +108,7 @@ def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # define the primary mass in the same way primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == None: + if f_bin == -1: f_bin = get_binary_fraction(mass) # find the fraction that are below the m2 mass cut From 8ef014985c8a892cfe4b7163f6712b4d23013ccc Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Wed, 26 Feb 2025 04:17:59 +0000 Subject: [PATCH 25/47] updating binary fraction calculation --- .../totalMassEvolvedPerZ.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index eddb753cf..d34e21088 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -222,16 +222,34 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( p(M) \propto M^-1.3 for M between m2 and m3; p(M) = alpha * M^-2.3 for M between m3 and m4; + m1_min, m1_max are the min and max sampled primary masses + m2_min is the min sampled secondary mass + @Ilya Mandel's derivation """ m1, m2, m3, m4 = imf_mass_bounds if m1_min < m3: raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") + if m1_min > m1_max: + raise ValueError(f"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})") + if m1_max > m4: + raise ValueError(f"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})") + + # normalize IMF over the complete mass range: alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + # average mass of stars (average mass of all binaries is a factor of 1.5 larger) m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) - # fraction of binaries that COMPAS simulates + + # fraction of binaries that COMPAS simulates (N_binaries_in_COMPAS/N_binaries_in_universe) + # i.e., p(m1)p(m2|m1) dm1dm2, which can be rewritten as p(m1)p(q|m1) dm1dq, assuming a flat mass q dist with m2_max = m1_max fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) + + # Average mass of systems (M_rep_by_all_binary_systems/N_binaries_in_universe) + # 1.5 = Average number of stars in single and binary systems, (1-fbin)/fbin) = ratio of single/binary systems + average_mass_per_binary = m_avg * (1.5 + (1-fbin)/fbin) + # mass represented by each binary simulated by COMPAS - m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) + # N_binaries_in_universe/N_binaries_in_COMPAS * M_rep_by_all_binary_systems/N_binaries_in_universe + m_rep = (1/fint) * average_mass_per_binary return m_rep \ No newline at end of file From 82379394094cff2704a710a1808b6912814d5445 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Mon, 7 Apr 2025 13:44:18 +0000 Subject: [PATCH 26/47] changed the analytical function for star forming mass per binary --- .../totalMassEvolvedPerZ.py | 78 +++++++++++++++---- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index d34e21088..2aa5d1620 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -80,7 +80,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct # Step 0: define mass bins and corresponding binary fractions # Values chosen to approximately follow Figure 1 from Offner et al. (2023) binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] - binaryFractions = [0.1, 0.25, 0.5, 0.75, 1] + binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] def get_binary_fraction(mass): for i in range(len(binary_bin_edges) - 1): if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: @@ -88,7 +88,7 @@ def get_binary_fraction(mass): return 0 # Default value if mass is out of range # first, for normalisation purposes, we can find the integral with no COMPAS cuts - def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): + def full_integral(mass, m1, m2, m3, m4, a12, a23, a34, f_bin): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass if f_bin == -1: @@ -101,7 +101,7 @@ def full_integral(mass, m1, m2, m3, m4, a12, a23, a34): binary_stars = f_bin * (primary_mass + expected_secondary_mass) return single_stars + binary_stars - full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34))[0] + full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34, f_bin))[0] # now we do a similar integral but for the COMPAS regime def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): @@ -210,6 +210,9 @@ def draw_samples_from_kroupa_imf( return m1_samples[mask] , m2_samples[mask] +################################################### +# New version of analytical calculation +################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] ): @@ -225,9 +228,14 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max are the min and max sampled primary masses m2_min is the min sampled secondary mass - @Ilya Mandel's derivation + This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max + Lieke base on Ilya Mandel's derivation """ + # Kroupa IMF m1, m2, m3, m4 = imf_mass_bounds + continuity_constants = [1./(m2*m3), 1./(m3), 1.0] + IMF_powers = [-0.3, -1.3, -2.3] + if m1_min < m3: raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") if m1_min > m1_max: @@ -237,19 +245,59 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # normalize IMF over the complete mass range: alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + # print('alpha', alpha) - # average mass of stars (average mass of all binaries is a factor of 1.5 larger) - m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) + # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS + # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe + # = 1/fint * 1/fbin * average mass of a stellar system in the Universe - # fraction of binaries that COMPAS simulates (N_binaries_in_COMPAS/N_binaries_in_universe) - # i.e., p(m1)p(m2|m1) dm1dm2, which can be rewritten as p(m1)p(q|m1) dm1dq, assuming a flat mass q dist with m2_max = m1_max + # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) - # Average mass of systems (M_rep_by_all_binary_systems/N_binaries_in_universe) - # 1.5 = Average number of stars in single and binary systems, (1-fbin)/fbin) = ratio of single/binary systems - average_mass_per_binary = m_avg * (1.5 + (1-fbin)/fbin) + # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe + # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction + # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + if fbin == None: + # use a binary fraction that varies with mass + binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] + else: + # otherwise use a constant binary fraction + binaryFractions = [fbin] * 5 + + # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe, + # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to: + # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. + # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins. + piece_wise_integral = 0 + + # For every binary fraction bin + for i in range(len(binary_bin_edges) - 1): + fbin = binaryFractions[i] # Binary fraction for this range + + # And every piece of the Kroupa IMF + for j in range(len(imf_mass_bounds) - 1): + exponent = IMF_powers[j] # IMF exponent for these masses + + # Check if the binary fraction bin overlaps with the IMF mass bin + if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]: + continue # No overlap + + # Integrate from the most narrow range + m_start = max(binary_bin_edges[i], imf_mass_bounds[j]) + m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1]) + + # Compute the definite integral: + integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j] + + # Compute the sum term + sum_term = (1 /fbin + 0.5) * integral + piece_wise_integral += sum_term + + # combining them: + Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral + + # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS + M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin - # mass represented by each binary simulated by COMPAS - # N_binaries_in_universe/N_binaries_in_COMPAS * M_rep_by_all_binary_systems/N_binaries_in_universe - m_rep = (1/fint) * average_mass_per_binary - return m_rep \ No newline at end of file + return M_sf_Univ_per_N_binary_COMPAS \ No newline at end of file From 8344454d92c897a4c26e65f0b3ce8e342477ac2c Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Mon, 7 Apr 2025 14:01:45 +0000 Subject: [PATCH 27/47] changed assert to reflect fbin changing with mass --- .../cosmic_integration/FastCosmicIntegration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 808515699..722df345b 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -396,7 +396,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig # assert that input will not produce errors assert max_redshift_detection <= max_redshift, "Maximum detection redshift cannot be below maximum redshift" assert m1_min <= m1_max, "Minimum sampled primary mass cannot be above maximum sampled primary mass" - # assert np.logical_and(fbin >= 0.0, fbin <= 1.0), "Binary fraction must be between 0 and 1" + assert fbin is None or (0.0 <= fbin <= 1.0), "Binary fraction must be between 0 and 1, or if None will vary with mass" assert Mc_step < Mc_max, "Chirp mass step size must be less than maximum chirp mass" assert eta_step < eta_max, "Symmetric mass ratio step size must be less than maximum symmetric mass ratio" assert snr_step < snr_max, "SNR step size must be less than maximum SNR" @@ -804,7 +804,7 @@ def parse_cli_args(): default=150.) parser.add_argument("--m2min", dest='m2_min', help="Minimum secondary mass sampled by COMPAS", type=float, default=0.1) - parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if -1 a f_bin will be changing with mass", type=float, default=0.7) + parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if None f_bin will be changing with mass", type=float, default=0.7) # Parameters determining dP/dZ and SFR(z), default options from Neijssel 2019 parser.add_argument("--mu0", dest='mu0', help="mean metallicity at redshhift 0", type=float, default=0.035) From 3d2bddb18696f8015c7e68b1a997167e236f8d99 Mon Sep 17 00:00:00 2001 From: Melanie Santiago Date: Mon, 7 Apr 2025 14:27:34 +0000 Subject: [PATCH 28/47] Allowed the argument f_bin to be None --- .../cosmic_integration/FastCosmicIntegration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 722df345b..6069eff8c 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -765,7 +765,9 @@ def plot_rates(save_dir, formation_rate, merger_rate, detection_rate, redshifts, else: plt.close() - +# To allow f_binary to be None or Float +def none_or_float(value): + return None if value.lower() == "none" else float(value) def parse_cli_args(): parser = argparse.ArgumentParser() @@ -804,7 +806,7 @@ def parse_cli_args(): default=150.) parser.add_argument("--m2min", dest='m2_min', help="Minimum secondary mass sampled by COMPAS", type=float, default=0.1) - parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if None f_bin will be changing with mass", type=float, default=0.7) + parser.add_argument("--fbin", dest='fbin', help="Binary fraction used by COMPAS, if None f_bin will be changing with mass", type=none_or_float, default=0.7) # Parameters determining dP/dZ and SFR(z), default options from Neijssel 2019 parser.add_argument("--mu0", dest='mu0', help="mean metallicity at redshhift 0", type=float, default=0.035) From 5ab15a5048f254634f80db69608a037fbafc3bae Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 14:24:32 -0400 Subject: [PATCH 29/47] fixing inconsistent naming BBH, and adding NSWD, WDBH options --- .../cosmic_integration/ClassCOMPAS.py | 26 ++++++++++++------- .../FastCosmicIntegration.py | 2 +- .../totalMassEvolvedPerZ.py | 5 +--- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 52307d796..9796a1aef 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -67,7 +67,7 @@ def __init__( def setCOMPASDCOmask( self, types="BHBH", withinHubbleTime=True, pessimistic=True, noRLOFafterCEE=True ): - # By default, we mask for BBHs that merge within a Hubble time, assuming + # By default, we mask for BHBHs that merge within a Hubble time, assuming # the pessimistic CEE prescription (HG donors cannot survive a CEE) and # not allowing immediate RLOF post-CEE @@ -75,14 +75,14 @@ def setCOMPASDCOmask( self.get_COMPAS_variables("BSE_Double_Compact_Objects", ["Stellar_Type(1)", "Stellar_Type(2)", "Merges_Hubble_Time", "SEED"]) dco_seeds = dco_seeds.flatten() - if types == "CHE_BBH" or types == "NON_CHE_BBH": + if types == "CHE_BHBH" or types == "NON_CHE_BHBH": stellar_type_1_zams, stellar_type_2_zams, che_ms_1, che_ms_2, sys_seeds = \ self.get_COMPAS_variables("BSE_System_Parameters", ["Stellar_Type@ZAMS(1)", "Stellar_Type@ZAMS(2)", "CH_on_MS(1)", "CH_on_MS(2)", "SEED"]) che_mask = np.logical_and.reduce((stellar_type_1_zams == 16, stellar_type_2_zams == 16, che_ms_1 == True, che_ms_2 == True)) che_seeds = sys_seeds[()][che_mask] - self.CHE_mask = np.in1d(dco_seeds, che_seeds) if types == "CHE_BBH" or types == "NON_CHE_BBH" else np.repeat(False, len(dco_seeds)) + self.CHE_mask = np.in1d(dco_seeds, che_seeds) if types == "CHE_BHBH" or types == "NON_CHE_BHBH" else np.repeat(False, len(dco_seeds)) # if user wants to mask on Hubble time use the flag, otherwise just set all to True, use astype(bool) to set masks to bool type hubble_mask = hubble_flag.astype(bool) if withinHubbleTime else np.repeat(True, len(dco_seeds)) @@ -91,13 +91,17 @@ def setCOMPASDCOmask( type_masks = { "all": np.repeat(True, len(dco_seeds)), "BHBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), - "BHNS": np.logical_and(np.isin(stellar_type_1,[13,14]),np.isin(stellar_type_2,[13,14])), "NSNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), - "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])) + "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])), + "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14),np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13)) + "NSWD": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 13), + np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 13)), + "WDBH": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 14), + np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 14)), } - type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BHBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) - type_masks["NON_CHE_BBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BHBH"]) if types == "NON_CHE_BBH" else np.repeat(True, len(dco_seeds)) + type_masks["CHE_BHBH"] = np.logical_and(self.CHE_mask, type_masks["BHBH"]) if types == "CHE_BHBH" else np.repeat(False, len(dco_seeds)) + type_masks["NON_CHE_BHBH"] = np.logical_and(np.logical_not(self.CHE_mask), type_masks["BHBH"]) if types == "NON_CHE_BHBH" else np.repeat(True, len(dco_seeds)) # if the user wants to make RLOF or optimistic CEs if noRLOFafterCEE or pessimistic: @@ -129,11 +133,13 @@ def setCOMPASDCOmask( # create a mask for each dco type supplied self.DCOmask = type_masks[types] * hubble_mask * rlof_mask * pessimistic_mask self.BHBHmask = type_masks["BHBH"] * hubble_mask * rlof_mask * pessimistic_mask - self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask self.NSNSmask = type_masks["NSNS"] * hubble_mask * rlof_mask * pessimistic_mask self.WDWDmask = type_masks["WDWD"] * hubble_mask * rlof_mask * pessimistic_mask - self.CHE_BHBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask - self.NonCHE_BHBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask + self.WDWDmask = type_masks["NSWD"] * hubble_mask * rlof_mask * pessimistic_mask + self.WDWDmask = type_masks["WDBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.CHE_BHBHmask = type_masks["CHE_BHBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.NonCHE_BHBHmask = type_masks["NON_CHE_BHBH"] * hubble_mask * rlof_mask * pessimistic_mask self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask self.optimisticmask = pessimistic_mask diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 6069eff8c..d25f2de32 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -335,7 +335,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig == Arguments for finding and masking COMPAS file == =================================================== path --> [string] Path to the COMPAS data file that contains the output - dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BHBH", "BHNS", "NSNS", "WDWD"] + dco_type --> [string] Which DCO type to calculate rates for: one of ["all", "BHBH", "NSNS", "WDWD", "BHNS", "NSWD", "WDBH"] merger_output_filename --> [string] Optional name of output file to store merging DCOs (do not create the extra output if None) weight_column --> [string] Name of column in "DoubleCompactObjects" file that contains adaptive sampling weights (Leave this as None if you have unweighted samples) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 2aa5d1620..6a6b4ee8a 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -210,16 +210,13 @@ def draw_samples_from_kroupa_imf( return m1_samples[mask] , m2_samples[mask] -################################################### -# New version of analytical calculation ################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] ): """ Analytical computation of the mass of stars formed per binary star formed within the - [m1 min, m1 max] and [m2 min, ..] rage, - using the Kroupa IMF: + [m1 min, m1 max] and [m2 min, ..] rage, using the Kroupa IMF: p(M) \propto M^-0.3 for M between m1 and m2 p(M) \propto M^-1.3 for M between m2 and m3; From 6708a6e698ab5990c3ecb084d10b94322bdd3d19 Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 14:26:01 -0400 Subject: [PATCH 30/47] fix typo in arg parser help --- compas_python_utils/cosmic_integration/FastCosmicIntegration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index d25f2de32..824394725 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -782,7 +782,7 @@ def parse_cli_args(): help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", type=str, default=None) parser.add_argument("--keep_pessimistic_CEE", dest='remove_pessimistic_CEE', - help="keep_pessimistic_CEE will set remove_pessimistic_CEE to false. The default behaviour (remove_pessimistic_CEE == True), will mask binaries that binaries that experience a CEE while on the HG", + help="keep_pessimistic_CEE will set remove_pessimistic_CEE to false. The default behaviour (remove_pessimistic_CEE == True), will mask binaries that experience a CEE while on the HG", action='store_false', default=True) parser.add_argument("--keepRLOF_postCE", dest='remove_RLOF_after_CEE', help="keepRLOF_postCE will set remove_RLOF_after_CEE to false. The default behaviour (remove_RLOF_after_CEE == True), will mask binaries that have immediate RLOF after a CCE", From 17bd64dfdc38d11a115e61f110b6cfd6aa98f98b Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 15:14:47 -0400 Subject: [PATCH 31/47] fixed the commented out imports and few typos --- .../cosmic_integration/ClassCOMPAS.py | 7 +++++-- .../cosmic_integration/FastCosmicIntegration.py | 17 ++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index 9796a1aef..c77283412 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -2,7 +2,10 @@ import numpy as np import h5py as h5 import os -# from . import totalMassEvolvedPerZ as MPZ +import sys +# Get the COMPAS_ROOT_DIR var, and add the cosmic_integration directory to the path +compas_root_dir = os.getenv('COMPAS_ROOT_DIR') +sys.path.append(os.path.join(compas_root_dir, 'compas_python_utils/cosmic_integration')) import totalMassEvolvedPerZ as MPZ @@ -93,7 +96,7 @@ def setCOMPASDCOmask( "BHBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), "NSNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), "WDWD": np.logical_and(np.isin(stellar_type_1,[10,11,12]),np.isin(stellar_type_2,[10,11,12])), - "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14),np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13)) + "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14),np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13)), "NSWD": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 13), np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 13)), "WDBH": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 14), diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 824394725..a102d97cb 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -1,21 +1,24 @@ import numpy as np import h5py as h5 import os +import sys import time import matplotlib.pyplot as plt import scipy from scipy.interpolate import interp1d from scipy.stats import norm as NormDist -# from compas_python_utils.cosmic_integration import ClassCOMPAS -import ClassCOMPAS -# from compas_python_utils.cosmic_integration import selection_effects -import selection_effects import warnings import astropy.units as u import argparse import importlib -# from compas_python_utils.cosmic_integration.cosmology import get_cosmology + +# Get the COMPAS_ROOT_DIR var, and add the cosmic_integration directory to the path +compas_root_dir = os.getenv('COMPAS_ROOT_DIR') +sys.path.append(os.path.join(compas_root_dir, 'compas_python_utils/cosmic_integration')) +import ClassCOMPAS from cosmology import get_cosmology +import selection_effects + def calculate_redshift_related_params(max_redshift=10.0, max_redshift_detection=1.0, redshift_step=0.001, z_first_SF = 10.0, cosmology=None): """ @@ -583,7 +586,7 @@ def append_rates(path, detection_rate, formation_rate, merger_rate, redshifts, C N_dco_in_z_bin = (merger_rate[:,:] * fine_shell_volumes[:]) print('fine_shell_volumes', fine_shell_volumes) - # The number of merging BHBHs that need a weight + # The number of merging DCO systems that need a weight N_dco = len(merger_rate[:,0]) #################### @@ -776,7 +779,7 @@ def parse_cli_args(): # For what DCO would you like the rate? options: ALL, BHBH, BHNS NSNS, WDWD parser.add_argument("--dco_type", dest='dco_type', - help="Which DCO type you used to calculate rates, one of: ['all', 'BHBH', 'BHNS', 'NSNS', 'WDWD'] ", + help="Which DCO type you used to calculate rates, one of: ['all', 'BHBH', 'NSNS', 'WDWD', 'BHNS', 'NSWD', 'WDBH'] ", type=str, default="BHBH") parser.add_argument("--weight", dest='weight_column', help="Name of column w AIS sampling weights, i.e. 'mixture_weight'(leave as None for unweighted samples) ", From d154a0218ca33ee0dd809f30b7b8936ec457f68a Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 15:23:40 -0400 Subject: [PATCH 32/47] Added chekcs for empty DCO, or no systems left after masking --- compas_python_utils/cosmic_integration/ClassCOMPAS.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index c77283412..ef15200f4 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -172,6 +172,9 @@ def setCOMPASData(self): primary_masses, secondary_masses, formation_times, coalescence_times, dco_seeds = \ self.get_COMPAS_variables("BSE_Double_Compact_Objects", ["Mass(1)", "Mass(2)", "Time", "Coalescence_Time", "SEED"]) + # Raise an error if DCO table is empty + if len(primary_masses) == 0: + raise ValueError("BSE_Double_Compact_Objects is empty!") initial_seeds, initial_Z = self.get_COMPAS_variables("BSE_System_Parameters", ["SEED", "Metallicity@ZAMS(1)"]) @@ -187,6 +190,10 @@ def setCOMPASData(self): self.mass1 = primary_masses[self.DCOmask] self.mass2 = secondary_masses[self.DCOmask] + #Check that you have some systems of interest in your DCO table (i.e. len(primary_masses[self.DCOmask])>0 ) + if len(self.mass1) == 0: + raise ValueError("No DCOs found with the current mask. Please check your DCO table, or change your mask settings.") + # Stuff of data I dont need for integral # but I might be to laze to read in myself # and often use. Might turn it of for memory efficiency From a110bc908aff70d8dda4cc12ffe6260ca19e24c8 Mon Sep 17 00:00:00 2001 From: LiekeVanSon Date: Tue, 22 Apr 2025 15:39:53 -0400 Subject: [PATCH 33/47] added dco_type warning to compute_snr_and_detection_grids --- .../cosmic_integration/FastCosmicIntegration.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index a102d97cb..8da39e2c5 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -218,7 +218,7 @@ def find_formation_and_merger_rates(n_binaries, redshifts, times, time_first_SF, merger_rate[i, :first_too_early_index - 1] = formation_rate[i, z_of_formation_index] return formation_rate, merger_rate -def compute_snr_and_detection_grids(sensitivity="O1", snr_threshold=8.0, Mc_max=300.0, Mc_step=0.1, +def compute_snr_and_detection_grids(dco_type, sensitivity="O1", snr_threshold=8.0, Mc_max=300.0, Mc_step=0.1, eta_max=0.25, eta_step=0.01, snr_max=1000.0, snr_step=0.1): """ Compute a grid of SNRs and detection probabilities for a range of masses and SNRs @@ -244,6 +244,10 @@ def compute_snr_and_detection_grids(sensitivity="O1", snr_threshold=8.0, Mc_max= snr_grid_at_1Mpc --> [2D float array] The snr of a binary with masses (Mc, eta) at a distance of 1 Mpc detection_probability_from_snr --> [list of floats] A list of detection probabilities for different SNRs """ + # If DCO type includes a WD, return empty arrays since we currently only support LVK sensitivity + if dco_type in ["WDWD", "NSWD", "WDBH"]: + warnings.warn("!! Detected rate is not computed since DCO type {} doesnt work with LVK sensitivity {}".format(dco_type, sensitivity)) + # get interpolator given sensitivity interpolator = selection_effects.SNRinterpolator(sensitivity) @@ -475,7 +479,7 @@ def find_detection_rate(path, dco_type="BHBH", merger_output_filename=None, weig COMPAS.delayTimes, COMPAS.sw_weights) # create lookup tables for the SNR at 1Mpc as a function of the masses and the probability of detection as a function of SNR - snr_grid_at_1Mpc, detection_probability_from_snr = compute_snr_and_detection_grids(sensitivity, snr_threshold, Mc_max, Mc_step, + snr_grid_at_1Mpc, detection_probability_from_snr = compute_snr_and_detection_grids(dco_type, sensitivity, snr_threshold, Mc_max, Mc_step, eta_max, eta_step, snr_max, snr_step) # use lookup tables to find the probability of detecting each binary at each redshift From 3fd136b0beca109e222a41be96d80123e98b7b63 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Fri, 8 Aug 2025 16:50:08 +0200 Subject: [PATCH 34/47] don't use totalMassEvolvedPerZ in star_forming_mass_per_bin, this is slow and error prone, just directly call get_compas_fraction --- .../cosmic_integration/totalMassEvolvedPerZ.py | 11 ++++++++--- py_tests/test_total_mass_evolved_per_z.py | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 6a6b4ee8a..83046988f 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -144,17 +144,17 @@ def totalMassEvolvedPerZ(path, Mlower, Mupper, m2_low, binaryFraction, mass_rati """ Calculate the total mass evolved per metallicity as a function of redshift in a COMPAS simulation. """ - # calculate the fraction of mass in the COMPAS simulation vs. the real population without sample cuts fraction = get_COMPAS_fraction(m1_low=Mlower, m1_upp=Mupper, m2_low=m2_low, f_bin=binaryFraction, mass_ratio_pdf_function=mass_ratio_pdf_function, m1=m1, m2=m2, m3=m3, m4=m4, a12=a12, a23=a23, a34=a34) multiplicationFactor = 1 / fraction + # LvS: This is slow and buggy! (esp if you sample metallicities smoothly) # get the mass evolved for each metallicity bin and convert to a total mass using the fraction MassEvolvedPerZ = retrieveMassEvolvedPerZ(path) - totalMassEvolvedPerMetallicity = MassEvolvedPerZ / fraction + totalMassEvolvedPerMetallicity = MassEvolvedPerZ / fraction return multiplicationFactor, totalMassEvolvedPerMetallicity @@ -166,7 +166,12 @@ def star_forming_mass_per_binary( """ Calculate the total mass of stars formed per binary star formed within the COMPAS simulation. """ - multiplicationFactor, _ = totalMassEvolvedPerZ(**locals()) + fraction = get_COMPAS_fraction(m1_low=Mlower,m1_upp=Mupper,m2_low=m2_low, + f_bin=binaryFraction,mass_ratio_pdf_function=mass_ratio_pdf_function, + m1=m1, m2=m2, m3=m3, m4=m4, + a12=a12, a23=a23, a34=a34) + + multiplicationFactor = 1 / fraction # get the total mass in COMPAS and number of binaries with h5.File(path, 'r') as f: diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index fac88e4d6..9279167a3 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -5,12 +5,15 @@ from compas_python_utils.cosmic_integration.binned_cosmic_integrator.binary_population import \ generate_mock_population import numpy as np +import matplotlib +matplotlib.use("Agg") # Use non-interactive backend + import matplotlib.pyplot as plt import h5py as h5 import pytest -MAKE_PLOTS = False +MAKE_PLOTS = True M1_MIN = 5 M1_MAX = 150 @@ -61,20 +64,23 @@ def test_analytical_function(): def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output, tmpdir, test_archive_dir): + fake_compas_output = '/Users/lvanson/CompasOutput/v02.35.02/FiducialN1e6/MainRun/COMPAS_Output.h5' np.random.seed(42) m1_max = M1_MAX m1_min = M1_MIN m2_min = M2_MIN fbin = 1 - numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(m1_min, m1_max, m2_min, fbin) - + numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) + assert numerical > 0 assert analytical > 0 assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: + tmpdir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' + test_archive_dir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) fig.savefig(f"{test_archive_dir}/analytical_vs_numerical.png") @@ -90,11 +96,11 @@ def plot_star_forming_mass_per_binary_comparison( vals = np.zeros(len(n_samps)) for i, n in enumerate(n_samps): fname = f"{tmpdir}/test_{i}.h5" - generate_mock_population(tmpdir, n_systems=int(n)) + generate_mock_bbh_population_file(fname, n_systems=int(n)) vals[i] = (star_forming_mass_per_binary(fname, m1_min, m1_max, m2_min, fbin)) numerical_vals.append(vals) - # plot the upper and lower bounds of the numerical values + # plot the upper and lower bounds of the numerical values numerical_vals = np.array(numerical_vals) lower = np.percentile(numerical_vals, 5, axis=0) upper = np.percentile(numerical_vals, 95, axis=0) @@ -108,6 +114,7 @@ def plot_star_forming_mass_per_binary_comparison( plt.xlabel("Number of samples") plt.xlim(min(n_samps), max(n_samps)) plt.legend() + plt.savefig(f"{tmpdir}/analytical_vs_numerical.png") return plt.gcf() From cd8023b17f091a42d91bc22dd3a03f60f5e1dd00 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Fri, 8 Aug 2025 23:02:51 +0200 Subject: [PATCH 35/47] cleaned up some print statements --- .../totalMassEvolvedPerZ.py | 19 ++++++++----------- py_tests/test_total_mass_evolved_per_z.py | 12 +++++------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 83046988f..d2abf00cc 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -51,7 +51,6 @@ def IMF(m, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): return 0.0 - def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_function=lambda q: 1, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): """Calculate the fraction of mass in a COMPAS population relative to the total Universal population. This @@ -78,10 +77,10 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct The fraction of mass in a COMPAS population relative to the total Universal population """ # Step 0: define mass bins and corresponding binary fractions - # Values chosen to approximately follow Figure 1 from Offner et al. (2023) - binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] - binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] def get_binary_fraction(mass): + # Values chosen to approximately follow Figure 1 from Offner et al. (2023) + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] for i in range(len(binary_bin_edges) - 1): if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: return binaryFractions[i] @@ -91,7 +90,7 @@ def get_binary_fraction(mass): def full_integral(mass, m1, m2, m3, m4, a12, a23, a34, f_bin): primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == -1: + if f_bin == None: f_bin = get_binary_fraction(mass) # find the expected companion mass given the mass ratio pdf function @@ -108,7 +107,7 @@ def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): # define the primary mass in the same way primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - if f_bin == -1: + if f_bin == None: f_bin = get_binary_fraction(mass) # find the fraction that are below the m2 mass cut @@ -150,7 +149,7 @@ def totalMassEvolvedPerZ(path, Mlower, Mupper, m2_low, binaryFraction, mass_rati m1=m1, m2=m2, m3=m3, m4=m4, a12=a12, a23=a23, a34=a34) multiplicationFactor = 1 / fraction - # LvS: This is slow and buggy! (esp if you sample metallicities smoothly) + # Warning: This is slow and error prone! esp if you sample metallicities smoothly # get the mass evolved for each metallicity bin and convert to a total mass using the fraction MassEvolvedPerZ = retrieveMassEvolvedPerZ(path) @@ -171,8 +170,6 @@ def star_forming_mass_per_binary( m1=m1, m2=m2, m3=m3, m4=m4, a12=a12, a23=a23, a34=a34) - multiplicationFactor = 1 / fraction - # get the total mass in COMPAS and number of binaries with h5.File(path, 'r') as f: allSystems = f['BSE_System_Parameters'] @@ -181,7 +178,7 @@ def star_forming_mass_per_binary( n_binaries = len(m1s) total_star_forming_mass_in_COMPAS = sum(m1s) + sum(m2s) - total_star_forming_mass = total_star_forming_mass_in_COMPAS * multiplicationFactor + total_star_forming_mass = total_star_forming_mass_in_COMPAS / fraction return total_star_forming_mass / n_binaries @@ -247,7 +244,6 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # normalize IMF over the complete mass range: alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) - # print('alpha', alpha) # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe @@ -256,6 +252,7 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) + print('Analytical fint', fint, ' = N_binaries_in_COMPAS/N_binaries_in_universe') # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index 9279167a3..444c45518 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -5,8 +5,8 @@ from compas_python_utils.cosmic_integration.binned_cosmic_integrator.binary_population import \ generate_mock_population import numpy as np -import matplotlib -matplotlib.use("Agg") # Use non-interactive backend + +from py_tests.conftest import test_archive_dir, fake_compas_output import matplotlib.pyplot as plt import h5py as h5 @@ -15,7 +15,7 @@ MAKE_PLOTS = True -M1_MIN = 5 +M1_MIN = 10 M1_MAX = 150 M2_MIN = 0.1 @@ -64,12 +64,11 @@ def test_analytical_function(): def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output, tmpdir, test_archive_dir): - fake_compas_output = '/Users/lvanson/CompasOutput/v02.35.02/FiducialN1e6/MainRun/COMPAS_Output.h5' np.random.seed(42) m1_max = M1_MAX m1_min = M1_MIN m2_min = M2_MIN - fbin = 1 + fbin = None analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(m1_min, m1_max, m2_min, fbin) numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) @@ -79,8 +78,6 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: - tmpdir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' - test_archive_dir = '/Users/lvanson/Documents/Projects/Proj_Melanie/output' fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) fig.savefig(f"{test_archive_dir}/analytical_vs_numerical.png") @@ -112,6 +109,7 @@ def plot_star_forming_mass_per_binary_comparison( plt.xscale("log") plt.ylabel("Star forming mass per binary [M$_{\odot}$]") plt.xlabel("Number of samples") + plt.ylim(bottom=10) plt.xlim(min(n_samps), max(n_samps)) plt.legend() plt.savefig(f"{tmpdir}/analytical_vs_numerical.png") From 841070ce2054452dcdff29943d2aed926f556615 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Sat, 9 Aug 2025 18:05:25 +0200 Subject: [PATCH 36/47] it matters what m_min1 is? --- .../totalMassEvolvedPerZ.py | 35 ++++++++++++++++++- py_tests/test_total_mass_evolved_per_z.py | 8 ++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index d2abf00cc..439577f5f 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -171,6 +171,7 @@ def star_forming_mass_per_binary( a12=a12, a23=a23, a34=a34) # get the total mass in COMPAS and number of binaries + print("Reading COMPAS data from", path) with h5.File(path, 'r') as f: allSystems = f['BSE_System_Parameters'] m1s = (allSystems['Mass@ZAMS(1)'])[()] @@ -212,6 +213,39 @@ def draw_samples_from_kroupa_imf( return m1_samples[mask] , m2_samples[mask] + + + +################################################### +# Old version of analytical calculation +################################################### +# def analytical_star_forming_mass_per_binary_using_kroupa_imf( +# m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] +# ): +# """ +# Analytical computation of the mass of stars formed per binary star formed within the +# [m1 min, m1 max] and [m2 min, ..] rage, +# using the Kroupa IMF: + +# p(M) \propto M^-0.3 for M between m1 and m2 +# p(M) \propto M^-1.3 for M between m2 and m3; +# p(M) = alpha * M^-2.3 for M between m3 and m4; + +# @Ilya Mandel's derivation +# """ +# m1, m2, m3, m4 = imf_mass_bounds +# if m1_min < m3: +# raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") +# alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) +# # average mass of stars (average mass of all binaries is a factor of 1.5 larger) +# m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) +# # fraction of binaries that COMPAS simulates +# fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) +# # mass represented by each binary simulated by COMPAS +# m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) +# return m_rep + + ################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] @@ -252,7 +286,6 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) - print('Analytical fint', fint, ' = N_binaries_in_COMPAS/N_binaries_in_universe') # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index 444c45518..0be22f22d 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -15,9 +15,10 @@ MAKE_PLOTS = True -M1_MIN = 10 +M1_MIN = 5 M1_MAX = 150 M2_MIN = 0.1 +F_BIN = 0.5 #None def test_imf(test_archive_dir): @@ -68,7 +69,7 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output m1_max = M1_MAX m1_min = M1_MIN m2_min = M2_MIN - fbin = None + fbin = F_BIN analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(m1_min, m1_max, m2_min, fbin) numerical = star_forming_mass_per_binary(fake_compas_output, m1_min, m1_max, m2_min, fbin) @@ -79,7 +80,7 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) - fig.savefig(f"{test_archive_dir}/analytical_vs_numerical.png") + fig.savefig(f"{test_archive_dir}/analytical_vs_numerical_const.png") def plot_star_forming_mass_per_binary_comparison( @@ -112,7 +113,6 @@ def plot_star_forming_mass_per_binary_comparison( plt.ylim(bottom=10) plt.xlim(min(n_samps), max(n_samps)) plt.legend() - plt.savefig(f"{tmpdir}/analytical_vs_numerical.png") return plt.gcf() From 4aabb4042931e7d72649741cc7bb44bff9fb3bb1 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 11 Aug 2025 11:16:43 +0200 Subject: [PATCH 37/47] move test values to separate so generate_mock_bbh_population_file is called consistently with same m1_min and max etc --- .../totalMassEvolvedPerZ.py | 57 +++++++++---------- py_tests/conftest.py | 7 +++ py_tests/test_total_mass_evolved_per_z.py | 22 +++---- py_tests/test_values.py | 6 ++ 4 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 py_tests/test_values.py diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 439577f5f..4cf4b3ed6 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -65,7 +65,7 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_funct m2_low : `float` Lower limit on the sampled secondary mass f_bin : `float` - Binary fraction, if set to -1, you will use a mass-dependent binary fraction + Binary fraction, if set to None, you will use a mass-dependent binary fraction mass_ratio_pdf_function : `function`, optional Function to calculate the mass ratio PDF, by default a uniform mass ratio distribution mi, aij : `float` @@ -171,7 +171,6 @@ def star_forming_mass_per_binary( a12=a12, a23=a23, a34=a34) # get the total mass in COMPAS and number of binaries - print("Reading COMPAS data from", path) with h5.File(path, 'r') as f: allSystems = f['BSE_System_Parameters'] m1s = (allSystems['Mass@ZAMS(1)'])[()] @@ -216,34 +215,34 @@ def draw_samples_from_kroupa_imf( -################################################### +################################################## # Old version of analytical calculation -################################################### -# def analytical_star_forming_mass_per_binary_using_kroupa_imf( -# m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] -# ): -# """ -# Analytical computation of the mass of stars formed per binary star formed within the -# [m1 min, m1 max] and [m2 min, ..] rage, -# using the Kroupa IMF: - -# p(M) \propto M^-0.3 for M between m1 and m2 -# p(M) \propto M^-1.3 for M between m2 and m3; -# p(M) = alpha * M^-2.3 for M between m3 and m4; - -# @Ilya Mandel's derivation -# """ -# m1, m2, m3, m4 = imf_mass_bounds -# if m1_min < m3: -# raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") -# alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) -# # average mass of stars (average mass of all binaries is a factor of 1.5 larger) -# m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) -# # fraction of binaries that COMPAS simulates -# fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) -# # mass represented by each binary simulated by COMPAS -# m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) -# return m_rep +################################################## +def old_analytical_star_forming_mass_per_binary_using_kroupa_imf( + m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] +): + """ + Analytical computation of the mass of stars formed per binary star formed within the + [m1 min, m1 max] and [m2 min, ..] rage, + using the Kroupa IMF: + + p(M) \propto M^-0.3 for M between m1 and m2 + p(M) \propto M^-1.3 for M between m2 and m3; + p(M) = alpha * M^-2.3 for M between m3 and m4; + + @Ilya Mandel's derivation + """ + m1, m2, m3, m4 = imf_mass_bounds + if m1_min < m3: + raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") + alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + # average mass of stars (average mass of all binaries is a factor of 1.5 larger) + m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) + # fraction of binaries that COMPAS simulates + fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) + # mass represented by each binary simulated by COMPAS + m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) + return m_rep ################################################### diff --git a/py_tests/conftest.py b/py_tests/conftest.py index 1af7d4650..2124a3698 100644 --- a/py_tests/conftest.py +++ b/py_tests/conftest.py @@ -7,6 +7,10 @@ from compas_python_utils.cosmic_integration.binned_cosmic_integrator.binary_population import \ generate_mock_population +# Testvalues used in test_total_mass_evolved_per_z defined in py_tests/test_values.py +from py_tests.test_values import MAKE_PLOTS, M1_MIN, M1_MAX, M2_MIN, F_BIN + + HERE = os.path.dirname(__file__) TEST_CONFIG_DIR = os.path.join(HERE, "test_data") TEST_BASH = os.path.join(TEST_CONFIG_DIR, "run.sh") @@ -60,5 +64,8 @@ def fake_compas_output(tmpdir) -> str: fname = f"{tmpdir}/COMPAS_mock_output.h5" generate_mock_population( filename=fname, + m1_min=M1_MIN, + m1_max=M1_MAX, + m2_min=M2_MIN ) return fname \ No newline at end of file diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index 0be22f22d..1c23900b8 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -13,12 +13,8 @@ import pytest -MAKE_PLOTS = True - -M1_MIN = 5 -M1_MAX = 150 -M2_MIN = 0.1 -F_BIN = 0.5 #None +# Testvalues defined in py_tests/test_values.py +from py_tests.test_values import MAKE_PLOTS, M1_MIN, M1_MAX, M2_MIN, F_BIN def test_imf(test_archive_dir): @@ -80,21 +76,27 @@ def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output assert np.isclose(numerical, analytical, rtol=1) if MAKE_PLOTS: fig = plot_star_forming_mass_per_binary_comparison(tmpdir, analytical, m1_min, m1_max, m2_min, fbin) - fig.savefig(f"{test_archive_dir}/analytical_vs_numerical_const.png") + fig.savefig(f"{test_archive_dir}/analytical_vs_numerical_var.png") def plot_star_forming_mass_per_binary_comparison( tmpdir, analytical, m1_min, m1_max, m2_min, fbin, nreps=5, nsamps=5 ): - plt.axhline(analytical, color='tab:blue', label="analytical", ls='--') + # Analytical values + plt.axhline(analytical, color='tab:blue', label=f"analytical fbin = {fbin}", ls='--') + + # Compute numerical values n_samps = np.geomspace(1e3, 5e4, nsamps) numerical_vals = [] for _ in range(nreps): vals = np.zeros(len(n_samps)) for i, n in enumerate(n_samps): fname = f"{tmpdir}/test_{i}.h5" - generate_mock_bbh_population_file(fname, n_systems=int(n)) + + generate_mock_bbh_population_file(filename=fname, n_systems=int(n), + m1_min=m1_min, m1_max=m1_max, m2_min=m2_min) + # generate_mock_bbh_population_file(fname, n_systems=int(n)) vals[i] = (star_forming_mass_per_binary(fname, m1_min, m1_max, m2_min, fbin)) numerical_vals.append(vals) @@ -108,7 +110,7 @@ def plot_star_forming_mass_per_binary_comparison( ) plt.plot(n_samps, np.median(numerical_vals, axis=0), color='tab:orange', label="numerical") plt.xscale("log") - plt.ylabel("Star forming mass per binary [M$_{\odot}$]") + plt.ylabel(r"Star forming mass per binary [M$_{\odot}$]") plt.xlabel("Number of samples") plt.ylim(bottom=10) plt.xlim(min(n_samps), max(n_samps)) diff --git a/py_tests/test_values.py b/py_tests/test_values.py new file mode 100644 index 000000000..f38ef0c28 --- /dev/null +++ b/py_tests/test_values.py @@ -0,0 +1,6 @@ +# Testvalues used in test_total_mass_evolved_per_z.py +MAKE_PLOTS = True +M1_MIN = 5 +M1_MAX = 150 +M2_MIN = 0.1 +F_BIN = None # None = variable f_bin, otherwise fixed value \ No newline at end of file From c5cb2b75c7fdd9f397b79e4c7c99f8ccfe2460a8 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 11 Aug 2025 11:35:02 +0200 Subject: [PATCH 38/47] enabled the get_COMPAS_fraction to handle sharp edges of binary bin edges --- .../totalMassEvolvedPerZ.py | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 4cf4b3ed6..47e1a59b9 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -51,75 +51,71 @@ def IMF(m, m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): return 0.0 -def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mass_ratio_pdf_function=lambda q: 1, - m1=0.01, m2=0.08, m3=0.5, m4=200.0, a12=0.3, a23=1.3, a34=2.3): - """Calculate the fraction of mass in a COMPAS population relative to the total Universal population. This - can be used to normalise the rates of objects from COMPAS simulations. + +def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, + mass_ratio_pdf_function=lambda q: 1, + m1=0.01, m2=0.08, m3=0.5, m4=200.0, + a12=0.3, a23=1.3, a34=2.3): + """ + Calculate the fraction of mass in a COMPAS population relative to the total Universal population. + Can be used to normalise the rates of objects from COMPAS simulations. Parameters ---------- - m1_low : `float` - Lower limit on the sampled primary mass - m1_upp : `float` - Upper limit on the sampled primary mass - m2_low : `float` - Lower limit on the sampled secondary mass - f_bin : `float` - Binary fraction, if set to None, you will use a mass-dependent binary fraction - mass_ratio_pdf_function : `function`, optional - Function to calculate the mass ratio PDF, by default a uniform mass ratio distribution - mi, aij : `float` - Settings for the IMF choice, see `IMF` for details, by default follows Kroupa (2001) - - Returns - ------- - fraction - The fraction of mass in a COMPAS population relative to the total Universal population - """ - # Step 0: define mass bins and corresponding binary fractions + m1_low, m1_upp : float + Primary mass cuts in COMPAS simulation + m2_low : float + Secondary mass cutoff + f_bin : float or None + Binary fraction. If None, use a stepwise mass-dependent binary fraction. + mass_ratio_pdf_function : function + PDF of mass ratio q + mi, aij : float + IMF breakpoints and slopes + """ + binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + def get_binary_fraction(mass): - # Values chosen to approximately follow Figure 1 from Offner et al. (2023) - binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] for i in range(len(binary_bin_edges) - 1): if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: return binaryFractions[i] - return 0 # Default value if mass is out of range - - # first, for normalisation purposes, we can find the integral with no COMPAS cuts - def full_integral(mass, m1, m2, m3, m4, a12, a23, a34, f_bin): - primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - - if f_bin == None: - f_bin = get_binary_fraction(mass) - - # find the expected companion mass given the mass ratio pdf function - expected_secondary_mass = quad(lambda q: q * mass_ratio_pdf_function(q), 0, 1)[0] * primary_mass - - single_stars = (1 - f_bin) * primary_mass - binary_stars = f_bin * (primary_mass + expected_secondary_mass) - return single_stars + binary_stars - - full_mass = quad(full_integral, m1, m4, args=(m1, m2, m3, m4, a12, a23, a34, f_bin))[0] - - # now we do a similar integral but for the COMPAS regime - def compas_integral(mass, m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34): - # define the primary mass in the same way - primary_mass = IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass - - if f_bin == None: - f_bin = get_binary_fraction(mass) - - # find the fraction that are below the m2 mass cut - f_below_m2low = quad(mass_ratio_pdf_function, 0, m2_low / mass)[0] - - # expectation value of the secondary mass given the m2 cut and mass ratio pdf function - expected_secondary_mass = quad(lambda q: q * mass_ratio_pdf_function(q), m2_low / mass, 1)[0] * primary_mass - - # return total mass of binary stars that have m2 above the cut - return f_bin * (1 - f_below_m2low) * (primary_mass + expected_secondary_mass) - - compas_mass = quad(compas_integral, m1_low, m1_upp, args=(m2_low, f_bin, m1, m2, m3, m4, a12, a23, a34))[0] + return 0 # catch-all + + def IMF_mass(mass): + return IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass + + def integrand_full(mass, f_bin): + local_f_bin = get_binary_fraction(mass) if f_bin is None else f_bin + expected_q = quad(lambda q: q * mass_ratio_pdf_function(q), 0, 1)[0] + return (1 - local_f_bin + local_f_bin * (1 + expected_q)) * IMF_mass(mass) + + def integrand_compas(mass, f_bin): + local_f_bin = get_binary_fraction(mass) if f_bin is None else f_bin + q_min = m2_low / mass + if q_min >= 1: + return 0 # No valid secondaries + f_q = quad(mass_ratio_pdf_function, q_min, 1)[0] + expected_q = quad(lambda q: q * mass_ratio_pdf_function(q), q_min, 1)[0] + return local_f_bin * f_q * (1 + expected_q) * IMF_mass(mass) + + # split integral at binary fraction steps if f_bin is None (i.e. variable and like a step function) + def split_integral(func, a, b, f_bin): + total = 0 + for edge_start, edge_end in zip(binary_bin_edges[:-1], binary_bin_edges[1:]): + left = max(a, edge_start) + right = min(b, edge_end) + if left < right: + result, _ = quad(func, left, right, args=(f_bin,)) + total += result + return total + + if f_bin is None: + full_mass = split_integral(integrand_full, m1, m4, f_bin) + compas_mass = split_integral(integrand_compas, m1_low, m1_upp, f_bin) + else: + full_mass = quad(integrand_full, m1, m4, args=(f_bin,))[0] + compas_mass = quad(integrand_compas, m1_low, m1_upp, args=(f_bin,))[0] return compas_mass / full_mass From 51fbde5cdfb6d0b9d7a7d577d9b8222d43e9ef79 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 29 Dec 2025 18:52:57 +0100 Subject: [PATCH 39/47] debugging MC sampler and fbin variable analytic --- .../totalMassEvolvedPerZ.py | 32 - py_tests/test_fbinary_perM_inclMCMC.ipynb | 1308 +++++++++++++++++ 2 files changed, 1308 insertions(+), 32 deletions(-) create mode 100644 py_tests/test_fbinary_perM_inclMCMC.ipynb diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 47e1a59b9..235c379af 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -209,38 +209,6 @@ def draw_samples_from_kroupa_imf( - - -################################################## -# Old version of analytical calculation -################################################## -def old_analytical_star_forming_mass_per_binary_using_kroupa_imf( - m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] -): - """ - Analytical computation of the mass of stars formed per binary star formed within the - [m1 min, m1 max] and [m2 min, ..] rage, - using the Kroupa IMF: - - p(M) \propto M^-0.3 for M between m1 and m2 - p(M) \propto M^-1.3 for M between m2 and m3; - p(M) = alpha * M^-2.3 for M between m3 and m4; - - @Ilya Mandel's derivation - """ - m1, m2, m3, m4 = imf_mass_bounds - if m1_min < m3: - raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") - alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) - # average mass of stars (average mass of all binaries is a factor of 1.5 larger) - m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7)) - # fraction of binaries that COMPAS simulates - fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) - # mass represented by each binary simulated by COMPAS - m_rep = (1/fint) * m_avg * (1.5 + (1-fbin)/fbin) - return m_rep - - ################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] diff --git a/py_tests/test_fbinary_perM_inclMCMC.ipynb b/py_tests/test_fbinary_perM_inclMCMC.ipynb new file mode 100644 index 000000000..a26176f28 --- /dev/null +++ b/py_tests/test_fbinary_perM_inclMCMC.ipynb @@ -0,0 +1,1308 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Notebook to test implementation of mass-dependent binary fraction\n", + "\n", + "in the [compas_python_utils/cosmic_integration](https://github.com/TeamCOMPAS/COMPAS/tree/dev/compas_python_utils/cosmic_integration) folder of `COMPAS`, there are a couple of functions that take the binary fraction `fbin`. \n", + "\n", + "We keep on assuming that fbin is a constant, while really it depends on the primary mass. In this notebook I want to describe the analytical derivation of using $f_{bin}(M_1)$, and test that my derivation gets you the same answer as an MC sampler. \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Where is fbinary used? \n", + "\n", + "It is passed to `FastCosmicIntegration.py`, \n", + "\n", + "``` python\n", + "COMPAS = ClassCOMPAS.COMPASData(path, Mlower=m1_min, Mupper=m1_max, m2_min=m2_min, binaryFraction=fbin, suppress_reminder=True)\n", + "```\n", + "\n", + "which passes it to `ClassCOMPAS.py`, where it is part of the `class COMPASData(object):`\n", + "It is specifically used in `def setGridAndMassEvolved(self)`, `recalculateTrueSolarMassEvolved(self, Mlower, Mupper, binaryFraction):` and lastly `find_star_forming_mass_per_binary_sampling(self)`\n", + "\n", + "The functions ClassCOMPAS are again just referring to `totalMassEvolvedPerZ.py` which is where the real magic happens \n", + "\n", + "This is again adding another layer and just pointing to `get_COMPAS_fraction` and `analytical_star_forming_mass_per_binary_using_kroupa_imf`. We really only need/use this last function, so I am going to address that:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "###################################################\n", + "# Old version of analytical calculation\n", + "###################################################\n", + "def OLD_analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200]\n", + "):\n", + " \"\"\"\n", + " Analytical computation of the mass of stars formed per binary star formed within the\n", + " [m1 min, m1 max] and [m2 min, ..] rage,\n", + " using the Kroupa IMF:\n", + "\n", + " p(M) \\propto M^-0.3 for M between m1 and m2\n", + " p(M) \\propto M^-1.3 for M between m2 and m3;\n", + " p(M) = alpha * M^-2.3 for M between m3 and m4;\n", + "\n", + " m1_min, m1_max are the min and max sampled primary masses\n", + " m2_min is the min sampled secondary mass\n", + "\n", + " @Ilya Mandel's derivation\n", + " \"\"\"\n", + " m1, m2, m3, m4 = imf_mass_bounds\n", + " if m1_min < m3:\n", + " raise ValueError(f\"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})\")\n", + " if m1_min > m1_max:\n", + " raise ValueError(f\"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})\")\n", + " if m1_max > m4:\n", + " raise ValueError(f\"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})\")\n", + " \n", + " # normalize IMF over the complete mass range:\n", + " alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1)\n", + " # print('alpha', alpha)\n", + "\n", + " # average mass of stars (average mass of all binaries is a factor of 1.5 larger)\n", + " m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7))\n", + "\n", + " # fraction of binaries that COMPAS simulates (i.e., N_binaries_in_COMPAS/N_binaries_in_universe) \n", + " # second term assumes a flat mass ratio distribution with m2_max = m1_max\n", + " fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3))\n", + "\n", + " # Average mass of systems (M_rep_by_all_binary_systems/N_binaries_in_universe)\n", + " # 1.5 = Average number of stars in single and binary systems, (1-fbin)/fbin) = ratio of single/binary systems\n", + " average_mass_per_binary = m_avg * (1.5 + (1-fbin)/fbin)\n", + "\n", + " # mass represented by each binary simulated by COMPAS\n", + " # N_binaries_in_universe/N_binaries_in_COMPAS * M_rep_by_all_binary_systems/N_binaries_in_universe\n", + " m_rep = (1/fint) * average_mass_per_binary \n", + " \n", + " analytical_results = {\n", + " 'alpha': alpha, \n", + " 'm_avg': m_avg,\n", + " 'fint': fint,\n", + " 'average_mass_per_binary': average_mass_per_binary,\n", + " 'm_rep': m_rep\n", + " }\n", + " return analytical_results\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Breaking down the analytical calculation of $\\tt{star\\_forming\\_mass\\_per\\_binary\\_using\\_kroupa\\_imf}$\n", + "\n", + "We are calculating the average stellar mass formed in the universe (both singles and binaries) per a binary system formed in COMPAS, in other words:\n", + "\n", + "$$\n", + "\\begin{align}\n", + "\\frac{M_{\\rm stellar\\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}} & =\\\\\n", + " & \\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} \n", + " \\textcolor{orange}{\\times \\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}}\n", + " \\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} }\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "The first term is just the fraction of the integral over $m1$ and $q$ that is spanned by the COMPAS simulation. \n", + "Importantly, we cannot neglect the fbin(m1) function in this integral! This is the difference between calculating the “fraction of IMF-weighted primaries that land in COMPAS” versus the “fraction of binary-weighted primaries that land in COMPAS” (the latter is what we want). \n", + "\n", + "$$\n", + "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} = \\frac{\\int\\int_{min(UNIV)}^{max(UNIV)} P(m1) f_{bin}(m1) P(q) dm1 dq}{\\int\\int_{min(COMPAS)}^{max(COMPAS)} P(m1) f_{bin}(m1) P(q) dm1 dq}\n", + "$$\n", + "\n", + "\n", + "Now we assume that $p(m1) = \\rm Kroupa$. \n", + "Furthermore, we adopt $p(q) = U(0,1)$ (so flat in q).\n", + "\n", + "With this, the COMPAS integral part:\n", + "\n", + "$$\n", + "\\int_{m1,min}^{m1,max} \\int_{m2,min/m1}^{1} P(m1) f_{bin}(m1) P(q) dm1 dq \n", + "= \n", + "\\int_{m1,min}^{m1,max} P(m1)f_{bin}(m1) \\left( 1 - \\frac{m_{2,min}}{m_1} \\right) dm_1 dq \n", + "$$\n", + "\n", + "So we can re-write the whole thing as\n", + "\n", + "$$\n", + "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}}\n", + "=\n", + "\\frac{ \\int_{UNIV} P(m_1) f_{bin}(m_1) dm_1\n", + "}{\n", + "\\int_{COMPAS} P(m_1) f_{bin}(m_1) \\left( 1 - \\frac{m_{2,min}}{m_1} \\right) dm_1 }.\n", + "$$\n", + "\n", + "\n", + "We assume that the **primary-mass range of interest satisfies $a > 0.5\\,M_\\odot$**, such that only the high-mass Kroupa slope applies.\n", + "\n", + "Hence in this regime the IMF is\n", + "$$\n", + "p(m_1) = \\alpha\\, m_1^{-2.3},\n", + "$$\n", + "with $\\alpha$ the (global) IMF normalization constant.\n", + "\n", + "Let's evaluate the top and bottom separately \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\\begin{aligned}\n", + "\\textcolor{green}{N_{\\rm binaries \\ in \\ COMPAS}} & = \n", + "\\int_{m_{1,\\min}}^{m_{1,\\max}} p(m_1)\\, f_{\\mathrm{bin}}(m_1)\\, \\left(1 - \\frac{m_{2,\\min}}{m_1}\\right)\\, \\mathrm{d}m_1. \\\\\n", + " & = \\alpha \\sum_i f_i \\int_{A_i}^{B_i} \\left( m_1^{-2.3} - m_{2,\\min}\\, m_1^{-3.3} \\right) \\mathrm{d}m_1,\n", + "\\end{aligned}\n", + "\n", + "Where the second line has written this out piecewise over the binary-fraction bins, and\n", + "\n", + "$[A_i,B_i]$ is the overlap of the $i$-th binary-fraction bin with\n", + "$[m_{1,\\min}, m_{1,\\max}]$.\n", + "\n", + "Evaluating the integrals:\n", + "\n", + "\n", + "$$\n", + "\\textcolor{green}{N_{\\rm binaries \\ in \\ COMPAS}} = \n", + "\\alpha \\sum_i f_i\n", + "\\left( \n", + " \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{A_i}^{B_i} - m_{2,\\min}\n", + " \\left[ \\frac{m_1^{-2.3}}{-2.3} \\right]_{A_i}^{B_i}\n", + "\\right).\n", + "$$\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, \n", + "\n", + "$$\n", + "\\textcolor{green}{N_{\\rm binaries\\ in \\ Univ}} =\n", + "\\int_{\\mathrm{Univ}} p(m_1)\\, f_{\\mathrm{bin}}(m_1)\\, \\mathrm{d}m_1\n", + "=\n", + "\\alpha \\sum_i f_i \\int_{a_i}^{b_i} m_1^{-2.3}\\, \\mathrm{d}m_1,\n", + "$$\n", + "\n", + "(this is the IMF– and binary-fraction–weighted number of binaries)\n", + "where: $f_i$ is the binary fraction in mass bin $i$, $[a_i,b_i]$ is the overlap of the $i$-th binary-fraction bin with the universal mass range.\n", + "\n", + "Evaluating the integral:\n", + "\n", + "$$\n", + "\\textcolor{green}{N_{\\rm binaries\\ in \\ Univ}} = \n", + "\\alpha \\sum_i f_i \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{a_i}^{b_i}.\n", + "$$\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally \n", + "\n", + "$$\n", + "f_{\\mathrm{int}} =\n", + "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} = \n", + "\\frac{\\alpha \\sum_i f_i \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{a_i}^{b_i}}{\n", + "\\alpha \\sum_i f_i\n", + "\\left( \n", + " \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{A_i}^{B_i} - m_{2,\\min}\n", + " \\left[ \\frac{m_1^{-2.3}}{-2.3} \\right]_{A_i}^{B_i}\n", + "\\right). \n", + "}\n", + "\n", + "$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*** \n", + "^^ !! I AM HERE !! ^^\n", + "\n", + "which, if we normalized the functions, is just 1 over the integral in the COMPAS range:\n", + "\n", + "$$\n", + " p(m2|m1) = \\int_{m2, min COMPAS}^{min(1, m1 max COMPAS)} \\int_{m1, min COMAPS}^{m1, max COMPAS} P(m1) P(q) dm1 dq\n", + "$$\n", + "\n", + "where we have used that $q \\equiv m2/m1$.\n", + "\n", + "If we also enforce/assume that $a > 0.5M_{\\odot}$ (i.e. $a > \\rm \\tt{imf\\_mass\\_bounds[2]}$),\n", + "we get:\n", + "$$\n", + "\\begin{align}\n", + "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} & = \\alpha \\int_{a}^{b} m1^{-2.3} (1- \\frac{c}{m1}) dm1 = \\alpha \\int_{a}^{b} m_1^{-2.3} - c m_1^{-3.3} dm_1 \\\\\n", + "& \\alpha \\Bigg[ \\frac{-1}{1.3} m_1^{-1.3} \\Bigg]_{a}^{b} + \\alpha \\Bigg[ \\frac{c}{2.3} m_1^{-2.3} \\Bigg]_{a}^{b}\n", + "\\end{align}\n", + "$$\n", + "\n", + "with $\\alpha$ the normalizing constant for the Kroupa IMF, $a = \\rm min(m1_{COMPAS})$, $b = \\rm max(m1_{COMPAS})$, and $c = \\rm min(m2_{COMPAS})$. \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**This gets us the following line of code:**\n", + "\n", + "\n", + "``` python\n", + "# fraction of binaries that COMPAS simulates (i.e., N_binaries_in_COMPAS/N_binaries_in_universe) \n", + "# second term assumes a flat mass ratio distribution with m2_max = m1_max\n", + "fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the second term is just 1 over the binary fraction, which is now a function of $m_1$\n", + "\n", + "$$\n", + " \\textcolor{orange}{\\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}} = \\frac{1}{f_b(m_1)}\n", + "$$\n", + "\n", + "well get back to this (it will be incorporated in the sum of the third term)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "Lastly the third term represents the average (or expected) mass of a stellar system (binaries and singles) in the Universe. \n", + "i.e., the average mass of single stars (weighted by the single fraction) plus the average mass of binary systems (weighted by binary fraction fb)\n", + "\n", + "$$\n", + "\\begin{align}\n", + "\\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } & = \\left< (1 - f_b(m_1)) m_s \\right> + \\left< f_(b) m_{binary} \\right> \\\\\n", + "& = \\left< (1 - f_b(m_1)) \\cdot m_1 \\right> + \\left< f_b(m_1) \\cdot m_{1}(1 + q) \\right> \\\\\n", + "& = \\left< (1 - f_b(m_1)) \\cdot m_1 \\right> + \\left< f_b(m_1) m_{1} \\right> \\left< (1 + q) \\right>\n", + "\\end{align}\n", + "$$\n", + "(we can split this up because $m1$ and $q$ are independent)\n", + "\n", + "$$\n", + "\\left< (1 + q) \\right> = 1 + \\int_0^1 q P(q)dq = 1 + \\frac{1}{2} q^2 \\big]_0^1 = 1.5\n", + "$$\n", + "\n", + "and since averages are linear, we can combine the above to\n", + "\n", + "$$\n", + "\\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } \n", + "= \\left< (1 + 0.5 f_b(m_1)) m_1 \\right>\n", + "$$\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "BUT, we shouldn't forget the $1/f_b$ term from above! Bringing that back in we have: \n", + "\n", + "$$\n", + "\\begin{align}\n", + " \\textcolor{orange}{\\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}} \\times \\textcolor{blue}{ \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } & = \\left< \\frac{1 + 0.5 f_b(m_1)}{f_b(m_1)} m_1 \\right>\n", + " = \\left< \\left(\\frac{1}{f_b(m_1)} + 0.5 \\right) m_1 \\right> \\\\\n", + "& \\int_{A}^{B} \\left(\\frac{1}{f_b(m_1)} + 0.5 \\right) m_1 P(m1) dm_1 \\\\\n", + "\\end{align}\n", + "$$\n", + "\n", + "Where $A$ and $B$ are now the minimum and maximum values of $m_1$ in our Universe, \n", + "(that is $\\tt{imf\\_mass\\_bounds[0]}$, and $\\tt{imf\\_mass\\_bounds[3]}$), \n", + "\n", + "Now, we adopt\n", + "$$\n", + "f_b(m_1) =\n", + "\\begin{cases}\n", + "0.1 & \\text{for } 0.01 < m_1 \\leq 0.08, \\\\\n", + "0.225 & \\text{for } 0.08 < m_1 < 0.5, \\\\\n", + "0.5 & \\text{for } 0.5 < m_1 \\leq 1, \\\\\n", + "0.80 & \\text{for } 1 < m_1 \\leq 10, \\\\\n", + "1 & \\text{for } 10 < m_1 \\leq 200.\n", + "\\end{cases}\n", + "$$\n", + "chosen to approximately follow Figure 1 from Offner et al. (2023).\n", + "This is piecewise constant and discontinuous, so we can break the integral into parts over the intervals where f(x) is constant.\n", + "This means we have some bookkeeping to do, because the $P(m_1)$ is also a piecewise function in $m_1$, so we have to evaluate each piece for both bin ranges:\n", + "\n", + "$$\n", + "\\int_{A}^{B} \\left( \\frac{1}{f(m_1)} + 0.5 \\right) m_1 P(m_1) dm_1\n", + "= \\sum_{i} \\left( \\frac{1}{f_{b,i}(m_1)} + 0.5 \\right) \\int_{A}^{B} m_1 P(m_1) \\, dm_1\n", + "$$\n", + "\n", + "\n", + "$$\n", + "\\sum_{i} \\left( \\frac{1}{f_{b,i}} + 0.5 \\right) \n", + "\\sum_{j} \\int_{m_j}^{m_{j+1}} m_1 P(m_1) \\, dm_1\n", + "$$\n", + "\n", + "Also, keep in mind that the Kroupa IMF needs some normalizing constants to ensure continuity\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**This leads to the below bulk of code**\n", + "\n", + "``` python\n", + "# Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", + "# N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction \n", + "# fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023)\n", + "binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] \n", + "if fbin == None:\n", + " # use a binary fraction that varies with mass\n", + " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] \n", + "else:\n", + " # otherwise use a constant binary fraction\n", + " binaryFractions = [fbin] * 5\n", + "\n", + "# M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe,\n", + "# we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to:\n", + "# int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. \n", + "# This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins.\n", + "piece_wise_integral = 0\n", + "\n", + "# For every binary fraction bin\n", + "for i in range(len(binary_bin_edges) - 1):\n", + " fbin = binaryFractions[i] # Binary fraction for this range\n", + "\n", + " # And every piece of the Kroupa IMF\n", + " for j in range(len(imf_mass_bounds) - 1):\n", + " exponent = IMF_powers[j] # IMF exponent for these masses\n", + "\n", + " # Check if the binary fraction bin overlaps with the IMF mass bin\n", + " if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]:\n", + " continue # No overlap\n", + "\n", + " # Integrate from the most narrow range\n", + " m_start = max(binary_bin_edges[i], imf_mass_bounds[j])\n", + " m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1])\n", + "\n", + " # Compute the definite integral:\n", + " integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j]\n", + "\n", + " # Compute the sum term\n", + " sum_term = (1 /fbin + 0.5) * integral\n", + " piece_wise_integral += sum_term\n", + "\n", + "# combining them:\n", + "Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The final analytical function becomes: \n", + "\n", + "$$\n", + "\\begin{align}\n", + "\\frac{M_{\\rm stellar\\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}} & =\\\\\n", + " & \\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} \n", + " \\textcolor{orange}{\\times \\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}}\n", + " \\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } \\\\\n", + "& = \\left( \\alpha \\Bigg[ \\frac{-1}{1.3} m_1^{-1.3} \\Bigg]_{a}^{b} + \\alpha \\Bigg[ \\frac{c}{2.3} m_1^{-2.3} \\Bigg]_{a}^{b} \\right)\n", + "\\times \n", + "\\alpha \\left( \\sum_{i} \\left( \\frac{1}{f_{b,i}} + 0.5 \\right) \n", + "\\sum_{j} \\int_{m_j}^{m_{j+1}} m_1 P(m_1) \\, dm_1 \\right)\n", + "\\end{align} \n", + "$$\n", + "\n", + "with $\\alpha$ the normalizing constant for the Kroupa IMF, $a = \\rm min(m1_{COMPAS})$, $b = \\rm max(m1_{COMPAS})$, and $c = \\rm min(m2_{COMPAS})$. \n", + "Note that the second term is integrated over the full range of $m_1$ that span the Universe " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "###################################################\n", + "# New version of analytical calculation\n", + "###################################################\n", + "def analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200]\n", + "):\n", + " \"\"\"\n", + " Analytical computation of the mass of stars formed per binary star formed within the\n", + " [m1 min, m1 max] and [m2 min, ..] rage,\n", + " using the Kroupa IMF:\n", + "\n", + " p(M) \\propto M^-0.3 for M between m1 and m2\n", + " p(M) \\propto M^-1.3 for M between m2 and m3;\n", + " p(M) = alpha * M^-2.3 for M between m3 and m4;\n", + "\n", + " m1_min, m1_max are the min and max sampled primary masses\n", + " m2_min is the min sampled secondary mass\n", + "\n", + " This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max\n", + " Lieke base on Ilya Mandel's derivation\n", + " \"\"\"\n", + " #########\n", + " # Kroupa IMF \n", + " m1, m2, m3, m4 = imf_mass_bounds\n", + " continuity_constants = [1./(m2*m3), 1./(m3), 1.0] \n", + " IMF_powers = [-0.3, -1.3, -2.3] \n", + "\n", + " if m1_min < m3:\n", + " raise ValueError(f\"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})\")\n", + " if m1_min > m1_max:\n", + " raise ValueError(f\"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})\")\n", + " if m1_max > m4:\n", + " raise ValueError(f\"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})\")\n", + " \n", + " # normalize IMF over the complete mass range:\n", + " alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1)\n", + " # print('alpha', alpha)\n", + "\n", + " #########\n", + " # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023)\n", + " binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] \n", + " if fbin == None:\n", + " # use a binary fraction that varies with mass\n", + " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] \n", + " else:\n", + " # otherwise use a constant binary fraction\n", + " binaryFractions = [fbin] * 5\n", + "\n", + " ##################\n", + " # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", + " # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", + "\n", + " ### !! PRESUMABLY ERROR IS IN HERE !! ###\n", + " # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates\n", + " # fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3))\n", + "\n", + " def N_binaries_kroupa_fbin(m1_low, m1_high, m2_min):\n", + " \"\"\"\n", + " Computes:\n", + " N = alpha * Σ_i Σ_j binaryFractions[i] *\n", + " ∫ ( m^{IMF_powers[j]} - m2_min * m^{IMF_powers[j]-1} )\n", + " * continuity_constants[j] dm\n", + "\n", + " The integral is taken over the overlap of:\n", + " - binary-fraction bin i,\n", + " - IMF segment j,\n", + " - [m1_low, m1_high].\n", + " \"\"\"\n", + " if m1_high <= m1_low:\n", + " raise ValueError(\"Require m1_high > m1_low\")\n", + "\n", + " total = 0.0\n", + " #Compute double piecewise integral\n", + " for i in range(len(binaryFractions)):\n", + " # overlap of binary-fraction bin with [m1_low, m1_high]\n", + " bin_lo = max(binary_bin_edges[i], m1_low)\n", + " bin_hi = min(binary_bin_edges[i + 1], m1_high)\n", + "\n", + " if bin_hi <= bin_lo:\n", + " continue\n", + " # split across IMF segments\n", + " for j in range(len(IMF_powers)):\n", + " m_start = max(bin_lo, imf_mass_bounds[j])\n", + " m_end = min(bin_hi, imf_mass_bounds[j + 1])\n", + "\n", + " if m_end <= m_start:\n", + " continue\n", + "\n", + " # ∫ m^{IMF_powers[j]} dm\n", + " integral_main = (\n", + " m_end**(IMF_powers[j] + 1) - m_start**(IMF_powers[j] + 1)\n", + " ) / (IMF_powers[j] + 1)\n", + "\n", + " # ∫ m^{IMF_powers[j]-1} dm (only if m2_min > 0)\n", + " if m2_min > 0.0:\n", + " integral_m2 = (\n", + " m_end**(IMF_powers[j]) - m_start**(IMF_powers[j])\n", + " ) / (IMF_powers[j])\n", + " else:\n", + " integral_m2 = 0.0\n", + "\n", + " total += binaryFractions[i] * continuity_constants[j] * (integral_main - m2_min * integral_m2)\n", + "\n", + " return alpha * total\n", + "\n", + " # Integral for the full universe, has IMF bound limits, and m2_min = 0\n", + " N_univ = N_binaries_kroupa_fbin(m1_low=m1, m1_high=m4, m2_min=0.0)\n", + " # Integral for COMPAS sampled binaries\n", + " N_compas = N_binaries_kroupa_fbin(m1_low=m1_min, m1_high=m1_max, m2_min=m2_min)\n", + "\n", + " fint = N_compas / N_univ\n", + "\n", + "\n", + " ##################\n", + " # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", + " # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction \n", + "\n", + " # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe,\n", + " # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to:\n", + " # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. \n", + " # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins.\n", + " piece_wise_integral = 0\n", + "\n", + " # For every binary fraction bin\n", + " for i in range(len(binary_bin_edges) - 1):\n", + " fbin = binaryFractions[i] # Binary fraction for this range\n", + "\n", + " # And every piece of the Kroupa IMF\n", + " for j in range(len(imf_mass_bounds) - 1):\n", + " exponent = IMF_powers[j] # IMF exponent for these masses\n", + "\n", + " # Check if the binary fraction bin overlaps with the IMF mass bin\n", + " if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]:\n", + " continue # No overlap\n", + "\n", + " # Integrate from the most narrow range\n", + " m_start = max(binary_bin_edges[i], imf_mass_bounds[j])\n", + " m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1])\n", + "\n", + " # Compute the definite integral:\n", + " integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j]\n", + "\n", + " # Compute the sum term\n", + " sum_term = (1 /fbin + 0.5) * integral\n", + " piece_wise_integral += sum_term\n", + "\n", + " # combining them:\n", + " Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral\n", + "\n", + " # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", + " M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin\n", + "\n", + "\n", + " analytical_results = {\n", + " 'M_sf_Univ_per_N_binary_COMPAS': M_sf_Univ_per_N_binary_COMPAS, \n", + " 'fint': fint,\n", + " 'Average_mass_stellar_sys_per_fbin': Average_mass_stellar_sys_per_fbin,\n", + " 'piece_wise_integral': piece_wise_integral,\n", + " 'alpha': alpha,\n", + " 'binaryFractions': binaryFractions,\n", + " 'binary_bin_edges': binary_bin_edges,\n", + " 'imf_mass_bounds': imf_mass_bounds,\n", + " }\n", + " return analytical_results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Let's test that they lead to the same answer:" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " old analytical m_rep =253.53899506390485, new analytical m_rep = 253.53899506390468, so well get an change of', 1.0000000000000007\n" + ] + } + ], + "source": [ + "m1_min = 10\n", + "m1_max = 150\n", + "m2_min = 0.1 \n", + "fbin= 0.7\n", + "\n", + "old_analytical = OLD_analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", + "\n", + "new_analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", + "\n", + "\n", + "# print(old_analytical.keys())\n", + "# print(new_analytical.keys())\n", + "print(f\" old analytical m_rep ={old_analytical['m_rep']}, \\\n", + " new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']},\\\n", + " so well get an change of', {old_analytical['m_rep']/new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\")\n", + "\n", + "# Pretty much the same! " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Now lastly we want to test if a Numerical MC sampler also agreed with this\n", + "\n", + "\n", + "### CREATE A MOCK UNIVERSE\n", + "\n", + "- Step 1: Sample N primary masses from the Kroupa IMF\n", + "\n", + "- Step 2: Sample binaries based on a variable binary fraction\n", + "\n", + " > Sum the total mass of stellar systems in the mock universe (= all m1 + m2)\n", + "\n", + "\n", + "- Step 3: figure out which of your systems would be part of your COMPAS simulation\n", + " (I.e. apply m1_min - m1_max, m2_min, and only keep binary systems )\n", + "\n", + "> Count the number of binaries that would be in the COMPAS simulation\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjoAAAGwCAYAAACgi8/jAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAIEtJREFUeJzt3Q+QVdV9B/DfAgIS2TUEBREQjcZmjYEJLJSmarA0iIZp7dQ4qW2ROExrtiaWxAQ6FZpWg2kMUhMSjCkhbU0kTSrG0DJJiYpRKAhFGzcoREhoLf8SYQEji+x2zp3ujit/VNjd9/a8z2fmzr77h/vOew/2fTnnd+6tamlpaQkAgAz1KHUDAAA6i6ADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbvaLCNTc3xwsvvBD9+/ePqqqqUjcHAHgD0mUA9+3bF0OGDIkePY7db1PxQSeFnGHDhr2R9xQAKDPbtm2LoUOHHnN/xQed1JPT+kZVV1d34UcDAJyoxsbGoqOi9Xv8WCo+6LQOV6WQI+gAQPfyemUnipEBgGxVbNBZsGBB1NbWRl1dXambAgB0kqpKv3t5GuOrqamJvXv3GroCgMy+vyu2RwcAyJ+gAwBkS9ABALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAyJagAwBkS9ABALLVq9QNgJM1Yuaybvcmbr3jqlI3AaAi6NEBALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAyJagAwBkS9ABALIl6AAA2RJ0AIBsZXGvqy1btsSHP/zh2LFjR/Ts2TNWr14db3nLW0rdrG6pO943CgCyDjrXX3993HbbbXHJJZfEL3/5y+jTp0+pmwRZBko3IwW6m24fdJ555pk45ZRTipCTDBgwoNRNAgDKRMlrdFauXBlTpkyJIUOGRFVVVSxduvSIYxYsWBAjRoyIvn37xrhx42LNmjVt+zZt2hSnnXZacY73vOc98ZnPfKaLXwEAUK5KHnQOHDgQI0eOLMLM0SxZsiRmzJgRc+bMifXr1xfHTpo0KXbu3Fnsf+WVV+Kxxx6LL33pS7Fq1ar4wQ9+UCwAACUPOpMnTy7qa66++uqj7p83b15Mnz49pk2bFrW1tbFw4cLo169fLFq0qNh/9tlnx5gxY2LYsGFFbc6VV14ZGzZsOObzHTx4MBobG9stAECeSh50jqepqSnWrVsXEydObNvWo0ePYj313iR1dXVF786LL74Yzc3NxVDYO9/5zmOec+7cuVFTU9O2pIAEAOSprIPO7t274/DhwzFo0KB229P69u3bi8e9evUq6nIuvfTSePe73x0XXHBBfOADHzjmOWfNmhV79+5tW7Zt29bprwMAKI1uP+uqdfgrLW9EGt4y/RwAKkNZ9+gMHDiwuABguhDgq6X1wYMHl6xdAED3UNZBp3fv3jF69OhYsWJF27ZUh5PWx48fX9K2AQDlr+RDV/v374/Nmze3u51DmjWVLvw3fPjwYmr51KlTi5lVY8eOjfnz5xdT0tMsrJORprOnJdUAAQB5qmppaWkpZQMeeeSRmDBhwhHbU7hZvHhx8fiLX/xifO5znysKkEeNGhV33313ceHAjpCml6fZV6kwubq6Oipdd701AV3DLSCAcvFGv79LHnRKTdBpT9DheAQdoLt9f5d1jQ4AwMkQdACAbFVs0EmFyOmWEunKygBAnio26NTX10dDQ0OsXbu21E0BADpJxQYdACB/gg4AkC1BBwDIlqADAGSrYoOOWVcAkL+KDTpmXQFA/io26AAA+RN0AIBsCToAQLYEHQAgW4IOAJCtig06ppcDQP4qNuiYXg4A+avYoAMA5K9XqRuQsxEzl5W6CQBQ0fToAADZEnQAgGwJOgBAttToAFnXnW2946pSNwEooYrt0XEdHQDIX8UGHdfRAYD8VWzQAQDyJ+gAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZKtig44rIwNA/io26LgyMgDkr2KDDgCQP0EHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZKtig46begJA/io26LipJwDkr2KDDgCQP0EHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFsVG3QWLFgQtbW1UVdXV+qmAACdpGKDTn19fTQ0NMTatWtL3RQAoJNUbNABAPIn6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAyJagAwBkS9ABALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJCtXqVuAEBnGjFzWbd7g7fecVWpmwDZ0KMDAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGQriysjjxgxIqqrq6NHjx7x1re+NR5++OFSNwkAKANZBJ3kiSeeiNNOO63UzQAAyoihKwAgWyUPOitXrowpU6bEkCFDoqqqKpYuXXrEMQsWLCiGp/r27Rvjxo2LNWvWtNuf/txll10WdXV1cd9993Vh6wGAclbyoHPgwIEYOXJkEWaOZsmSJTFjxoyYM2dOrF+/vjh20qRJsXPnzrZjfvSjH8W6deviu9/9bnzmM5+Jp59++pjPd/DgwWhsbGy3AAB5KnnQmTx5ctx2221x9dVXH3X/vHnzYvr06TFt2rSora2NhQsXRr9+/WLRokVtx5x99tnFz7POOiuuvPLKIhAdy9y5c6OmpqZtGTZsWCe8KgCgHJQ86BxPU1NT0VMzceLEtm1pZlVaX7VqVVuP0L59+4rH+/fvjx/+8Idx0UUXHfOcs2bNir1797Yt27Zt64JXAgCUQlnPutq9e3ccPnw4Bg0a1G57Wt+4cWPxeMeOHW29QenY1PuTanWOpU+fPsUCAOSvrIPOG3HeeefFU089VepmAABlqKyHrgYOHBg9e/Ysem1eLa0PHjy4ZO0CALqHsg46vXv3jtGjR8eKFSvatjU3Nxfr48ePP6lzp1leqbj5eMNcAED3VvKhq1RAvHnz5rb1LVu2xIYNG2LAgAExfPjwYmr51KlTY8yYMTF27NiYP39+UYCcZmGdjPr6+mJJ08vT7CsAID8lDzpPPvlkTJgwoW09BZskhZvFixfHtddeG7t27YrZs2fH9u3bY9SoUbF8+fIjCpQBAF6rqqWlpSUqWGuPTppqnm4M2pFGzFzWoecDKsPWO64qdRMgm+/vsq7RAQA4GRUbdBQjA0D+KjbopELkhoaGWLt2bambAgB0kooNOgBA/gQdACBbgg4AkC1BBwDIVsUGHbOuACB/FRt0zLoCgPxVbNABAPIn6AAA2RJ0AIBsCToAQLYEHQAgWxUbdEwvB4D8VWzQMb0cAPJXsUEHAMifoAMAZEvQAQCyJegAANkSdACAbJ1Q0Ln88stjz549R2xvbGws9gEAdNug88gjj0RTU9MR219++eV47LHHOqJdAAAnrdebOfjpp59ue9zQ0BDbt29vWz98+HAsX748zj777OguFwxMS2o3AJCnNxV0Ro0aFVVVVcVytCGqU089Nb7whS9Ed7lgYFrScFtNTU2pmwMAlDrobNmyJVpaWuK8886LNWvWxBlnnNG2r3fv3nHmmWdGz549O6OdAACdG3TOOeec4mdzc/ObfyYAgHIOOq+2adOmePjhh2Pnzp1HBJ/Zs2d3RNsAALo+6Nx7771x4403xsCBA2Pw4MFFzU6r9FjQAQC6bdC57bbb4vbbb49PfepTHd8iAIBSXkfnxRdfjGuuuaaj2gAAUD5BJ4Wc73//+x3fGgCAUg9dnX/++XHrrbfG6tWr4+KLL45TTjml3f6PfvSjHdU+AIATVtWSLozzJp177rnHPmFVVTz//PPRna6M/Nxzz8XevXujurq6Q59jxMxlHXo+oDJsveOqUjcByl7rBX9f7/v7hIJOJb5RJ0LQASqFcEa5fn+fUI0OAEC2NTof/vCHj7t/0aJFJ9oeAIDSBp00vfzVDh06FD/+8Y9jz549R73ZJwBAtwk6DzzwwBHb0m0g0tWS3/72t3dEuwAATlqH1ej06NEjZsyYEXfddVdHnRIA4KR0aDHyT3/603jllVc68pQAAF07dJV6bl4tzVD/3//931i2bFlMnTr1xFsDAFDqoPOf//mfRwxbnXHGGfH5z3/+dWdkAQCUddB5+OGHO74lAADlEHRa7dq1K5599tni8YUXXlj06gAAdOti5AMHDhRDVGeddVZceumlxTJkyJC44YYb4qWXXur4VgIAdFXQScXIjz76aDz00EPFRQLT8uCDDxbbPv7xj0d3kG7oWVtbG3V1daVuCgDQSU7opp4DBw6Mb3/72/G+973viNqdD37wg8WQVnfhpp4AJ89NPcnqpp5peGrQoEFHbD/zzDMNXQEAZeOEgs748eNjzpw58fLLL7dt+9WvfhWf/vSni30AAN121tX8+fPjiiuuiKFDh8bIkSOLbU899VT06dMnvv/973d0GwEAui7oXHzxxbFp06a47777YuPGjcW2D33oQ3HdddfFqaeeemItAQAoh6Azd+7cokZn+vTp7bYvWrSoKET+1Kc+1VHtAwDo2hqde+65J37t137tiO0XXXRRLFy48MRbAwBQ6qCzffv24mKBr5WujJxu7gkA0G2DzrBhw+Lxxx8/Ynvalq6QDADQbWt0Um3OzTffHIcOHYrLL7+82LZixYr45Cc/2W2ujAwA5O+Egs4tt9wSv/jFL+IjH/lINDU1Fdv69u1bFCHPmjWro9sIANB1Qaeqqio++9nPxq233ho/+clPiinlF1xwQXEdHQCAbh10Wp122mluigkA5FWMDADQHQg6AEC2BB0AIFuCDgCQrYoNOgsWLIja2lrF1ACQsYoNOvX19dHQ0BBr164tdVMAgE5SsUEHAMifoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQrWyCzksvvRTnnHNOfOITnyh1UwCAMtErMnH77bfHr//6r5e6GQAVacTMZdHdbL3jqlI3gS6QRY/Opk2bYuPGjTF58uRSNwUAKCMlDzorV66MKVOmxJAhQ6KqqiqWLl16xDELFiyIESNGRN++fWPcuHGxZs2advvTcNXcuXO7sNUAQHdQ8qBz4MCBGDlyZBFmjmbJkiUxY8aMmDNnTqxfv744dtKkSbFz585i/4MPPhjveMc7iuWNOHjwYDQ2NrZbAIA8lbxGJw03HW/Iad68eTF9+vSYNm1asb5w4cJYtmxZLFq0KGbOnBmrV6+O+++/P/75n/859u/fH4cOHYrq6uqYPXv2Uc+Xen4+/elPd9rrAQDKR8l7dI6nqakp1q1bFxMnTmzb1qNHj2J91apVbcFl27ZtsXXr1rjzzjuLUHSskJPMmjUr9u7d27akPwsA5KnkPTrHs3v37jh8+HAMGjSo3fa0noqPT0SfPn2KBQDIX1kHnTfr+uuvL3UTAIAyUtZDVwMHDoyePXvGjh072m1P64MHDy5ZuwCA7qGsg07v3r1j9OjRsWLFirZtzc3Nxfr48eNP6txplldtbW3U1dV1QEsBgHJU8qGrNFNq8+bNbetbtmyJDRs2xIABA2L48OHF1PKpU6fGmDFjYuzYsTF//vxiSnrrLKwTVV9fXyxpenlNTU0HvBIAoNyUPOg8+eSTMWHChLb1FGySFG4WL14c1157bezatauYSbV9+/YYNWpULF++/IgCZQCA16pqaWlpiQrW2qOTppqn6+9U+r1fACqFe11Vxvd3WdfoAACcjIoNOoqRASB/FRt0UiFyQ0NDrF27ttRNAQA6ScUGHQAgf4IOAJAtQQcAyJagAwBkq2KDjllXAJC/ig06Zl0BQP4qNugAAPkTdACAbAk6AEC2BB0AIFuCDgCQrYoNOqaXA0D+KjbomF4OAPmr2KADAORP0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbFRt0XEcHAPJXsUHHdXQAIH8VG3QAgPwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZqtig48rIAJC/ig06rowMAPmr2KADAORP0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZqtig46aeAJC/ig06buoJAPnrVeoGAEApjJi5rNu98VvvuKrUTeh2KrZHBwDIn6ADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbFRt0FixYELW1tVFXV1fqpgAAnaRig059fX00NDTE2rVrS90UAKCTVGzQAQDyJ+gAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZKvbB509e/bEmDFjYtSoUfGud70r7r333lI3CQAoE72im+vfv3+sXLky+vXrFwcOHCjCzu/93u/F2972tlI3DQAosW7fo9OzZ88i5CQHDx6MlpaWYgEAKHnQSb0xU6ZMiSFDhkRVVVUsXbr0iGMWLFgQI0aMiL59+8a4ceNizZo1RwxfjRw5MoYOHRq33HJLDBw4sAtfAQBQrkoedNJwUwopKcwczZIlS2LGjBkxZ86cWL9+fXHspEmTYufOnW3HnH766fHUU0/Fli1b4hvf+Ebs2LGjC18BAFCuSh50Jk+eHLfddltcffXVR90/b968mD59ekybNi1qa2tj4cKFxVDVokWLjjh20KBBRRB67LHHjvl8aXirsbGx3QIA5KnkQed4mpqaYt26dTFx4sS2bT169CjWV61aVayn3pt9+/YVj/fu3VsMhV144YXHPOfcuXOjpqambRk2bFgXvBIAoBTKOujs3r07Dh8+XPTUvFpa3759e/H4Zz/7WVxyySVFT076edNNN8XFF198zHPOmjWrCESty7Zt2zr9dQAApdHtp5ePHTs2NmzY8IaP79OnT7EAAPkr6x6dNHsqTR9/bXFxWh88eHDJ2gUAdA9lHXR69+4do0ePjhUrVrRta25uLtbHjx9/UudOs7xScXNdXV0HtBQAKEclH7rav39/bN68uW09TRFPQ1EDBgyI4cOHF1PLp06dWtzmIQ1TzZ8/v5iSnmZhnYz6+vpiSbOuUlEyAJCfkgedJ598MiZMmNC2noJNksLN4sWL49prr41du3bF7NmziwLkdE+r5cuXH1GgDADwWlUtFX6/hNYenTQDq7q6ukPPPWLmsg49HwB0N1vvuKqk399lXaMDAHAyBB0AIFsVG3TMugKA/FVs0EkzrhoaGmLt2rWlbgoA0EkqNugAAPkTdACAbAk6AEC2BB0AIFsVG3TMugKA/FVs0DHrCgDyV7FBBwDIn6ADAGRL0AEAsiXoAADZEnQAgGz1igqeXp6WV155pVhvbGzs8OdoPvhSh58TALqTxk74fn31eVtaWo57XFXL6x2Ruf/+7/+OYcOGlboZAMAJ2LZtWwwdOvSY+ys+6DQ3N8cLL7wQ/fv3j6qqqujIpJkCVPoAqqurO+y8dByfUfnzGZU/n1H5a8z0+yj10+zbty+GDBkSPXocuxKnYoeuWqU353hJ8GSlv1Q5/cXKkc+o/PmMyp/PqPxVZ/h9VFNT87rHKEYGALIl6AAA2RJ0OkmfPn1izpw5xU/Kk8+o/PmMyp/PqPz1qfDvo4ovRgYA8qVHBwDIlqADAGRL0AEAsiXoAADZEnQ62datW+OGG26Ic889N0499dR4+9vfXlS/NzU1dfZT8ybcfvvt8Ru/8RvRr1+/OP300713ZSDdi27EiBHRt2/fGDduXKxZs6bUTeJVVq5cGVOmTCmuSpuuKr906VLvTxmZO3du1NXVFVf9P/PMM+N3f/d349lnn41KJOh0so0bNxa3mbjnnnvimWeeibvuuisWLlwYf/EXf9HZT82bkILnNddcEzfeeKP3rQwsWbIkZsyYUfynYP369TFy5MiYNGlS7Ny5s9RN4/8dOHCg+FxSIKX8PProo1FfXx+rV6+OH/zgB3Ho0KF4//vfX3xulcb08hL43Oc+F1/+8pfj+eefL8XTcxyLFy+Om2++Ofbs2eN9KqHUg5P+N/rFL36xWE//WUj36rnpppti5syZPpsyk3p0HnjggaLXgPK0a9euomcnBaBLL700KokenRLYu3dvDBgwoBRPDd2id23dunUxceLEdvekS+urVq0qadugO3/vJJX43SPodLHNmzfHF77whfiTP/mTrn5q6BZ2794dhw8fjkGDBrXbnta3b99esnZBd9Xc3Fz0VL/3ve+Nd73rXVFpBJ0TlLrPU3ft8ZZUn/Nq//M//xNXXHFFUQsyffr0jvj86ODPCCA39fX18eMf/zjuv//+qES9St2A7urjH/94XH/99cc95rzzzmt7/MILL8SECROKmT1f+cpXuqCFvNnPiPIwcODA6NmzZ+zYsaPd9rQ+ePDgkrULuqM/+7M/i+9973vFLLmhQ4dGJRJ0TtAZZ5xRLG9E6slJIWf06NHxta99rag3oLw+I8pH7969i38rK1asaCtuTV3vaT390gZeX0tLS1G8/8ADD8QjjzxSXOKkUgk6nSyFnPe9731xzjnnxJ133llUvrfyv9Py8fOf/zx++ctfFj9TfciGDRuK7eeff36cdtpppW5exUlTy6dOnRpjxoyJsWPHxvz584tpsdOmTSt10/h/+/fvL2oOW23ZsqX4d5OKXYcPH+59KoPhqm984xvx4IMPFtfSaa1vq6mpKa7pVklML++C6crH+uWcEjflIQ1xff3rXz9i+8MPP1wEVbpemlqeLsWQfkGPGjUq7r777mLaOeUh9RKknurXSgE1/d6jtFIN4tF87Wtfe90h/dwIOgBAthSLAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAB0iXUH65ptv9m4CZUXQAQCyJegAANkSdIBO8eKLL8Yf//Efx1vf+tbo169fTJ48OTZt2tTumHvvvTeGDRtW7L/66qtj3rx5cfrppx/znFu3bi1uVvitb30rLrnkkuIuzHV1dfHcc8/F2rVri7udp7vNp+fatWtX259L+377t387Bg4cWNy9+bLLLov169e3u8HuX/3VXxV33e7Tp08MGTIkPvrRj7bt/9KXvhQXXHBB9O3bNwYNGhS///u/37avubk55s6dG+eee27RnpEjR8a3v/3tdu/DddddF2eccUaxP50n3VgR6BqCDtAp0h2Sn3zyyfjud78bq1atKsLElVdeGYcOHSr2P/744/Gnf/qn8bGPfSw2bNhQBJHbb7/9DZ17zpw58Zd/+ZdFWOnVq1f8wR/8QXzyk5+Mv/u7v4vHHnssNm/eHLNnz247ft++fcVdtX/0ox/F6tWri7CR2pK2J9/5znfirrvuinvuuacIY0uXLo2LL7642JdeQwo9f/3Xfx3PPvtsLF++PC699NK2c6eQ8w//8A+xcOHCeOaZZ+LP//zP4w//8A/j0UcfLfbfeuut0dDQEP/2b/8WP/nJT+LLX/5yEbiALtIC0AEuu+yylo997GPF4+eee64l/Xp5/PHH2/bv3r275dRTT2351re+Vaxfe+21LVdddVW7c1x33XUtNTU1x3yOLVu2FOf96le/2rbtm9/8ZrFtxYoVbdvmzp3bcuGFFx7zPIcPH27p379/y0MPPVSsf/7zn295xzve0dLU1HTEsd/5zndaqqurWxobG4/Y9/LLL7f069ev5Yknnmi3/YYbbmj50Ic+VDyeMmVKy7Rp047ZFqBz6dEBOlzquUg9LePGjWvb9ra3vS0uvPDCYl+SekfGjh3b7s+9dv1Y3v3ud7c9TkNJSWsPTOu2nTt3tq3v2LEjpk+fXvTkpKGr6urq2L9/f/z85z8v9l9zzTXxq1/9Ks4777ziuAceeCBeeeWVYl/qaTrnnHOKfX/0R38U9913X7z00kvFvtRzlB6nY9KQWeuSenh++tOfFsfceOONcf/998eoUaOKXqcnnnjiBN5R4EQJOkC3c8opp7Q9TjU7R9uWamdapWGrNDyWhrZS0EiPU/Bqamoq9qc6oRS8Ui1OqqP5yEc+UgxPpWG2/v37F0Nk3/zmN+Oss84qhsRSHc6ePXuKsJQsW7asOGfrkoaqWut0Ur3Qz372s2JI64UXXojf+q3fik984hNd9l5BpRN0gA73zne+s+gR+Y//+I+2bb/4xS+KMFFbW1usp96dVCT8aq9d7yipHijV2aS6nIsuuqgoON69e3e7Y1LAmTJlStx9993xyCOPFHVF//Vf/1XsS71TEydOjL/927+Np59+uiiK/uEPf1i8lnSu1DN0/vnnt1tSeGqVCpFT2Pqnf/qnmD9/fnzlK1/plNcJHKnXUbYBnJQ0RPQ7v/M7xTBQKvBNvSIzZ86Ms88+u9ie3HTTTUWvSZpplQJGCg6pYLe1h6aj2/OP//iPxaysxsbGuOWWW4pg02rx4sVx+PDhYqgtzQBLgSTtT0NW3/ve9+L5558v2ppmkP3rv/5r0VuUglp6Xal3JvXWpG2/+Zu/GXv37i2CVRoeS+Em9QCNHj26CFgHDx4szpeCINA19OgAnSJNoU5f8B/4wAdi/PjxxayrFBJah5je+973FjOVUtBJQ0FpNlMKDGkKd0f7+7//+2Ka93ve856izib17px55plt+9OU9jTVPbUp1f/8+7//ezz00EPF8Fba9y//8i9x+eWXFwEltTkNY6XgkvzN3/xNMbMqzb5K+6+44opiKCtNN0969+4ds2bNKs6bwlLPnj2Lmh2ga1SliuQuei6A40o9QBs3biymiAN0BENXQMnceeedxYylt7zlLcWw1de//vWiIBigo+jRAUrmgx/8YFH4my7cl6Zvp7qddBFBgI4i6AAA2VKMDABkS9ABALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAiFz9HxqO8xe+BFYOAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def kroupa_imf_sample(n_samples, m1_min=0.01, m1_max=200):\n", + " \"\"\"\n", + " Sample from Kroupa IMF with mass bounds [0.01, 0.08, 0.5, 200]\n", + " using inverse CDF sampling.\n", + "\n", + " IMF convention:\n", + " dN/dm ~ m^alpha\n", + " \"\"\"\n", + "\n", + " # Kroupa IMF mass bounds\n", + " m1, m2, m3, m4 = 0.01, 0.08, 0.5, 200\n", + "\n", + " # IMF slopes\n", + " alpha1, alpha2, alpha3 = -0.3, -1.3, -2.3\n", + "\n", + " # Enforce truncation\n", + " m_min = max(m1_min, m1)\n", + " m_max = min(m1_max, m4)\n", + " if m_max <= m_min:\n", + " raise ValueError(\"Invalid mass bounds\")\n", + "\n", + " # Continuity factors (ensure dN/dm is continuous)\n", + " k1 = 1.0\n", + " k2 = k1 * m2**(alpha1 - alpha2)\n", + " k3 = k2 * m3**(alpha2 - alpha3)\n", + "\n", + " # Integral of k * m^alpha over a segment\n", + " def segment_integral(k, alpha, lo, hi):\n", + " if hi <= lo:\n", + " return 0.0\n", + " return k * (hi**(alpha + 1) - lo**(alpha + 1)) / (alpha + 1)\n", + "\n", + " # Segment limits after truncation\n", + " seg1_lo, seg1_hi = m_min, min(m2, m_max)\n", + " seg2_lo, seg2_hi = max(m_min, m2), min(m3, m_max)\n", + " seg3_lo, seg3_hi = max(m_min, m3), m_max\n", + "\n", + " # Relative weights of each segment\n", + " I1 = segment_integral(k1, alpha1, seg1_lo, seg1_hi)\n", + " I2 = segment_integral(k2, alpha2, seg2_lo, seg2_hi)\n", + " I3 = segment_integral(k3, alpha3, seg3_lo, seg3_hi)\n", + " Itot = I1 + I2 + I3\n", + "\n", + " p1, p2, p3 = I1 / Itot, I2 / Itot, I3 / Itot\n", + "\n", + " # Number of samples per segment\n", + " n1, n2, n3 = np.random.multinomial(n_samples, [p1, p2, p3])\n", + "\n", + " masses = np.zeros(n_samples)\n", + " idx = 0\n", + "\n", + " # Inverse CDF for power-law IMF segment\n", + " def inverse_cdf(alpha, lo, hi, u):\n", + " return (u * (hi**(alpha + 1) - lo**(alpha + 1)) + lo**(alpha + 1))**(1 / (alpha + 1))\n", + "\n", + " # Segment 1: 0.01 – 0.08 Msun\n", + " if n1 > 0:\n", + " u1 = np.random.uniform(0, 1, n1)\n", + " masses[idx:idx+n1] = inverse_cdf(alpha1, seg1_lo, seg1_hi, u1)\n", + " idx += n1\n", + "\n", + " # Segment 2: 0.08 – 0.5 Msun\n", + " if n2 > 0:\n", + " u2 = np.random.uniform(0, 1, n2)\n", + " masses[idx:idx+n2] = inverse_cdf(alpha2, seg2_lo, seg2_hi, u2)\n", + " idx += n2\n", + "\n", + " # Segment 3: 0.5 – 200 Msun\n", + " if n3 > 0:\n", + " u3 = np.random.uniform(0, 1, n3)\n", + " masses[idx:idx+n3] = inverse_cdf(alpha3, seg3_lo, seg3_hi, u3)\n", + " idx += n3\n", + "\n", + " np.random.shuffle(masses)\n", + " return masses\n", + "\n", + "\n", + "\n", + "primary_masses = kroupa_imf_sample(int(5e6), m1_min=0.01, m1_max=200)\n", + "log_masses = np.log10(primary_masses)\n", + "plt.hist( log_masses)\n", + "plt.xlabel('log masses')\n", + "plt.ylabel('count')\n", + "plt.yscale('log')\n", + "plt.show()\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using mass-dependent binaryFractions from Offner et al. (2023)\n" + ] + } + ], + "source": [ + "def get_binary_fraction(m1, binaryFractions=None, binary_bin_edges=None):\n", + " \"\"\"\n", + " Get binary fraction for a given primary mass.\n", + "\n", + " - If `binaryFractions` is a scalar (float/int), return a constant fraction across all masses.\n", + " - If `binaryFractions` is None or a list, use mass-dependent binary fractions with the provided or default bin edges.\n", + " \"\"\"\n", + " # If a constant fraction is requested, broadcast it to the shape of m1\n", + " if isinstance(binaryFractions, (int, float, np.floating)):\n", + " print(f'using constant binaryFractions = {binaryFractions}')\n", + " m1_arr = np.asarray(m1)\n", + " if m1_arr.ndim == 0:\n", + " return float(binaryFractions)\n", + " return np.full(m1_arr.shape, float(binaryFractions))\n", + "\n", + " # Default mass-dependent fractions from Offner et al. (2023)\n", + " if binaryFractions is None:\n", + " print('using mass-dependent binaryFractions from Offner et al. (2023)')\n", + " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0]\n", + " \n", + " # Default bin edges\n", + " if binary_bin_edges is None:\n", + " binary_bin_edges = [0.01, 0.08, 0.5, 1, 10, 200]\n", + " \n", + " # Map masses to bins\n", + " m1_arr = np.asarray(m1)\n", + " bin_index = np.digitize(m1_arr, binary_bin_edges) - 1\n", + " bin_index = np.clip(bin_index, 0, len(binaryFractions) - 1)\n", + " \n", + " fb_array = np.array(binaryFractions, dtype=float)[bin_index]\n", + " if m1_arr.ndim == 0:\n", + " return float(fb_array)\n", + " return fb_array\n", + "\n", + "\n", + "fb_for_every_m1 = get_binary_fraction(primary_masses, binaryFractions=None, binary_bin_edges=None)\n", + "\n", + "# print(binary_fractions, bin_indices)\n", + "# print(fb_for_every_m1)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "def create_mock_universe(n_primary=10**7, m1_min=10, m1_max=150, m2_min=0.1, \n", + " binaryFractions=None, binary_bin_edges=None, verbose = False):\n", + " \"\"\"\n", + " Create a mock universe with mass-dependent binary fractions\n", + " \n", + " Parameters:\n", + " -----------\n", + " n_primary : int\n", + " Number of primary stars to sample\n", + " m1_min, m1_max : float\n", + " Min and max primary masses for COMPAS simulation\n", + " m2_min : float\n", + " Minimum secondary mass\n", + " binaryFractions : list\n", + " Binary fractions for each mass bin\n", + " binary_bin_edges : list\n", + " Mass bin edges for binary fractions\n", + " \n", + " Returns:\n", + " --------\n", + " dict : Dictionary containing simulation results\n", + " \"\"\"\n", + " \n", + " if verbose: print(\"Step 1: Sampling primary masses from Kroupa IMF...\")\n", + " # Step 1: Sample primary masses from Kroupa IMF\n", + " primary_masses = kroupa_imf_sample(n_primary)\n", + " \n", + " if verbose: print(f\"Sampled {len(primary_masses)} primary masses\")\n", + " if verbose: print(f\"Mass range: {primary_masses.min():.3f} - {primary_masses.max():.3f} M_sun\")\n", + " \n", + " if verbose: print(\"\\nStep 2: Sampling binaries based on mass-dependent binary fraction...\")\n", + " # Step 2: Sample binaries based on mass-dependent binary fraction\n", + " fb_for_every_m1 = get_binary_fraction(primary_masses, binaryFractions, binary_bin_edges)\n", + " \n", + " # Determine which stars are in binaries\n", + " binary_mask = np.random.random(len(primary_masses)) < fb_for_every_m1\n", + " \n", + " # For binary systems, sample secondary masses (flat mass ratio distribution)\n", + " secondary_masses = np.zeros(len(primary_masses))\n", + " binary_systems = []\n", + " \n", + " for i, (m1, is_binary) in enumerate(zip(primary_masses, binary_mask)):\n", + " if is_binary:\n", + " # Sample mass ratio q = m2/m1 from uniform distribution [0, 1]\n", + " q = np.random.uniform(0, 1)\n", + " m2 = q * m1\n", + " secondary_masses[i] = m2\n", + " binary_systems.append((m1, m2))\n", + " \n", + " if verbose: print(f\"Created {len(binary_systems)} binary systems\")\n", + " if verbose: print(f\"Binary fraction varies from {np.min(fb_for_every_m1)} to {np.max(fb_for_every_m1)}\")\n", + " \n", + " if verbose: print(\"\\nStep 3: Calculating total mass of stellar systems...\")\n", + " # Step 3: Sum total mass of stellar systems\n", + " total_mass_singles = np.sum(primary_masses[~binary_mask])\n", + " total_mass_binaries = np.sum(primary_masses[binary_mask] + secondary_masses[binary_mask])\n", + " total_mass_universe = total_mass_singles + total_mass_binaries\n", + " \n", + " if verbose: print(f\"Total mass in single stars: {total_mass_singles:.2e} M_sun\")\n", + " if verbose: print(f\"Total mass in binary systems: {total_mass_binaries:.2e} M_sun\")\n", + " if verbose: print(f\"Total mass in universe: {total_mass_universe:.2e} M_sun\")\n", + " \n", + " if verbose: print(\"\\nStep 4: Identifying systems for COMPAS simulation...\")\n", + " # Step 4: Figure out which systems would be part of COMPAS simulation\n", + " compas_mask = ((primary_masses >= m1_min) & \n", + " (primary_masses <= m1_max) & \n", + " (binary_mask) & # Only binary systems\n", + " (secondary_masses >= m2_min))\n", + " \n", + " compas_systems = []\n", + " for i, (m1, m2, in_compas) in enumerate(zip(primary_masses, secondary_masses, compas_mask)):\n", + " if in_compas:\n", + " compas_systems.append((m1, m2))\n", + " \n", + " if verbose: print(f\"Number of binaries in COMPAS simulation: {len(compas_systems)}\")\n", + " if verbose: print(f\"COMPAS mass range: {m1_min} - {m1_max} M_sun (primary), >= {m2_min} M_sun (secondary)\")\n", + " \n", + " # Calculate statistics\n", + " results = {\n", + " 'n_primary': len(primary_masses),\n", + " 'n_binaries': len(binary_systems),\n", + " 'n_compas': len(compas_systems),\n", + " 'total_mass_universe': total_mass_universe,\n", + " 'total_mass_singles': total_mass_singles,\n", + " 'total_mass_binaries': total_mass_binaries,\n", + " 'primary_masses': primary_masses,\n", + " 'secondary_masses': secondary_masses,\n", + " 'binary_mask': binary_mask,\n", + " 'compas_mask': compas_mask,\n", + " 'binary_systems': binary_systems,\n", + " 'compas_systems': compas_systems,\n", + " 'fb_for_every_m1': fb_for_every_m1\n", + " }\n", + " \n", + " return results\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compare analytical to MC\n", + "\n", + "### for a fixed value of fbin" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " old analytical m_rep =253.53899506390485, new analytical m_rep = 253.53899506390468\n", + "using constant binaryFractions = 0.7\n", + "MC_results m_rep = 252.58192718551336\n", + "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.002969911830251223\n" + ] + } + ], + "source": [ + "m1_min = 10\n", + "m1_max = 150\n", + "m2_min = 0.1\n", + "fbin = 0.7\n", + "\n", + "# Analytical calculation\n", + "old_analytical = OLD_analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", + "\n", + "new_analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", + "\n", + "print(f\" old analytical m_rep ={old_analytical['m_rep']}, \\\n", + " new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\" )\n", + "\n", + "# MCMC simulation\n", + "MC_results = create_mock_universe( n_primary=int(1e7), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", + "\n", + "print('MC_results m_rep = ', MC_results['total_mass_universe']/MC_results['n_compas'] )\n", + "\n", + "\n", + "print(f\"{10*'*'} fint = N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']},\\\n", + " MC nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "***\n", + "\n", + "They agree!!\n", + "\n", + "***\n", + "\n", + "## Now compare MC to new analytical \n", + "\n", + "### (both with variable fbin)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " new analytical m_rep = 42.89068714952707\n", + "using mass-dependent binaryFractions from Offner et al. (2023)\n", + "MC_results m_rep = 78.22524271488052\n", + "fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.02715404524930118, \n", + " MC nbin_compas/nbin: 0.027145053029190222\n" + ] + } + ], + "source": [ + "m1_min = 5\n", + "m1_max = 150\n", + "m2_min = 0.1\n", + "fbin = None\n", + "\n", + "# Analytical calculation\n", + "new_analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", + "print(f\" new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\" )\n", + "\n", + "# MCMC simulation\n", + "MC_results = create_mock_universe( n_primary=int(1e7), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", + "\n", + "print('MC_results m_rep = ', MC_results['total_mass_universe']/MC_results['n_compas'] )\n", + "\n", + "# fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates\n", + "print(f\"fint = N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']}, \\n \\\n", + " MC nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['n_primary', 'n_binaries', 'n_compas', 'total_mass_universe', 'total_mass_singles', 'total_mass_binaries', 'primary_masses', 'secondary_masses', 'binary_mask', 'compas_mask', 'binary_systems', 'compas_systems', 'fb_for_every_m1'])" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "MC_results.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['M_sf_Univ_per_N_binary_COMPAS', 'fint', 'Average_mass_stellar_sys_per_fbin', 'piece_wise_integral', 'alpha', 'binaryFractions', 'binary_bin_edges', 'imf_mass_bounds'])" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "new_analytical.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.007368877135189561, MC nbin_compas/nbin: 0.026630534785299677\n" + ] + } + ], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "m1_min = 0.5\n", + "m1_min = 1\n", + "m1_min = 5\n", + "m1_min = 10\n", + "m1_min = 20\n", + "m1_min = 50\n", + "m1_min = 100\n" + ] + } + ], + "source": [ + "m1_min = 5\n", + "m1_max = 200\n", + "m2_min = 0.1 \n", + "fbin = 0.7 #None #0.7\n", + "\n", + "\n", + "binaryFractions = None #0.7#[0.1, 0.225, 0.5, 0.8, 1.0]\n", + "\n", + "\n", + "analytical_results_list = []\n", + "MC_results_list = []\n", + "\n", + "m1_mins = [0.5, 1, 5, 10, 20, 50, 100,]\n", + "for m1_min in m1_mins:\n", + " print(f'm1_min = {m1_min}')\n", + "### Analytical\n", + " M_sf_Univ_per_N_binary_COMPAS = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", + "\n", + "\n", + " ### MCMC\n", + " results = create_mock_universe(n_primary=10**7,m1_min=m1_min,m1_max=m1_max,m2_min=m2_min,\n", + " binaryFractions=binaryFractions,binary_bin_edges=None)\n", + " MCMC_Msf_per_Nbin_COMPAS = results['total_mass_universe']/results['n_compas']\n", + "\n", + " analytical_results_list.append(M_sf_Univ_per_N_binary_COMPAS['M_sf_Univ_per_N_binary_COMPAS'])\n", + " MC_results_list.append(MCMC_Msf_per_Nbin_COMPAS)\n", + "\n", + "# print('\\n', 50*'*')\n", + "# print(f'Analytical M_sf_Univ/N_bin_COMPAS = {M_sf_Univ_per_N_binary_COMPAS}')\n", + "# print(f'MCMC M_sf_Univ/N_bin_COMPAS = {MCMC_Msf_per_Nbin_COMPAS}')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[5.616057836080422, 13.007919822132509, 101.32434416603401, 251.1374670796301, 635.9562719126669, 2376.71231751028, 8224.422379739088]\n", + "[np.float64(5.157970165610914), np.float64(9.36058824727188), np.float64(66.91977886848493), np.float64(145.92929321284583), np.float64(372.72197499297187), np.float64(1406.7721893999285), np.float64(4753.33613920003)]\n" + ] + } + ], + "source": [ + "print(analytical_results_list)\n", + "print(MC_results_list)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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uHTt2tU5FikmJsbt+/TYcU2fNUy5/VB58kh0dLRbmUqVKv3tNRzXXLvoHC5cuGAert1uAAAgOHl0peo//vhDhgwZImXLls1SeV3EsU6dOmZmmrYO6bgfbX3S8UK+NmrUKNMa5XjoQGwAABCcrjoQnT171qxDpDO44uLiZNmyZabFJyt05ph+j6vrr79e9u3bZ76OjY01z4cOHXIro8eOa/qclJTkdv3SpUtm5plrmYxew/U90hs6dKicOHHC+WCxSQAAgleOA9GqVaukR48eJtToytU6Tmfp0qXm/KBBg7L0GjrDTMfxpG9l0tlgSru5NLAsXrzYretKxwY1atTIHOtzcnKyc+abWrJkiWl90rFGjjI68ywlJcVZRmekVa1aNcPuMpU3b14zjd/1AQAAglO2A5FOi69evbq0b9/ehAkNGps2bZKQkBCzllB29O/f3wQo7TLbuXOnTJ8+3UyF79Wrl7mur9mvXz959dVXzQBsfZ8uXbpI6dKlpW3bts4WpZYtW5qutjVr1phB3b179zYDu7Wcevjhh824JF2fSKfnf/nll/L2229nuSULAAAEuexOXwsLC7Oee+4569KlS27n8+TJY23ZsiXb0+HmzZtn1ahRw0ylr1atmvXhhx/+ber9sGHDrJiYGFNGp/Rv377drczRo0etjh07WgULFrSioqKsbt26WadOnXIrs3HjRjPFX1+jTJky1ujRo7NVT6bdAwAQeLL6+R2i/5PdwcY6ZkgXRuzYsaN07tzZbOiqiytu3Ljxb2OCgoV21engah1PRPcZAADB9fmd7S4zHWys43w+/fRTM2Vdx+nUqlXLbLHBmj4AAMBWg6pvu+02+eSTT+TgwYPy1FNPSd26dc25xo0bm0HWAAAAgSLbXWYOumihTm/XvcUcdNDzxx9/bAZHp58KH+joMgMAIPB4rcvs8OHD0qpVKylYsKB54YYNG5oZYkp3qn/rrbfMDvYAAACBItuBSFei3rBhg9m649///rdZA0invLvSAdYAAABBu5eZLmg4depUszmquvvuu81aQNqFposZAgAABH0L0YEDB8ysMocqVaqYIKSDqwEAAGwzyywsLOxvxzkcmw0AABB4XWYafK677jqzrYbD6dOnzW71oaH/y1e6uSoAAEBQBiJdpRoAAMDWgahr167eqQkAAECgrVQNAABg2xaiypUrZ6ncrl27clIfAAAA/w9Ee/bskQoVKsjDDz8sJUuW9E6tAAAA/DkQffnllzJ58mSzgatu4dG9e3e566673GaYAQAABJJsp5gOHTrIggULzP5lusN9//79pVy5cvLss8/Kjh07vFNLAAAAL8pxs06ZMmXk+eefNyFId7dfvXq1VKtWTY4fP+7ZGgIAAPhbl5mr8+fPy1dffWW60DQQaetR/vz5PVc7AAAAfw1EGn4+/vhjmTlzppl1puOIvv76aylatKjnawgAAOBvgah69eqSlJRkZpktW7bMbaNXAACAQBRiZXNXVp1NVqBAAcmTJ4/bfmbpBdteZidPnpTChQvLiRMnJCoqytfVAQAAHvz8Zi8zAABge+xlBgAAbC/Hs8zOnTsnixYtkj/++MMcV61aVZo1ayaRkZGerB8AAIB/BqK5c+dKjx495MiRI27nixcvbmaftWnTxlP1AwAA8L+FGVeuXCnt27eXW2+9VVasWGEGT+tj+fLl0qRJE3Nt1apV3qktAACAP8wy033LdKuODz74IMPr//rXv2T//v3y/fffSzBhlhkAAMH7+Z3tFiJt/endu3em13v16iXx8fHZfVkAAACfCc3JYOorJSxNYbqlBwAAQNAGoipVqsiSJUsyvb548WJTBgAAIGgDUbdu3WTgwIEZjhH67rvvZPDgwfLoo496qn4AAAD+N+3+6aefNjPN7r77brP20PXXXy86Lnvbtm2yY8cOadu2rfTr1887tQUAAPCHFiLdy2zWrFnyxRdfmED0+++/y/bt26VatWry+eefm13vtQwAAEDQTru3K6bdAwAQeLw27f7AgQNmDJG+QXr6ZoMGDZJDhw5lv8YAAAA+ku1ANG7cOBOGMkpZmsBOnTplygAAAARtIFq4cKF06dIl0+t6bf78+VdbLwAAAP8NRLt375by5ctner1s2bKyZ8+eq60XAACA/waiyMjIKwYevaZlAAAAgjYQNWjQQD799NNMr0+bNk3q169/tfUCAADw34UZdYbZnXfeaQZQ64yymJgYc15nlo0ZM0amTp0qP/74ozfqCgAAgk1aqsjelSKnD4kUjBGp0FgkNCww1iH64IMPzIrVKSkpZrZZSEiImXIfHh4ub775pvTs2VOCDesQAQDgYVvniiwcInLywP/ORZUWafm6SNw9ufr5neOFGRMSEmTmzJmyc+dOs3XHdddd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Tp3M19zrq6f/BnzzzTfO46zc061bt5rvW7t2rbPMggULrJCQECshIcHyNFqIctHFixdl3bp1plnQdUsQPY6Pj/dp3YLNiRMnzHN0dLR51vuurUau975atWpSvnx57n0OaGtc69at3e6n4j57zty5c6VevXrSoUMH0w1cu3ZtmTRpkvP67t27JTEx0e1e6/YE2g3Pvc6exo0by+LFi+WPP/4wxxs3bpTly5dLq1atzDH32vOyck/1WbvJ9L8DBy2vn5vaouRp7GWWi44cOWL6qmNiYtzO6/Hvv//us3oF40a8OqZFuxtq1Khhzul/eBEREeY/rvT3Xq8h62bMmGG6e7XLLD3us+fs2rXLdONoF/tzzz1n7nffvn3N/e3atavzfmb07wn3OnueffZZs7mohvewsDDz7/TIkSNNd43iXnteVu6pPusfA67y5Mlj/tD1xn0nECEoWy82b95s/sKDZ+lO1E8//bTpz9dJAfBusNe/jF977TVzrC1E+v9rHW+hgQieM3PmTPn8889l+vTpUr16ddmwYYP5o0oHA3Ov7YMus1xUvHhx89dH+hk3ehwbG+uzegWT3r17m4F3S5culbJlyzrP6/3VLsvk5GS38tz77NEuMZ0AUKdOHfOXmj504LQOjNSv9a877rNn6MybuLg4t3PXX3+97Nu3z3ztuJ/8e3L1Bg0aZFqJHnroITOTr3PnzmZygM5eVdxrz8vKPdXn9BOOLl26ZGaeeeO+E4hykTZ1161b1/RVu/4VqMeNGjXyad0CnY7Z0zD0zTffyJIlS8z0WVd633W2juu912n5+uHCvc+6pk2byqZNm8xf0I6HtmJo14Lja+6zZ2iXb/qlI3SMS4UKFczX+v9x/VBwvdfa7aNjK7jX2XP27FkzLsWV/vGq/z4r7rXnZeWe6rP+caV/iDnov+/6e9GxRh7n8WHauKIZM2aYUfRTp041I+ifeOIJq0iRIlZiYqKvqxbQevbsaRUuXNj66aefrIMHDzofZ8+edZZ58sknrfLly1tLliyxfvnlF6tRo0bmgavjOstMcZ89Y82aNVaePHmskSNHWjt27LA+//xzK3/+/NZnn33mLDN69Gjz78ecOXOs3377zbr33nutSpUqWefOnfNp3QNN165drTJlyljz58+3du/ebc2ePdsqXry4NXjwYGcZ7nXOZqP++uuv5qFxY9y4cebrvXv3ZvmetmzZ0qpdu7a1evVqa/ny5WZ2a8eOHS1vIBD5wDvvvGM+MCIiIsw0/FWrVvm6SgFP/2PL6DFlyhRnGf2P7KmnnrKKFi1qPljuu+8+E5rg2UDEffacefPmWTVq1DB/RFWrVs368MMP3a7r1OVhw4ZZMTExpkzTpk2t7du3+6y+gerkyZPm/8P673K+fPmsypUrW88//7x14cIFZxnudfYtXbo0w3+XNYBm9Z4ePXrUBKCCBQtaUVFRVrdu3UzQ8gZ2uwcAALbHGCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAyKaKFSvKW2+95etqAPAgVqoGEND69u0rK1askM2bN5vd4HWTWW87fPiwFChQQPLnz+/19wKQO2ghAhDwunfvLg8++GCuvV+JEiUIQ0CQIRAB8Bu333679OnTR/r16ydFixaVmJgYmTRpkpw5c0a6desmhQoVkmuvvVYWLFjg/J7x48dLr169pHLlytl+v6lTp0qRIkVk/vz5UrVqVRNy2rdvL2fPnpVPPvnEdI1pPbQVKjU1NdMus5CQEPnoo4/kvvvuM69RpUoVmTt3rvP68ePHpVOnTiZIRUZGmutTpky5qnsFwLMIRAD8igaR4sWLy5o1a0w46tmzp3To0EEaN24s69evl+bNm0vnzp1NaPEEfR0NVTNmzJCFCxfKTz/9ZILN999/bx6ffvqpfPDBB/LVV19d8XVGjBghDzzwgPz2229y1113mQB07Ngxc23YsGGydetWE+S2bdsmEydOND8jAP9BIALgV2rVqiUvvPCCaUUZOnSo5MuXz4SHxx9/3JwbPny4HD161AQPT0hJSTEBpXbt2nLrrbeaFqLly5fLxx9/LHFxcXL33XfLHXfcIUuXLr3i6zz66KPSsWNH04L12muvyenTp02oU/v27TOvX69ePdO61KxZM2nTpo1H6g/AMwhEAPzKDTfc4Pw6LCxMihUrJjVr1nSe0240lZSU5JH30y6ua665xu31NbQULFjQ7dw/vZ9rvXXAdVRUlPN7tJVLW6BuvPFGGTx4sKxcudIjdQfgOQQiAH4lPDzc7VjH57ie02OVlpaWK+/nOPdP73el72nVqpXs3btX+vfvLwcOHJCmTZvKwIEDPVJ/AJ5BIAKAXKADqrt27SqfffaZGZD94Ycf+rpKAFzkcT0AgECzc+dOM14nMTFRzp0751yHSMf/REREiD/QcU9169aV6tWry4ULF8ysNl0zCYD/IBABCGg9evSQZcuWOY918LLavXu3GQvkDzSY6QDxPXv2mGn3TZo0MWOKAPgPVqoGAAC2xxgiAABgewQiAEFLZ3fp9PmMHrpWEAA40GUGIGglJCSYgdYZiY6ONg8AUAQiAABge3SZAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAAsbv/D1sWiOpJ0WQXAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.scatter(m1_mins, analytical_results_list, label = 'analytical')\n", + "plt.scatter(m1_mins, MC_results_list, label = 'MC')\n", + "\n", + "plt.xlabel('m1_mins')\n", + "plt.ylabel('M_sf_Univ/N_bin_COMPAS')\n", + "\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGxCAYAAACeKZf2AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQAANAlJREFUeJzt3Qd0VNXa//EntAQEAoQaehXpTZCigGChRMD3iiJKE7wqCgKi8CooehUUpXm5KIKAiiLqDYIiglQRLj0ovUXBEEBACKFLzn89+74z/yQkYSaZeub7WeuszJxzktk5gcwvez97nzDLsiwBAACwiVz+bgAAAIAnEW4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICt5JEQk5KSIkePHpVChQpJWFiYv5sDAABcoGsOnzt3TqKjoyVXrqz7ZkIu3GiwKV++vL+bAQAAsuHIkSNSrly5LM8JuXCjPTaOi1O4cGF/NwcAALggKSnJdE443sezEnLhxjEUpcGGcAMAQHBxpaSEgmIAAGArhBsAAGArhBsAAGArhBsAAGArhBsAAGArhBsAAGArhBsAAGArhBsAAGArhBsAAGArIbdCMQAA8I5rKZZsjD8tJ85dkpKFIqRp5WKSO5fvb1JNuAEAADm2ZEeijFm0SxLPXnLuKxMZIS/H1JJ765QRX2JYCgAA5DjYPPnJ1jTBRh07e8ns1+O+RLgBAAA5GorSHhsrg2OOfXpcz/MVwg0AAMg2rbFJ32OTmkYaPa7n+Qo1NyEoUAq+AADB78S5Sx49zxMINyEmkAq+AADBr2ShCI+e5wkMS4WQQCv4AgAEv6aVi5k/kjPr/9f9elzP8xXCTYgIxIIvAEDwy50rzPT+q/QBx/Fcj/uy/IFwEyICseALAGAP99YpI9MeaSSlI9MOPelz3e/rsgdqbkJEIBZ8AQDs4946ZeSuWqUDYsIK4SZEBGLBFwDAXnLnCpPmVaP83QzCTagVfGnxcEZVNWH/133oy4IvdzB9HQDgKsJNiBV86awojQRWABR8uYrp6wAAd1BQHEICreDLFUxfBwC4i56bEBNIBV85nb6uLdbj+v0EYvsBACHYc7NmzRqJiYmR6OhoCQsLkwULFmR5fp8+fcx56bfatWv7rM12Kvjq0qCs+RiowYDp6wCAoAs358+fl/r168vUqVNdOn/y5MmSmJjo3I4cOSLFihWTBx54wOtthe8xfR0AEHTDUh06dDCbqyIjI83moD09f/75p/Tt29dLLYQ/MX0dABByBcUzZ86U9u3bS8WKFf3dFITI/UoAAIEvaMPN0aNH5bvvvpP+/ftned7ly5clKSkpzYbgEIj3KwEABL6gDTdz5syRIkWKSNeuXbM8b+zYsc7hLN3Kly/vszYiNKevwz8z69YfPCVfxyWYj9wAFghtYZZlBcRvAZ31FBsbe8OworTJNWrUkM6dO8vEiRNv2HOjm4P23GjAOXv2rBQuXNgjbYf3sUIxMsMij0BoSEpKMp0Urrx/B+U6N6tXr5YDBw7IY489dsNzw8PDzYbgFij3K0FgLvKY/i80xyKP9O4Bocmvw1LJyckSFxdnNhUfH28eHz582DwfOXKk9OrVK8NC4mbNmkmdOnV83mYAwbHIo9LjDFEBocev4Wbz5s3SsGFDs6mhQ4eax6NHjzbPdS0bR9Bx0O6or776yqVeGwD2xSKPAAJyWKpNmzamfiYzs2fPvm6fjrdduHDByy0DEOhY5BGA7WZLAQhtLPIIIDOEGwBBiUUeAWSGcAMgKLHII4DMEG4ABC0WeQRgm3VuAMBBA8xdtUqzyCMAJ8INgKDHIo8AUmNYCgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2ArhBgAA2Ipfw82aNWskJiZGoqOjJSwsTBYsWHDDz7l8+bK8+OKLUrFiRQkPD5dKlSrJhx9+6JP2AgCAwJfHny9+/vx5qV+/vvTr10/uv/9+lz6ne/fucvz4cZk5c6ZUq1ZNEhMTJSUlxettBQAAwcGv4aZDhw5mc9WSJUtk9erVcujQISlWrJjZpz03AAAAQVlzs3DhQmnSpIm89dZbUrZsWalRo4Y899xzcvHixSyHsZKSktJsAADAvvzac+Mu7bFZu3atRERESGxsrJw8eVKeeuopOXXqlMyaNSvDzxk7dqyMGTPG520FAAD+EVQ9N1pbo4XHc+fOlaZNm0rHjh1lwoQJMmfOnEx7b0aOHClnz551bkeOHPF5uwEAgO8EVc9NmTJlzHBUZGSkc98tt9wilmXJ77//LtWrV7/uc3RGlW4AACA0BFXPTcuWLeXo0aOSnJzs3Ldv3z7JlSuXlCtXzq9tAwAAgcGv4UZDSlxcnNlUfHy8eXz48GHnkFKvXr2c5z/88MMSFRUlffv2lV27dpl1coYPH26mkufPn99v3wcAAAgcfg03mzdvloYNG5pNDR061DwePXq0ea5r2DiCjipYsKAsW7ZMzpw5Y2ZN9ezZ0ywCOGXKFL99DwAAILCEWVqwEkJ0KrjW7GhxceHChf3dHAAA4OH376CquQEAALgRwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AALAVwg0AAAjNcLNlyxZp27atJCUlXXfs7Nmz5tj27ds93T4AAADvhJt33nlH7rzzTilcuPB1xyIjI+Wuu+6S8ePHu/fqAAAA/go3GzZskC5dumR6PCYmRtatW+epdgEAAHg33CQkJEihQoUyPV6wYEFJTEzMXisAAAB8HW5KlCghe/fuzfT4nj17pHjx4p5qV8i7lmLJ+oOn5Ou4BPNRnwMAgBvLIy5q3769vP7663Lvvfded8yyLHNMz0HOLdmRKGMW7ZLEs5ec+8pERsjLMbXk3jpl/No2AAACXZilycQFBw8elMaNG8vNN98sw4YNMx8dPTZabLxv3z7ZvHmzVKtWTQKZzvbSAmid4ZVRcXQgBJsnP9kq6X8oYf/3cdojjQg4AICQk+TG+7fLPTdVq1aVH374Qfr06SMPPfSQhIX99+1Ws1GtWrVk2bJlAR9sAp0OPWmPTUZpU/fpFdfjd9UqLblzOeIOAADIVrhRTZo0kR07dkhcXJzs37/fBJsaNWpIgwYN3PkyyMTG+NNphqIyCjh6XM9rXjXKp20DAMCW4cZBwwyBxvNOnLvk0fMAAAhFLoebV1991aXzRo8enZP2hLSShSI8eh4AAKHI5YLiXLlySXR0tJQsWdIMR2X4xcLCZOvWrRLIArmgWGtuWr25Qo6dvZRh3Y1W2ZSOjJC1L9xJzQ0AIKQkufH+7fI6Nx06dJBTp05JhQoVZMyYMeZeU9u2bUuzuRts1qxZY1Y21tCkwWjBggVZnr9q1SpzXvrt2LFjYgcaWHS6t0ofXRzP9TjBBgAAD4Sbb7/91kwHb9asmQwfPlzKli0rL7zwQpYL+93I+fPnpX79+jJ16lS3Pk9fU1dDdmzam2QXOs1bp3trD01q+pxp4AAAeHBYKqNel1mzZslXX30ldevWNdPE8+fPn/2GhIVJbGysdO3aNcueG737+J9//ilFihSx3bBU+iEqnRWlxcNaY9O0cjF6bAAAISvJG+vcpHfrrbfKr7/+Krt27TJDUlevXs1RuHGHztS6fPmy1KlTR1555RVp2bJlpufqebqlvjjBQIMM070BAPDisJTD+vXrZcCAAVK6dGl59913pXfv3nL06FGf9IKUKVNG3nvvPdNbpFv58uWlTZs2Wdb6jB071iQ9x6afAwAA7MvlYam33npLZs+eLSdPnpSePXtK3759pV69ep5riAvDUhlp3bq1KXL++OOPXe650YAT6MNSAADAy8NSI0aMMCGie/fuJoho0MnIhAkTxJeaNm0qa9euzfR4eHi42QAAQGhwOdzccccdJtTs3Lkz03Mc95vyJb0VhA5XAQAAuBVudKaSpyUnJ8uBAwecz+Pj401YKVasmOklGjlypCQkJMhHH31kjk+aNEkqV64stWvXlkuXLsmMGTNkxYoVsnTpUn6aAADA/Z6bLl26yH333SfVq1cXT9i8ebOZ2u0wdOhQ81GLlHXYS9ewOXz4sPP4lStXZNiwYSbwFChQwNT86BT01F8DAACENpcLiufMmSMLFy40vSTlypUzIUe3Fi1a+GU4KruCZZ0bAACQvfdvtxfx05lHy5cvl6+//loWLVok165dk06dOpmgc8899/hsrZvsItwAABB8vHJvKQededSxY0d5//33zfo22pujBb2jRo2SqKgo6dy5s/z00085aT8AAIDvb7+QEb33lIYdXUfmb3/7mwQiem4AAAg+Xh2WCnaEGwAAgo/HF/ErWrSoy0XDp0+fdq2VAAAAXuBSuNH1ZQAAAGwTbnTdGQAAAFst4pcRXSVYF9ZLjToWAADgT25PBT9//rw8/fTTUrJkSbnppptMPU7qDQAAIKjCzfPPP2/u5zRt2jSz5o3e32nMmDESHR3tvAcUAABA0AxL6arEGmLatGkjffv2ldtvv12qVasmFStWlLlz50rPnj2901IAAABv9NzoVO8qVao462scU79btWola9ascffLAQAA+DfcaLCJj483j2vWrCnz58939ugUKVLEs60DAADwdrjRoajt27ebxyNGjJCpU6dKRESEDBkyRIYPH+7ulwMAAPCoHN9+4bfffpMtW7aYupt69epJoOP2CwAABB+P334hK1pIrBsAAEBQDksNGjRIpkyZct3+f/7zn/Lss896ql0AAAC+CTdfffWVtGzZ8rr9LVq0kC+//DJ7rQAAAPBXuDl16pQZ80pPx79OnjzpqXYBAAD4Jtxo4fCSJUuu2//dd985178BAADwF7cLiocOHWruLfXHH3/InXfeafYtX75c3nnnHZk0aZI32ggAAOC9cNOvXz+5fPmyvP766/Laa6+ZfZUqVTL3murVq5e7Xw4AACBw1rnR3pv8+fNLwYIFJViwzg0AAMHHZ+vclChRIiefDgAA4HEuhZtGjRqZupqiRYtKw4YNJSwsLNNzt27d6sn2AQAAeD7cdOnSRcLDw52Pswo3AAAAQX1vqWBDzQ0AAPZ+/3Z7nRtdy0YX8kvvzJkzrHMDAAD8zu1w8+uvv8q1a9eu26/Tw3///XdPtQsAACBbXJ4ttXDhQufj77//Ps0tGDTsaMFx5cqVs9cKAAAAX4ebrl27mo9aTNy7d+80x/LmzWsW8tNVigEAAIIi3KSkpJiP2juzadMmKV68uDfbBQAA4JuaGx1+ItgAAABb3RW8bdu28sknn8ilS5e80yoAAABfhRtdgbhevXrm7uClS5eWv//977Jx48bsvj4AAIB/w02DBg1k8uTJcvToUfnwww8lMTFRWrVqJXXq1JEJEyaYm2kCAAAETbhxyJMnj9x///3yxRdfyJtvvikHDhyQ5557TsqXLy+9evUyoQcAACBows3mzZvlqaeekjJlypgeGw02Bw8elGXLlpleHb0HFQAAQMBOBXfQIDNr1izZu3evdOzYUT766CPzMVeuXM6p4rNnzzbr3gAAAAR8uJk2bZr069dP+vTpY3ptMlKyZEmZOXOmJ9oHAADgFu4K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MTExEh0dLWFhYbJgwYIsz//3v/8td911l5QoUUIKFy4szZs3l++//95n7QUAAIHPr+Hm/PnzUr9+fZk6darLYUjDzeLFi2XLli3Stm1bE462bdvm9bYCAIDgEGZZliUBQHtuYmNjpWvXrm59Xu3ateXBBx+U0aNHu3R+UlKSREZGytmzZ03vDwAACHzuvH8Hdc1NSkqKnDt3TooVK+bvpgAAgACRR4LY22+/LcnJydK9e/dMz7l8+bLZUic/AABgX0Hbc/Ppp5/KmDFjZP78+VKyZMlMzxs7dqzpxnJs5cuX92k7AQCAbwVluJk3b57079/fBJv27dtnee7IkSPN+JxjO3LkiM/aCQAAfC/ohqU+++wz6devnwk4nTp1uuH54eHhZgMAAKHBr+FG62UOHDjgfB4fHy9xcXGmQLhChQqm1yUhIUE++ugj51BU7969ZfLkydKsWTM5duyY2Z8/f34z5AQAAODXYanNmzdLw4YNzaaGDh1qHjumdScmJsrhw4ed50+fPl3++usvGThwoJQpU8a5DR482G/fAwAACCwBs86Nr7DODQAAwSdk1rkBAABIj3ADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABsJY+/GwAAyDnLsuSvv/6Sa9eu+bspQLblzZtXcufOLTlFuAGAIHflyhVJTEyUCxcu+LspQI6EhYVJuXLlpGDBgjn6OoQbAAhiKSkpEh8fb/7ajY6Olnz58pk3CCAYex//+OMP+f3336V69eo56sEh3ABAkPfaaMApX768FChQwN/NAXKkRIkS8uuvv8rVq1dzFG4oKAYAG8iVi1/nCH5hHup19Ov/hjVr1khMTIzpStVvaMGCBVmer2PKDz/8sNSoUcP8R3722Wd91lYAABAc/Bpuzp8/L/Xr15epU6e6dP7ly5dNl9VLL71kPg8AAFWpUiWZNGmSBDodctE/5uPi4nL0dV555RVp0KCBx9o1e/ZsKVKkiNiFX8NNhw4d5B//+Id069bN5X+8kydPll69eklkZKTX2wcACCyZvQlv2rRJHn/8cbGjjEY2nnvuOVm+fLnXX3vOnDnSqlUr87hNmzamLePGjbvuvE6dOpljGrpSO3DggPTt29fMgAoPD5fKlStLjx49ZPPmzV5tN4O0AAC5lmLJ+oOn5Ou4BPNRn/u6MDontFc/lAqqdap0VFSU11/n66+/lvvuu8/5XAvXNWCmlpCQYIJWmTJl0uzXANO4cWPZt2+fvP/++7Jr1y6JjY2VmjVryrBhw7zabtuHGx3KSkpKSrMBAP6/JTsSpdWbK6THB/+RwfPizEd9rvu9RXsBnn76aVM7Wbx4cbnnnnvM/gkTJkjdunXlpptuMm+kTz31lCQnJ5tjq1atMr0AZ8+eNb0EqXsK0g9LHT58WLp06WJCQOHChaV79+5y/PjxLNv0wgsvmJpODUlVqlSRUaNGmVk76YeCPv74Y/N6OoLw0EMPyblz55znLFmyxPR0aO+Sho/OnTvLwYMHM536XK1aNXn77bfT7NchK/3etNdDX0fpCIfuczzPaFjqww8/lNq1a5seEg0aen0dsrqumbl06ZIsXbo0TbjR7+fkyZPy008/pendufvuu6VkyZJpvrc+ffqYKd0//vij6dmpWrWqafPLL79sQpM32T7cjB071vwDdGz6QwUA/JcGmCc/2SqJZy+l2X/s7CWz35sBR98UdV0efaN87733zD6dLDJlyhTZuXOnOb5ixQp5/vnnzbEWLVqYAKNhRSeY6KbDM+np1HgNNqdPn5bVq1fLsmXL5NChQ/Lggw9m2Z5ChQqZXgntYdASiA8++EAmTpyY5hwNKjpE9M0335hNv37qYRqtJR06dKjptdDeDP1+NJhom9LTsNKvXz+ZNWtWmv36/I477jDBR4fbHPv0+3U8T2/atGkycOBAMzT3yy+/yMKFC83nO2R1XTOj7S9btqzpaXHQn1fPnj3TtFmvmX4f6QOavpb20GQ0k8/r9T1WgNCmxMbGunx+69atrcGDB9/wvEuXLllnz551bkeOHDGvpY896a9rKda6AyetBdt+Nx/1OQB428WLF61du3aZj+7S31O3vfGDVfGFbzLcKr3wjTnujd9n+ju8YcOGNzzviy++sKKiopzPZ82aZUVGRl53XsWKFa2JEyeax0uXLrVy585tHT582Hl8586d5nf/xo0bXW7j+PHjrcaNGzufv/zyy1aBAgWspKQk577hw4dbzZo1y/Rr/PHHH+Z1f/nlF/M8Pj7ePN+2bZt5npCQYNq6YcMG8/zKlStW8eLFrdmzZ2f5/qhtqV+/vvN5dHS09eKLL7r8vX3hwnUdMGCA9dxzz133vhsXF2cVKlTISk5OtlavXm2VLFnSunr1qmmPtkt9/vnnpt1bt261PPXvWd+3XX3/tv0ifto9p5s36V82YxbtSvOXT5nICHk5ppbcWyftGCQABIqN8aev67FJTd9J9Lie17yq5+s7tB4jvR9++MH0uO/Zs8eUEej9snR4RG8t4WpNze7du00vfeqe+lq1apneAj126623Zvh5n3/+uend0N4ZHbLR19ZeotR0WEh7eBx0+OfEiRPO5/v375fRo0fLhg0bzPCNo8dGh8nq1Klz3WvqUig6ZKNDSk2bNpVFixaZcooHHnhAXKWvf/ToUWnXrl2m5/zg5nXVTKVtmT9//nXHdLayDjd9+eWXsnLlSnn00UclT560ceK/mcx//Dospf94tOvKMSVOlxDXx/qPQI0cOdLMjErNcb5+ri7TrI+1CzEUu3QBICdOnLvk0fPcpfUf6adJa01HvXr15KuvvpItW7Y4lwrJacHxjaxfv94Mt3Ts2NEMN23btk1efPHF615Xb+yYfmgp9ZCTrt2mw2E6pKUBR7cbtb9///4yb948uXjxohnu0eEzd4qj8+fPn+XxX7NxXTdu3GgCkA4FZkSHofRraMBJPySltHZJaZjyB7/23OiYZNu2bZ3PdZxS9e7d24zh6fiiI+g4NGzY0PlYf0CffvqpVKxY0fzwfE1nE2iPTUb5VPfpOot6/K5apSV3Lu71AiCwlCwU4dHzckp/p2tQeOedd5x1Gul7DrTm40Z3Pr/lllvkyJEjZnP03ugfwWfOnDE9OBlZt26deS/RQOPw22+/udX+U6dOyd69e02wuf32282+tWvX3vDzNFBp0NO6GS1I1gVu0weqrL5n7UnSHiWtkUn9nurOdU1PC361RymzWyDogrpa76S9OBldUy0c1v36mhrW0tfd6M/Cm3U3fg03Wi2fVddV+ulmgdDVFUhdugCQE00rFzND6NrTnNFvVv2TrHRkhDnPF7QAVmcnvfvuu6YHJHWhsYO+iWvPvb6R6xur9nCk7+Vo3769mRmkPTFagKw9EDo7qHXr1tKkSZMMX1uHWfSPae1B0WGrb7/91kxbdkfRokXNDKnp06eb4Sr9eiNGjLjh52mA0JlFOlqh7WjevPl137N+vy1btjRlFvo66ensqSeeeMLMWOrQoYOZwaXX75lnnnHpuqanBcmvvvpqlt+rdkCk78lK3aOlvVD6s9Cgp6FRC5P1Z6fDXToLS4uxvcX2s6Xs3KULADmhPcpaG6jS9y07nutxX/U8a1jRKctvvvmmqU+ZO3euqRNJTYdJ9E1cewN0bZu33norwzdW7XnQN2CddaRvsDq1W2tqMqPTnYcMGWKmT2uvg/bk6FRwd2jvhIYj7SnR9uvXGz9+vEuf+9hjj5khIp3qnp72fuiML+2FSj16kZqOeGiQ+9e//mWmg+swlNb/uHpdU9OaI52G7pienxnteUk/tJia1hDpCI2GqwEDBpgeNb3OOovK26tJh2lVsYQQLaTSKeG6TkL6QjF36UJXuh7EjXw24DZ6bgB4hRaFar2irvwaEZG94SMmRfifrgWjBcE6lFaqVCm/tmXChAmmAHnx4sUB9e/Znfdv28+WCqUuXQDIDg0wWhuoQ+ja06w1Nvp7i1pB79OZUTo5RoeVdIaUv4ON0lsl6BBZMGNYykZdugCQXfp7SnuYuzQoaz7ye8s3PvvsM1PIrAW2GQ2x+UP37t2dBdHBinDjgb94pj3SyPTQpKbPdT9dugCAzGghsc6E0jodXQ0YnsGwlAfQpQsAQOAg3Hi4SxcAAPgXw1IAYAMhNvEVNmV56N8x4QYAgphjETW9RxAQ7K783+0gMlsZ2VUMSwFAENM3AV1MzXHzRl2tVxexA4KN3iJCp8Xrv+H0N+J0F+EGAIJc6dKlzcfUd6cGgpGu8lyhQoUcB3TCDQAEOX0j0HsZ6X2F9B5CQLDSG6Omv8lmdhBuAMBGQ1Q5rVUA7ICCYgAAYCuEGwAAYCuEGwAAYCt5QnWBIL11OgAACA6O921XFvoLuXBz7tw587F8+fL+bgoAAMjG+3hkZGSW54RZIbZmty4SdPToUSlUqJDHFrrSNKlh6ciRI1K4cGGPfE1cj+vsO1xr3+Fa+wbXOfivtcYVDTbR0dE3nC4ecj03ekHKlSvnla+tP0T+03gf19l3uNa+w7X2Da5zcF/rG/XYOFBQDAAAbIVwAwAAbIVw4wHh4eHy8ssvm4/wHq6z73CtfYdr7Rtc59C61iFXUAwAAOyNnhsAAGArhBsAAGArhBsAAGArhJscmjp1qlSqVEkiIiKkWbNmsnHjRn83KeiNHTtWbr31VrPQYsmSJaVr166yd+/eNOdcunRJBg4cKFFRUVKwYEH5n//5Hzl+/Ljf2mwH48aNMwtbPvvss859XGfPSUhIkEceecRcy/z580vdunVl8+bNzuNa/jh69GgpU6aMOd6+fXvZv3+/X9scbK5duyajRo2SypUrm2tYtWpVee2119Is1891zp41a9ZITEyMWUBPf08sWLAgzXFXruvp06elZ8+eZu2bIkWKyGOPPSbJycniFVpQjOyZN2+elS9fPuvDDz+0du7caQ0YMMAqUqSIdfz4cX83Lajdc8891qxZs6wdO3ZYcXFxVseOHa0KFSpYycnJznOeeOIJq3z58tby5cutzZs3W7fddpvVokULv7Y7mG3cuNGqVKmSVa9ePWvw4MHO/Vxnzzh9+rRVsWJFq0+fPtaGDRusQ4cOWd9//7114MAB5znjxo2zIiMjrQULFljbt2+37rvvPqty5crWxYsX/dr2YPL6669bUVFR1jfffGPFx8dbX3zxhVWwYEFr8uTJznO4ztmzePFi68UXX7T+/e9/a1K0YmNj0xx35bree++9Vv369a3//Oc/1o8//mhVq1bN6tGjh+UNhJscaNq0qTVw4EDn82vXrlnR0dHW2LFj/douuzlx4oT5z7R69Wrz/MyZM1bevHnNLy6H3bt3m3PWr1/vx5YGp3PnzlnVq1e3li1bZrVu3doZbrjOnvPCCy9YrVq1yvR4SkqKVbp0aWv8+PHOfXr9w8PDrc8++8xHrQx+nTp1svr165dm3/3332/17NnTPOY6e0b6cOPKdd21a5f5vE2bNjnP+e6776ywsDArISHB8jSGpbLpypUrsmXLFtP1lvrWDvp8/fr1fm2b3Zw9e9Z8LFasmPmo1/3q1atprn3NmjWlQoUKXPts0GGnTp06pbmeiuvsOQsXLpQmTZrIAw88YIZaGzZsKB988IHzeHx8vBw7dizNtdZl5nWom2vtuhYtWsjy5ctl37595vn27dtl7dq10qFDB/Oc6+wdrlxX/ahDUfr/wEHP1/fNDRs2eLxNIXdvKU85efKkGd8tVapUmv36fM+ePX5rlx1vdKo1IC1btpQ6deqYffqfKF++fOY/Svprr8fgunnz5snWrVtl06ZN1x3jOnvOoUOHZNq0aTJ06FD53//9X3O9Bw0aZK5v7969ndczo98nXGvXjRgxwty0UUN47ty5ze/o119/3dR5KK6zd7hyXfWjBvvU8uTJY/5o9ca1J9wg4HsVduzYYf76gmfpHXsHDx4sy5YtMwXx8G5I179Y33jjDfNce2703/V7771nwg08Y/78+TJ37lz59NNPpXbt2hIXF2f+ONIiWK5zaGFYKpuKFy9u/jJIP3NEn5cuXdpv7bKTp59+Wr755htZuXJlmju56/XVYcEzZ86kOZ9r7x4ddjpx4oQ0atTI/AWl2+rVq2XKlCnmsf7VxXX2DJ1BUqtWrTT7brnlFjl8+LB57Lie/D7JmeHDh5vem4ceesjMRnv00UdlyJAhZgam4jp7hyvXVT/q75vU/vrrLzODyhvXnnCTTdqd3LhxYzO+m/qvM33evHlzv7Yt2Gm9mgab2NhYWbFihZnWmZpe97x586a59jpVXN8ouPaua9eunfzyyy/mr1vHpr0L2oXveMx19gwdVk2/nIHWhVSsWNE81n/j+gs+9bXW4RWtReBau+7ChQumhiM1/SNUfzcrrrN3uHJd9aP+oaR/VDno73f92Whtjsd5vEQ5xKaCazX47NmzTSX4448/bqaCHzt2zN9NC2pPPvmkmVK4atUqKzEx0blduHAhzRRlnR6+YsUKM0W5efPmZkPOpJ4tpbjOnptqnydPHjNVef/+/dbcuXOtAgUKWJ988kmaqbT6++Prr7+2fv75Z6tLly5MUXZT7969rbJlyzqnguu05eLFi1vPP/+88xyuc/ZnVW7bts1sGh0mTJhgHv/2228uX1edCt6wYUOzHMLatWvNLE2mggeod9991/zy1/VudGq4zt9Hzuh/nIw2XfvGQf/DPPXUU1bRokXNm0S3bt1MAIJnww3X2XMWLVpk1alTx/xBVLNmTWv69Olpjut02lGjRlmlSpUy57Rr187au3ev39objJKSksy/X/2dHBERYVWpUsWszXL58mXnOVzn7Fm5cmWGv5c1ULp6XU+dOmXCjK49VLhwYatv374mNHkDdwUHAAC2Qs0NAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINgJBWqVIlmTRpkr+bAcCDWKEYQMAYNGiQ/PTTT7Jjxw5z12y9gae3/fHHH3LTTTdJgQIFvP5aAHyDnhsAAaVfv37y4IMP+uz1SpQoQbABbIZwA8Ar2rRpI88884w8++yzUrRoUSlVqpR88MEHcv78eenbt68UKlRIqlWrJt99953zc6ZMmSIDBw6UKlWquP16s2fPliJFisg333wjN998swksf/vb3+TChQsyZ84cM/yk7dDeoWvXrmU6LBUWFiYzZsyQbt26ma9RvXp1WbhwofP4n3/+KT179jShKH/+/Ob4rFmzcnStAHgW4QaA12ioKF68uGzcuNEEnSeffFIeeOABadGihWzdulXuvvtuefTRR00A8QT9OhqQ5s2bJ0uWLJFVq1aZkLJ48WKzffzxx/L+++/Ll19+meXXGTNmjHTv3l1+/vln6dixowkzp0+fNsdGjRolu3btMqFs9+7dMm3aNPM9AggchBsAXlO/fn156aWXTO/GyJEjJSIiwgSBAQMGmH2jR4+WU6dOmRDhCVevXjVho2HDhnLHHXeYnpu1a9fKzJkzpVatWtK5c2dp27atrFy5Msuv06dPH+nRo4fpWXrjjTckOTnZBDR1+PBh8/WbNGlien3at28vMTExHmk/AM8g3ADwmnr16jkf586dW6KioqRu3brOfTpUpU6cOOGR19NhpKpVq6b5+hpAChYsmGbfjV4vdbu12Lhw4cLOz9HeJ+0ZatCggTz//POybt06j7QdgOcQbgB4Td68edM813qW1Pv0uUpJSfHJ6zn23ej1svqcDh06yG+//SZDhgyRo0ePSrt27eS5557zSPsBeAbhBgDcpMXEvXv3lk8++cQUI0+fPt3fTQKQSp7UTwDAnw4cOGDqW44dOyYXL150rnOj9TL58uWTQKB1Qo0bN5batWvL5cuXzewsXZMHQOAg3AAIGP3795fVq1c7n2vhroqPjze1M4FAQ5YWR//6669mKvjtt99uanAABA5WKAYAALZCzQ0AALAVwg2AoKCzlHRKd0abrkUDAA4MSwEICgkJCabIOCPFihUzGwAowg0AALAVhqUAAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAIDYyf8D44EUvaWaKhwAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.scatter(m1_mins, np.array(analytical_results_list)/np.array(MC_results_list) , label = 'ratio analytical/MC')\n", + "\n", + "plt.xlabel('m1_mins')\n", + "plt.ylabel('ratio analytical/MC')\n", + "\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "COMPAS", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From ecfbdb893386ab5b30eed0ef7eb04a29d2761b88 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Tue, 30 Dec 2025 11:11:53 +0100 Subject: [PATCH 40/47] debugging --- py_tests/test_fbinary_perM_inclMCMC.ipynb | 79 ++++++++++++++--------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/py_tests/test_fbinary_perM_inclMCMC.ipynb b/py_tests/test_fbinary_perM_inclMCMC.ipynb index a26176f28..2e15a3f6a 100644 --- a/py_tests/test_fbinary_perM_inclMCMC.ipynb +++ b/py_tests/test_fbinary_perM_inclMCMC.ipynb @@ -460,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 70, "metadata": {}, "outputs": [], "source": [ @@ -639,7 +639,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 71, "metadata": {}, "outputs": [ { @@ -700,12 +700,12 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 72, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjoAAAGwCAYAAACgi8/jAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAIEtJREFUeJzt3Q+QVdV9B/DfAgIS2TUEBREQjcZmjYEJLJSmarA0iIZp7dQ4qW2ROExrtiaWxAQ6FZpWg2kMUhMSjCkhbU0kTSrG0DJJiYpRKAhFGzcoREhoLf8SYQEji+x2zp3ujit/VNjd9/a8z2fmzr77h/vOew/2fTnnd+6tamlpaQkAgAz1KHUDAAA6i6ADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbvaLCNTc3xwsvvBD9+/ePqqqqUjcHAHgD0mUA9+3bF0OGDIkePY7db1PxQSeFnGHDhr2R9xQAKDPbtm2LoUOHHnN/xQed1JPT+kZVV1d34UcDAJyoxsbGoqOi9Xv8WCo+6LQOV6WQI+gAQPfyemUnipEBgGxVbNBZsGBB1NbWRl1dXambAgB0kqpKv3t5GuOrqamJvXv3GroCgMy+vyu2RwcAyJ+gAwBkS9ABALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAyJagAwBkS9ABALLVq9QNgJM1Yuaybvcmbr3jqlI3AaAi6NEBALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAyJagAwBkS9ABALIl6AAA2RJ0AIBsZXGvqy1btsSHP/zh2LFjR/Ts2TNWr14db3nLW0rdrG6pO943CgCyDjrXX3993HbbbXHJJZfEL3/5y+jTp0+pmwRZBko3IwW6m24fdJ555pk45ZRTipCTDBgwoNRNAgDKRMlrdFauXBlTpkyJIUOGRFVVVSxduvSIYxYsWBAjRoyIvn37xrhx42LNmjVt+zZt2hSnnXZacY73vOc98ZnPfKaLXwEAUK5KHnQOHDgQI0eOLMLM0SxZsiRmzJgRc+bMifXr1xfHTpo0KXbu3Fnsf+WVV+Kxxx6LL33pS7Fq1ar4wQ9+UCwAACUPOpMnTy7qa66++uqj7p83b15Mnz49pk2bFrW1tbFw4cLo169fLFq0qNh/9tlnx5gxY2LYsGFFbc6VV14ZGzZsOObzHTx4MBobG9stAECeSh50jqepqSnWrVsXEydObNvWo0ePYj313iR1dXVF786LL74Yzc3NxVDYO9/5zmOec+7cuVFTU9O2pIAEAOSprIPO7t274/DhwzFo0KB229P69u3bi8e9evUq6nIuvfTSePe73x0XXHBBfOADHzjmOWfNmhV79+5tW7Zt29bprwMAKI1uP+uqdfgrLW9EGt4y/RwAKkNZ9+gMHDiwuABguhDgq6X1wYMHl6xdAED3UNZBp3fv3jF69OhYsWJF27ZUh5PWx48fX9K2AQDlr+RDV/v374/Nmze3u51DmjWVLvw3fPjwYmr51KlTi5lVY8eOjfnz5xdT0tMsrJORprOnJdUAAQB5qmppaWkpZQMeeeSRmDBhwhHbU7hZvHhx8fiLX/xifO5znysKkEeNGhV33313ceHAjpCml6fZV6kwubq6Oipdd701AV3DLSCAcvFGv79LHnRKTdBpT9DheAQdoLt9f5d1jQ4AwMkQdACAbFVs0EmFyOmWEunKygBAnio26NTX10dDQ0OsXbu21E0BADpJxQYdACB/gg4AkC1BBwDIlqADAGSrYoOOWVcAkL+KDTpmXQFA/io26AAA+RN0AIBsCToAQLYEHQAgW4IOAJCtig06ppcDQP4qNuiYXg4A+avYoAMA5K9XqRuQsxEzl5W6CQBQ0fToAADZEnQAgGwJOgBAttToAFnXnW2946pSNwEooYrt0XEdHQDIX8UGHdfRAYD8VWzQAQDyJ+gAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZKtig44rIwNA/io26LgyMgDkr2KDDgCQP0EHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZKtig46begJA/io26LipJwDkr2KDDgCQP0EHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFsVG3QWLFgQtbW1UVdXV+qmAACdpGKDTn19fTQ0NMTatWtL3RQAoJNUbNABAPIn6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAyJagAwBkS9ABALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJCtXqVuAEBnGjFzWbd7g7fecVWpmwDZ0KMDAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGQriysjjxgxIqqrq6NHjx7x1re+NR5++OFSNwkAKANZBJ3kiSeeiNNOO63UzQAAyoihKwAgWyUPOitXrowpU6bEkCFDoqqqKpYuXXrEMQsWLCiGp/r27Rvjxo2LNWvWtNuf/txll10WdXV1cd9993Vh6wGAclbyoHPgwIEYOXJkEWaOZsmSJTFjxoyYM2dOrF+/vjh20qRJsXPnzrZjfvSjH8W6deviu9/9bnzmM5+Jp59++pjPd/DgwWhsbGy3AAB5KnnQmTx5ctx2221x9dVXH3X/vHnzYvr06TFt2rSora2NhQsXRr9+/WLRokVtx5x99tnFz7POOiuuvPLKIhAdy9y5c6OmpqZtGTZsWCe8KgCgHJQ86BxPU1NT0VMzceLEtm1pZlVaX7VqVVuP0L59+4rH+/fvjx/+8Idx0UUXHfOcs2bNir1797Yt27Zt64JXAgCUQlnPutq9e3ccPnw4Bg0a1G57Wt+4cWPxeMeOHW29QenY1PuTanWOpU+fPsUCAOSvrIPOG3HeeefFU089VepmAABlqKyHrgYOHBg9e/Ysem1eLa0PHjy4ZO0CALqHsg46vXv3jtGjR8eKFSvatjU3Nxfr48ePP6lzp1leqbj5eMNcAED3VvKhq1RAvHnz5rb1LVu2xIYNG2LAgAExfPjwYmr51KlTY8yYMTF27NiYP39+UYCcZmGdjPr6+mJJ08vT7CsAID8lDzpPPvlkTJgwoW09BZskhZvFixfHtddeG7t27YrZs2fH9u3bY9SoUbF8+fIjCpQBAF6rqqWlpSUqWGuPTppqnm4M2pFGzFzWoecDKsPWO64qdRMgm+/vsq7RAQA4GRUbdBQjA0D+KjbopELkhoaGWLt2bambAgB0kooNOgBA/gQdACBbgg4AkC1BBwDIVsUGHbOuACB/FRt0zLoCgPxVbNABAPIn6AAA2RJ0AIBsCToAQLYEHQAgWxUbdEwvB4D8VWzQMb0cAPJXsUEHAMifoAMAZEvQAQCyJegAANkSdACAbJ1Q0Ln88stjz549R2xvbGws9gEAdNug88gjj0RTU9MR219++eV47LHHOqJdAAAnrdebOfjpp59ue9zQ0BDbt29vWz98+HAsX748zj777OguFwxMS2o3AJCnNxV0Ro0aFVVVVcVytCGqU089Nb7whS9Ed7lgYFrScFtNTU2pmwMAlDrobNmyJVpaWuK8886LNWvWxBlnnNG2r3fv3nHmmWdGz549O6OdAACdG3TOOeec4mdzc/ObfyYAgHIOOq+2adOmePjhh2Pnzp1HBJ/Zs2d3RNsAALo+6Nx7771x4403xsCBA2Pw4MFFzU6r9FjQAQC6bdC57bbb4vbbb49PfepTHd8iAIBSXkfnxRdfjGuuuaaj2gAAUD5BJ4Wc73//+x3fGgCAUg9dnX/++XHrrbfG6tWr4+KLL45TTjml3f6PfvSjHdU+AIATVtWSLozzJp177rnHPmFVVTz//PPRna6M/Nxzz8XevXujurq6Q59jxMxlHXo+oDJsveOqUjcByl7rBX9f7/v7hIJOJb5RJ0LQASqFcEa5fn+fUI0OAEC2NTof/vCHj7t/0aJFJ9oeAIDSBp00vfzVDh06FD/+8Y9jz549R73ZJwBAtwk6DzzwwBHb0m0g0tWS3/72t3dEuwAATlqH1ej06NEjZsyYEXfddVdHnRIA4KR0aDHyT3/603jllVc68pQAAF07dJV6bl4tzVD/3//931i2bFlMnTr1xFsDAFDqoPOf//mfRwxbnXHGGfH5z3/+dWdkAQCUddB5+OGHO74lAADlEHRa7dq1K5599tni8YUXXlj06gAAdOti5AMHDhRDVGeddVZceumlxTJkyJC44YYb4qWXXur4VgIAdFXQScXIjz76aDz00EPFRQLT8uCDDxbbPv7xj0d3kG7oWVtbG3V1daVuCgDQSU7opp4DBw6Mb3/72/G+973viNqdD37wg8WQVnfhpp4AJ89NPcnqpp5peGrQoEFHbD/zzDMNXQEAZeOEgs748eNjzpw58fLLL7dt+9WvfhWf/vSni30AAN121tX8+fPjiiuuiKFDh8bIkSOLbU899VT06dMnvv/973d0GwEAui7oXHzxxbFp06a47777YuPGjcW2D33oQ3HdddfFqaeeemItAQAoh6Azd+7cokZn+vTp7bYvWrSoKET+1Kc+1VHtAwDo2hqde+65J37t137tiO0XXXRRLFy48MRbAwBQ6qCzffv24mKBr5WujJxu7gkA0G2DzrBhw+Lxxx8/Ynvalq6QDADQbWt0Um3OzTffHIcOHYrLL7+82LZixYr45Cc/2W2ujAwA5O+Egs4tt9wSv/jFL+IjH/lINDU1Fdv69u1bFCHPmjWro9sIANB1Qaeqqio++9nPxq233ho/+clPiinlF1xwQXEdHQCAbh10Wp122mluigkA5FWMDADQHQg6AEC2BB0AIFuCDgCQrYoNOgsWLIja2lrF1ACQsYoNOvX19dHQ0BBr164tdVMAgE5SsUEHAMifoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQrWyCzksvvRTnnHNOfOITnyh1UwCAMtErMnH77bfHr//6r5e6GQAVacTMZdHdbL3jqlI3gS6QRY/Opk2bYuPGjTF58uRSNwUAKCMlDzorV66MKVOmxJAhQ6KqqiqWLl16xDELFiyIESNGRN++fWPcuHGxZs2advvTcNXcuXO7sNUAQHdQ8qBz4MCBGDlyZBFmjmbJkiUxY8aMmDNnTqxfv744dtKkSbFz585i/4MPPhjveMc7iuWNOHjwYDQ2NrZbAIA8lbxGJw03HW/Iad68eTF9+vSYNm1asb5w4cJYtmxZLFq0KGbOnBmrV6+O+++/P/75n/859u/fH4cOHYrq6uqYPXv2Uc+Xen4+/elPd9rrAQDKR8l7dI6nqakp1q1bFxMnTmzb1qNHj2J91apVbcFl27ZtsXXr1rjzzjuLUHSskJPMmjUr9u7d27akPwsA5KnkPTrHs3v37jh8+HAMGjSo3fa0noqPT0SfPn2KBQDIX1kHnTfr+uuvL3UTAIAyUtZDVwMHDoyePXvGjh072m1P64MHDy5ZuwCA7qGsg07v3r1j9OjRsWLFirZtzc3Nxfr48eNP6txplldtbW3U1dV1QEsBgHJU8qGrNFNq8+bNbetbtmyJDRs2xIABA2L48OHF1PKpU6fGmDFjYuzYsTF//vxiSnrrLKwTVV9fXyxpenlNTU0HvBIAoNyUPOg8+eSTMWHChLb1FGySFG4WL14c1157bezatauYSbV9+/YYNWpULF++/IgCZQCA16pqaWlpiQrW2qOTppqn6+9U+r1fACqFe11Vxvd3WdfoAACcjIoNOoqRASB/FRt0UiFyQ0NDrF27ttRNAQA6ScUGHQAgf4IOAJAtQQcAyJagAwBkq2KDjllXAJC/ig06Zl0BQP4qNugAAPkTdACAbAk6AEC2BB0AIFuCDgCQrYoNOqaXA0D+KjbomF4OAPmr2KADAORP0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbFRt0XEcHAPJXsUHHdXQAIH8VG3QAgPwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZqtig48rIAJC/ig06rowMAPmr2KADAORP0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZqtig46aeAJC/ig06buoJAPnrVeoGAEApjJi5rNu98VvvuKrUTeh2KrZHBwDIn6ADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdACBbFRt0FixYELW1tVFXV1fqpgAAnaRig059fX00NDTE2rVrS90UAKCTVGzQAQDyJ+gAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZEvQAQCyJegAANkSdACAbAk6AEC2BB0AIFuCDgCQLUEHAMiWoAMAZKvbB509e/bEmDFjYtSoUfGud70r7r333lI3CQAoE72im+vfv3+sXLky+vXrFwcOHCjCzu/93u/F2972tlI3DQAosW7fo9OzZ88i5CQHDx6MlpaWYgEAKHnQSb0xU6ZMiSFDhkRVVVUsXbr0iGMWLFgQI0aMiL59+8a4ceNizZo1RwxfjRw5MoYOHRq33HJLDBw4sAtfAQBQrkoedNJwUwopKcwczZIlS2LGjBkxZ86cWL9+fXHspEmTYufOnW3HnH766fHUU0/Fli1b4hvf+Ebs2LGjC18BAFCuSh50Jk+eHLfddltcffXVR90/b968mD59ekybNi1qa2tj4cKFxVDVokWLjjh20KBBRRB67LHHjvl8aXirsbGx3QIA5KnkQed4mpqaYt26dTFx4sS2bT169CjWV61aVayn3pt9+/YVj/fu3VsMhV144YXHPOfcuXOjpqambRk2bFgXvBIAoBTKOujs3r07Dh8+XPTUvFpa3759e/H4Zz/7WVxyySVFT076edNNN8XFF198zHPOmjWrCESty7Zt2zr9dQAApdHtp5ePHTs2NmzY8IaP79OnT7EAAPkr6x6dNHsqTR9/bXFxWh88eHDJ2gUAdA9lHXR69+4do0ePjhUrVrRta25uLtbHjx9/UudOs7xScXNdXV0HtBQAKEclH7rav39/bN68uW09TRFPQ1EDBgyI4cOHF1PLp06dWtzmIQ1TzZ8/v5iSnmZhnYz6+vpiSbOuUlEyAJCfkgedJ598MiZMmNC2noJNksLN4sWL49prr41du3bF7NmziwLkdE+r5cuXH1GgDADwWlUtFX6/hNYenTQDq7q6ukPPPWLmsg49HwB0N1vvuKqk399lXaMDAHAyBB0AIFsVG3TMugKA/FVs0EkzrhoaGmLt2rWlbgoA0EkqNugAAPkTdACAbAk6AEC2BB0AIFsVG3TMugKA/FVs0DHrCgDyV7FBBwDIn6ADAGRL0AEAsiXoAADZEnQAgGz1igqeXp6WV155pVhvbGzs8OdoPvhSh58TALqTxk74fn31eVtaWo57XFXL6x2Ruf/+7/+OYcOGlboZAMAJ2LZtWwwdOvSY+ys+6DQ3N8cLL7wQ/fv3j6qqqujIpJkCVPoAqqurO+y8dByfUfnzGZU/n1H5a8z0+yj10+zbty+GDBkSPXocuxKnYoeuWqU353hJ8GSlv1Q5/cXKkc+o/PmMyp/PqPxVZ/h9VFNT87rHKEYGALIl6AAA2RJ0OkmfPn1izpw5xU/Kk8+o/PmMyp/PqPz1qfDvo4ovRgYA8qVHBwDIlqADAGRL0AEAsiXoAADZEnQ62datW+OGG26Ic889N0499dR4+9vfXlS/NzU1dfZT8ybcfvvt8Ru/8RvRr1+/OP300713ZSDdi27EiBHRt2/fGDduXKxZs6bUTeJVVq5cGVOmTCmuSpuuKr906VLvTxmZO3du1NXVFVf9P/PMM+N3f/d349lnn41KJOh0so0bNxa3mbjnnnvimWeeibvuuisWLlwYf/EXf9HZT82bkILnNddcEzfeeKP3rQwsWbIkZsyYUfynYP369TFy5MiYNGlS7Ny5s9RN4/8dOHCg+FxSIKX8PProo1FfXx+rV6+OH/zgB3Ho0KF4//vfX3xulcb08hL43Oc+F1/+8pfj+eefL8XTcxyLFy+Om2++Ofbs2eN9KqHUg5P+N/rFL36xWE//WUj36rnpppti5syZPpsyk3p0HnjggaLXgPK0a9euomcnBaBLL700KokenRLYu3dvDBgwoBRPDd2id23dunUxceLEdvekS+urVq0qadugO3/vJJX43SPodLHNmzfHF77whfiTP/mTrn5q6BZ2794dhw8fjkGDBrXbnta3b99esnZBd9Xc3Fz0VL/3ve+Nd73rXVFpBJ0TlLrPU3ft8ZZUn/Nq//M//xNXXHFFUQsyffr0jvj86ODPCCA39fX18eMf/zjuv//+qES9St2A7urjH/94XH/99cc95rzzzmt7/MILL8SECROKmT1f+cpXuqCFvNnPiPIwcODA6NmzZ+zYsaPd9rQ+ePDgkrULuqM/+7M/i+9973vFLLmhQ4dGJRJ0TtAZZ5xRLG9E6slJIWf06NHxta99rag3oLw+I8pH7969i38rK1asaCtuTV3vaT390gZeX0tLS1G8/8ADD8QjjzxSXOKkUgk6nSyFnPe9731xzjnnxJ133llUvrfyv9Py8fOf/zx++ctfFj9TfciGDRuK7eeff36cdtpppW5exUlTy6dOnRpjxoyJsWPHxvz584tpsdOmTSt10/h/+/fvL2oOW23ZsqX4d5OKXYcPH+59KoPhqm984xvx4IMPFtfSaa1vq6mpKa7pVklML++C6crH+uWcEjflIQ1xff3rXz9i+8MPP1wEVbpemlqeLsWQfkGPGjUq7r777mLaOeUh9RKknurXSgE1/d6jtFIN4tF87Wtfe90h/dwIOgBAthSLAADZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAB0iXUH65ptv9m4CZUXQAQCyJegAANkSdIBO8eKLL8Yf//Efx1vf+tbo169fTJ48OTZt2tTumHvvvTeGDRtW7L/66qtj3rx5cfrppx/znFu3bi1uVvitb30rLrnkkuIuzHV1dfHcc8/F2rVri7udp7vNp+fatWtX259L+377t387Bg4cWNy9+bLLLov169e3u8HuX/3VXxV33e7Tp08MGTIkPvrRj7bt/9KXvhQXXHBB9O3bNwYNGhS///u/37avubk55s6dG+eee27RnpEjR8a3v/3tdu/DddddF2eccUaxP50n3VgR6BqCDtAp0h2Sn3zyyfjud78bq1atKsLElVdeGYcOHSr2P/744/Gnf/qn8bGPfSw2bNhQBJHbb7/9DZ17zpw58Zd/+ZdFWOnVq1f8wR/8QXzyk5+Mv/u7v4vHHnssNm/eHLNnz247ft++fcVdtX/0ox/F6tWri7CR2pK2J9/5znfirrvuinvuuacIY0uXLo2LL7642JdeQwo9f/3Xfx3PPvtsLF++PC699NK2c6eQ8w//8A+xcOHCeOaZZ+LP//zP4w//8A/j0UcfLfbfeuut0dDQEP/2b/8WP/nJT+LLX/5yEbiALtIC0AEuu+yylo997GPF4+eee64l/Xp5/PHH2/bv3r275dRTT2351re+Vaxfe+21LVdddVW7c1x33XUtNTU1x3yOLVu2FOf96le/2rbtm9/8ZrFtxYoVbdvmzp3bcuGFFx7zPIcPH27p379/y0MPPVSsf/7zn295xzve0dLU1HTEsd/5zndaqqurWxobG4/Y9/LLL7f069ev5Yknnmi3/YYbbmj50Ic+VDyeMmVKy7Rp047ZFqBz6dEBOlzquUg9LePGjWvb9ra3vS0uvPDCYl+SekfGjh3b7s+9dv1Y3v3ud7c9TkNJSWsPTOu2nTt3tq3v2LEjpk+fXvTkpKGr6urq2L9/f/z85z8v9l9zzTXxq1/9Ks4777ziuAceeCBeeeWVYl/qaTrnnHOKfX/0R38U9913X7z00kvFvtRzlB6nY9KQWeuSenh++tOfFsfceOONcf/998eoUaOKXqcnnnjiBN5R4EQJOkC3c8opp7Q9TjU7R9uWamdapWGrNDyWhrZS0EiPU/Bqamoq9qc6oRS8Ui1OqqP5yEc+UgxPpWG2/v37F0Nk3/zmN+Oss84qhsRSHc6ePXuKsJQsW7asOGfrkoaqWut0Ur3Qz372s2JI64UXXojf+q3fik984hNd9l5BpRN0gA73zne+s+gR+Y//+I+2bb/4xS+KMFFbW1usp96dVCT8aq9d7yipHijV2aS6nIsuuqgoON69e3e7Y1LAmTJlStx9993xyCOPFHVF//Vf/1XsS71TEydOjL/927+Np59+uiiK/uEPf1i8lnSu1DN0/vnnt1tSeGqVCpFT2Pqnf/qnmD9/fnzlK1/plNcJHKnXUbYBnJQ0RPQ7v/M7xTBQKvBNvSIzZ86Ms88+u9ie3HTTTUWvSZpplQJGCg6pYLe1h6aj2/OP//iPxaysxsbGuOWWW4pg02rx4sVx+PDhYqgtzQBLgSTtT0NW3/ve9+L5558v2ppmkP3rv/5r0VuUglp6Xal3JvXWpG2/+Zu/GXv37i2CVRoeS+Em9QCNHj26CFgHDx4szpeCINA19OgAnSJNoU5f8B/4wAdi/PjxxayrFBJah5je+973FjOVUtBJQ0FpNlMKDGkKd0f7+7//+2Ka93ve856izib17px55plt+9OU9jTVPbUp1f/8+7//ezz00EPF8Fba9y//8i9x+eWXFwEltTkNY6XgkvzN3/xNMbMqzb5K+6+44opiKCtNN0969+4ds2bNKs6bwlLPnj2Lmh2ga1SliuQuei6A40o9QBs3biymiAN0BENXQMnceeedxYylt7zlLcWw1de//vWiIBigo+jRAUrmgx/8YFH4my7cl6Zvp7qddBFBgI4i6AAA2VKMDABkS9ABALIl6AAA2RJ0AIBsCToAQLYEHQAgW4IOAJAtQQcAiFz9HxqO8xe+BFYOAAAAAElFTkSuQmCC", + "image/png": "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il4DytOePXuKnp0UgC699NKoJHp0SqCxsTEGDBhQiqeGbtG7tmHDhpg4ceJR96RL66tXry5p26A7f+8klfjdI+h0sa1bt8aXvvSl+IM/+IOufmroFvbu3RtHjhyJQYMGHbU9re/cubNk7YLuqqWlpeipfs973hPvfOc7o9IIOicpdZ+n7tpXW1J9zsv9z//8T1xxxRVFLciMGTM64vOjgz8jgNzU19fHk08+Gffee29Uol6lbkB39alPfSquu+66Vz3mggsuaH/83HPPxYQJE4qZPV/96le7oIW80c+I8jBw4MDo2bNn7Nq166jtaX3w4MElaxd0R3/0R38U3/ve94pZckOHDo1KJOicpLPOOqtYXo/Uk5NCzujRo+NrX/taUW9AeX1GlI/evXsX/1ZWrlzZXtyaut7TevqlDby21tbWonj/vvvui4ceeqi4xEmlEnQ6WQo573//++O8886LO+64o6h8b+N/p+XjZz/7WfziF78o/kz1IZs2bSq2X3jhhXHGGWeUunkVJ00tnzZtWowZMybGjh0bCxYsKKbFTp8+vdRN4/8dOHCgqDlss23btuLfTSp2HT58uPepDIarvvGNb8T9999fXEunrb6tpqamuKZbJTG9vAumK5/ol3NK3JSHNMT19a9//ZjtDz74YBFU6Xppanm6FEP6BT1q1Kj44he/WEw7pzykXoLUU/1KKaCm33uUVqpBPJ6vfe1rrzmknxtBBwDIlmIRACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdoEOkK0jfdNNN3k2grAg6AEC2BB0AIFuCDtApnn/++fjoRz8ab37zm6Nfv34xefLk2LJly1HH3H333TFs2LBi/9VXXx3z58+PM88884Tn3L59e3Gzwm9961vxvve9r7gLc11dXTzzzDOxbt264m7n6W7z6bn27NnT/nNp3wc+8IEYOHBgcffmyy67LDZu3HjUDXb/7M/+rLjrdp8+fWLIkCHxiU98on3/l7/85Xjb294Wffv2jUGDBsVv//Zvt+9raWmJefPmxfnnn1+0Z+TIkfHtb3/7qPfh2muvjbPOOqvYn86TbqwIdA1BB+gU6Q7J69evj+9+97uxevXqIkxceeWVcfjw4WL/o48+Gn/4h38Yn/zkJ2PTpk1FELntttte17nnzp0bf/qnf1qElV69esXv/M7vxGc+85n4whe+EI888khs3bo15syZ0378/v37i7tq/+hHP4o1a9YUYSO1JW1PvvOd78Sdd94Zd911VxHGli1bFpdcckmxL72GFHr+4i/+Ip5++ulYsWJFXHrppe3nTiHnH/7hH2LRokXx1FNPxR//8R/H7/7u78bDDz9c7L/llluioaEh/u3f/i1+/OMfx1e+8pUicAFdpBWgA1x22WWtn/zkJ4vHzzzzTGv69fLoo4+279+7d2/r6aef3vqtb32rWJ86dWrrVVddddQ5rr322taampoTPse2bduK8/7d3/1d+7ZvfvObxbaVK1e2b5s3b17rRRdddMLzHDlypLV///6tDzzwQLH++c9/vvXtb397a3Nz8zHHfuc732mtrq5ubWpqOmbfiy++2NqvX7/Wxx577Kjt119/fetHPvKR4vGUKVNap0+ffsK2AJ1Ljw7Q4VLPReppGTduXPu2t7zlLXHRRRcV+5LUOzJ27Nijfu6V6yfyq7/6q+2P01BS0tYD07Zt9+7d7eu7du2KGTNmFD05aeiquro6Dhw4ED/72c+K/ddcc0388pe/jAsuuKA47r777ouXXnqp2Jd6ms4777xi3+/93u/FPffcEy+88EKxL/UcpcfpmDRk1rakHp6f/OQnxTE33HBD3HvvvTFq1Kii1+mxxx47iXcUOFmCDtDtnHbaae2PU83O8bal2pk2adgqDY+loa0UNNLjFLyam5uL/alOKAWvVIuT6mg+/vGPF8NTaZitf//+xRDZN7/5zTjnnHOKIbFUh7Nv374iLCXLly8vztm2pKGqtjqdVC/005/+tBjSeu655+LXf/3X49Of/nSXvVdQ6QQdoMO94x3vKHpE/uM//qN9289//vMiTNTW1hbrqXcnFQm/3CvXO0qqB0p1Nqku5+KLLy4Kjvfu3XvUMSngTJkyJb74xS/GQw89VNQV/dd//VexL/VOTZw4Mf76r/86nnjiiaIo+oc//GHxWtK5Us/QhRdeeNSSwlObVIicwtY//dM/xYIFC+KrX/1qp7xO4Fi9jrMN4JSkIaLf+I3fKIaBUoFv6hWZNWtWnHvuucX25MYbbyx6TdJMqxQwUnBIBbttPTQd3Z5//Md/LGZlNTU1xc0331wEmzZLliyJI0eOFENtaQZYCiRpfxqy+t73vhfPPvts0dY0g+xf//Vfi96iFNTS60q9M6m3Jm1773vfG42NjUWwSsNjKdykHqDRo0cXAevQoUPF+VIQBLqGHh2gU6Qp1OkL/kMf+lCMHz++mHWVQkLbENN73vOeYqZSCjppKCjNZkqBIU3h7mh///d/X0zzfte73lXU2aTenbPPPrt9f5rSnqa6pzal+p9///d/jwceeKAY3kr7/uVf/iUuv/zyIqCkNqdhrBRckr/8y78sZlal2Vdp/xVXXFEMZaXp5knv3r1j9uzZxXlTWOrZs2dRswN0japUkdxFzwXwqlIP0ObNm4sp4gAdwdAVUDJ33HFHMWPpTW96UzFs9fWvf70oCAboKHp0gJL58Ic/XBT+pgv3penbqW4nXUQQoKMIOgBAthQjAwDZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAIlf/BzSk9I15JYCOAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -806,7 +806,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 73, "metadata": {}, "outputs": [ { @@ -861,7 +861,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -973,7 +973,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 89, "metadata": {}, "outputs": [ { @@ -982,8 +982,8 @@ "text": [ " old analytical m_rep =253.53899506390485, new analytical m_rep = 253.53899506390468\n", "using constant binaryFractions = 0.7\n", - "MC_results m_rep = 252.58192718551336\n", - "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.002969911830251223\n" + "MC_results m_rep = 252.73313493239846\n", + "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.0029629506909933496\n" ] } ], @@ -1004,7 +1004,7 @@ " new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\" )\n", "\n", "# MCMC simulation\n", - "MC_results = create_mock_universe( n_primary=int(1e7), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", + "MC_results = create_mock_universe( n_primary=int(5e6), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", "\n", "print('MC_results m_rep = ', MC_results['total_mass_universe']/MC_results['n_compas'] )\n", "\n", @@ -1030,23 +1030,21 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 90, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - " new analytical m_rep = 42.89068714952707\n", + " new analytical m_rep = 94.10865010969138\n", "using mass-dependent binaryFractions from Offner et al. (2023)\n", - "MC_results m_rep = 78.22524271488052\n", - "fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.02715404524930118, \n", - " MC nbin_compas/nbin: 0.027145053029190222\n" + "MC_results m_rep = 171.7809993492964\n" ] } ], "source": [ - "m1_min = 5\n", + "m1_min = 10\n", "m1_max = 150\n", "m2_min = 0.1\n", "fbin = None\n", @@ -1057,18 +1055,44 @@ "print(f\" new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\" )\n", "\n", "# MCMC simulation\n", - "MC_results = create_mock_universe( n_primary=int(1e7), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", + "MC_results = create_mock_universe( n_primary=int(5e6), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", "\n", "print('MC_results m_rep = ', MC_results['total_mass_universe']/MC_results['n_compas'] )\n", - "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.012375649403900452, \n", + " MC nbin_compas/nbin: 0.012344955038427191\n", + "1/fbin(m) * Mstellar_sys_univ/Nstellar_sys_univ = 1.1646556596318784\n", + " total_mass_universe/n_primary = 0.5062729612822464\n" + ] + } + ], + "source": [ + "# Sanity checks\n", "# fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates\n", - "print(f\"fint = N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']}, \\n \\\n", - " MC nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )" + "print(f\"\\n fint = N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']}, \\n \\\n", + " MC nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )\n", + "\n", + "\n", + "# yellow and blue terms:\n", + "print(f\"1/fbin(m) * Mstellar_sys_univ/Nstellar_sys_univ = {new_analytical['Average_mass_stellar_sys_per_fbin']}\")\n", + "print(f\" total_mass_universe/n_primary = {MC_results['total_mass_universe']/MC_results['n_primary']}\")\n" ] }, { "cell_type": "code", - "execution_count": 59, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1077,7 +1101,7 @@ "dict_keys(['n_primary', 'n_binaries', 'n_compas', 'total_mass_universe', 'total_mass_singles', 'total_mass_binaries', 'primary_masses', 'secondary_masses', 'binary_mask', 'compas_mask', 'binary_systems', 'compas_systems', 'fb_for_every_m1'])" ] }, - "execution_count": 59, + "execution_count": 77, "metadata": {}, "output_type": "execute_result" } @@ -1088,7 +1112,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 78, "metadata": {}, "outputs": [ { @@ -1097,7 +1121,7 @@ "dict_keys(['M_sf_Univ_per_N_binary_COMPAS', 'fint', 'Average_mass_stellar_sys_per_fbin', 'piece_wise_integral', 'alpha', 'binaryFractions', 'binary_bin_edges', 'imf_mass_bounds'])" ] }, - "execution_count": 60, + "execution_count": 78, "metadata": {}, "output_type": "execute_result" } @@ -1130,13 +1154,6 @@ "outputs": [], "source": [] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": 12, From fe82a30b779d31e87cafa5dcd53cd6b5c94c3a8f Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Tue, 30 Dec 2025 19:22:45 +0100 Subject: [PATCH 41/47] debugging --- py_tests/test_fbinary_perM_inclMCMC.ipynb | 82 ++++++++++------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/py_tests/test_fbinary_perM_inclMCMC.ipynb b/py_tests/test_fbinary_perM_inclMCMC.ipynb index 2e15a3f6a..8168c376b 100644 --- a/py_tests/test_fbinary_perM_inclMCMC.ipynb +++ b/py_tests/test_fbinary_perM_inclMCMC.ipynb @@ -460,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 179, "metadata": {}, "outputs": [], "source": [ @@ -576,7 +576,6 @@ "\n", " fint = N_compas / N_univ\n", "\n", - "\n", " ##################\n", " # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", " # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction \n", @@ -586,15 +585,12 @@ " # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. \n", " # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins.\n", " piece_wise_integral = 0\n", + " average_mass_stellar_sys_int = 0\n", "\n", " # For every binary fraction bin\n", " for i in range(len(binary_bin_edges) - 1):\n", - " fbin = binaryFractions[i] # Binary fraction for this range\n", - "\n", " # And every piece of the Kroupa IMF\n", " for j in range(len(imf_mass_bounds) - 1):\n", - " exponent = IMF_powers[j] # IMF exponent for these masses\n", - "\n", " # Check if the binary fraction bin overlaps with the IMF mass bin\n", " if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]:\n", " continue # No overlap\n", @@ -604,15 +600,17 @@ " m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1])\n", "\n", " # Compute the definite integral:\n", - " integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j]\n", + " integral = (m_end**(IMF_powers[j] + 2) - m_start**(IMF_powers[j] + 2) ) / (IMF_powers[j] + 2) * continuity_constants[j]\n", "\n", " # Compute the sum term\n", - " sum_term = (1 /fbin + 0.5) * integral\n", - " piece_wise_integral += sum_term\n", + " piece_wise_integral += (1 /binaryFractions[i] + 0.5) * integral\n", + " average_mass_stellar_sys_int += (1 + 0.5 * binaryFractions[i]) * integral \n", "\n", " # combining them:\n", " Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral\n", "\n", + " average_mass_stellar_sys = alpha * average_mass_stellar_sys_int\n", + "\n", " # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", " M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin\n", "\n", @@ -621,7 +619,7 @@ " 'M_sf_Univ_per_N_binary_COMPAS': M_sf_Univ_per_N_binary_COMPAS, \n", " 'fint': fint,\n", " 'Average_mass_stellar_sys_per_fbin': Average_mass_stellar_sys_per_fbin,\n", - " 'piece_wise_integral': piece_wise_integral,\n", + " 'average_mass_stellar_sys': average_mass_stellar_sys ,\n", " 'alpha': alpha,\n", " 'binaryFractions': binaryFractions,\n", " 'binary_bin_edges': binary_bin_edges,\n", @@ -639,7 +637,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 180, "metadata": {}, "outputs": [ { @@ -700,12 +698,12 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 181, "metadata": {}, "outputs": [ { "data": { - "image/png": "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il4DytOePXuKnp0UgC699NKoJHp0SqCxsTEGDBhQiqeGbtG7tmHDhpg4ceJR96RL66tXry5p26A7f+8klfjdI+h0sa1bt8aXvvSl+IM/+IOufmroFvbu3RtHjhyJQYMGHbU9re/cubNk7YLuqqWlpeipfs973hPvfOc7o9IIOicpdZ+n7tpXW1J9zsv9z//8T1xxxRVFLciMGTM64vOjgz8jgNzU19fHk08+Gffee29Uol6lbkB39alPfSquu+66Vz3mggsuaH/83HPPxYQJE4qZPV/96le7oIW80c+I8jBw4MDo2bNn7Nq166jtaX3w4MElaxd0R3/0R38U3/ve94pZckOHDo1KJOicpLPOOqtYXo/Uk5NCzujRo+NrX/taUW9AeX1GlI/evXsX/1ZWrlzZXtyaut7TevqlDby21tbWonj/vvvui4ceeqi4xEmlEnQ6WQo573//++O8886LO+64o6h8b+N/p+XjZz/7WfziF78o/kz1IZs2bSq2X3jhhXHGGWeUunkVJ00tnzZtWowZMybGjh0bCxYsKKbFTp8+vdRN4/8dOHCgqDlss23btuLfTSp2HT58uPepDIarvvGNb8T9999fXEunrb6tpqamuKZbJTG9vAumK5/ol3NK3JSHNMT19a9//ZjtDz74YBFU6Xppanm6FEP6BT1q1Kj44he/WEw7pzykXoLUU/1KKaCm33uUVqpBPJ6vfe1rrzmknxtBBwDIlmIRACBbgg4AkC1BBwDIlqADAGRL0AEAsiXoAADZEnQAgGwJOgBAtgQdoEOkK0jfdNNN3k2grAg6AEC2BB0AIFuCDtApnn/++fjoRz8ab37zm6Nfv34xefLk2LJly1HH3H333TFs2LBi/9VXXx3z58+PM88884Tn3L59e3Gzwm9961vxvve9r7gLc11dXTzzzDOxbt264m7n6W7z6bn27NnT/nNp3wc+8IEYOHBgcffmyy67LDZu3HjUDXb/7M/+rLjrdp8+fWLIkCHxiU98on3/l7/85Xjb294Wffv2jUGDBsVv//Zvt+9raWmJefPmxfnnn1+0Z+TIkfHtb3/7qPfh2muvjbPOOqvYn86TbqwIdA1BB+gU6Q7J69evj+9+97uxevXqIkxceeWVcfjw4WL/o48+Gn/4h38Yn/zkJ2PTpk1FELntttte17nnzp0bf/qnf1qElV69esXv/M7vxGc+85n4whe+EI888khs3bo15syZ0378/v37i7tq/+hHP4o1a9YUYSO1JW1PvvOd78Sdd94Zd911VxHGli1bFpdcckmxL72GFHr+4i/+Ip5++ulYsWJFXHrppe3nTiHnH/7hH2LRokXx1FNPxR//8R/H7/7u78bDDz9c7L/llluioaEh/u3f/i1+/OMfx1e+8pUicAFdpBWgA1x22WWtn/zkJ4vHzzzzTGv69fLoo4+279+7d2/r6aef3vqtb32rWJ86dWrrVVddddQ5rr322taampoTPse2bduK8/7d3/1d+7ZvfvObxbaVK1e2b5s3b17rRRdddMLzHDlypLV///6tDzzwQLH++c9/vvXtb397a3Nz8zHHfuc732mtrq5ubWpqOmbfiy++2NqvX7/Wxx577Kjt119/fetHPvKR4vGUKVNap0+ffsK2AJ1Ljw7Q4VLPReppGTduXPu2t7zlLXHRRRcV+5LUOzJ27Nijfu6V6yfyq7/6q+2P01BS0tYD07Zt9+7d7eu7du2KGTNmFD05aeiquro6Dhw4ED/72c+K/ddcc0388pe/jAsuuKA47r777ouXXnqp2Jd6ms4777xi3+/93u/FPffcEy+88EKxL/UcpcfpmDRk1rakHp6f/OQnxTE33HBD3HvvvTFq1Kii1+mxxx47iXcUOFmCDtDtnHbaae2PU83O8bal2pk2adgqDY+loa0UNNLjFLyam5uL/alOKAWvVIuT6mg+/vGPF8NTaZitf//+xRDZN7/5zTjnnHOKIbFUh7Nv374iLCXLly8vztm2pKGqtjqdVC/005/+tBjSeu655+LXf/3X49Of/nSXvVdQ6QQdoMO94x3vKHpE/uM//qN9289//vMiTNTW1hbrqXcnFQm/3CvXO0qqB0p1Nqku5+KLLy4Kjvfu3XvUMSngTJkyJb74xS/GQw89VNQV/dd//VexL/VOTZw4Mf76r/86nnjiiaIo+oc//GHxWtK5Us/QhRdeeNSSwlObVIicwtY//dM/xYIFC+KrX/1qp7xO4Fi9jrMN4JSkIaLf+I3fKIaBUoFv6hWZNWtWnHvuucX25MYbbyx6TdJMqxQwUnBIBbttPTQd3Z5//Md/LGZlNTU1xc0331wEmzZLliyJI0eOFENtaQZYCiRpfxqy+t73vhfPPvts0dY0g+xf//Vfi96iFNTS60q9M6m3Jm1773vfG42NjUWwSsNjKdykHqDRo0cXAevQoUPF+VIQBLqGHh2gU6Qp1OkL/kMf+lCMHz++mHWVQkLbENN73vOeYqZSCjppKCjNZkqBIU3h7mh///d/X0zzfte73lXU2aTenbPPPrt9f5rSnqa6pzal+p9///d/jwceeKAY3kr7/uVf/iUuv/zyIqCkNqdhrBRckr/8y78sZlal2Vdp/xVXXFEMZaXp5knv3r1j9uzZxXlTWOrZs2dRswN0japUkdxFzwXwqlIP0ObNm4sp4gAdwdAVUDJ33HFHMWPpTW96UzFs9fWvf70oCAboKHp0gJL58Ic/XBT+pgv3penbqW4nXUQQoKMIOgBAthQjAwDZEnQAgGwJOgBAtgQdACBbgg4AkC1BBwDIlqADAGRL0AEAIlf/BzSk9I15JYCOAAAAAElFTkSuQmCC", + "image/png": "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whe+0JntAwDonqCzffv2aGtriwsuuCDWr18fZ511Vse+vn37xtlnnx29e/c+9dYAAJQq6Jx33nnFz9bW1s5sAwBA6YPOa23dujUeffTR2LNnz3HBZ968eZ3RNgCA7g869913X9x0000xePDgGDp0aFGz0y49FnQAgB4bdG6//fa444474tOf/nTntwgAoJTX0XnppZfiuuuu66w2AACUT9BJIefb3/5257cGAKDUQ1cXXnhh3HbbbbFu3bq49NJL47TTTjtm/8c+9rHoCVdGTku60CEAkKeqtnRhnF/Q+eeff/ITVlXFCy+8ED1F+5WRm5qaorq6ulPPPWrOyk49H0C52nHnNaVuAhWm+U1+f59Sj066cCAAQJY1OgAAPcEp9eh85CMfecP9S5cuPdX2AACUNuik6eWvdeTIkXj22Wdj//79J7zZJwBAjwk6Dz300HHb0m0g0tWS3/nOd3ZGuwAAyqdGp1evXjF79uy4++67O+uUAADlU4z8wx/+MF599dXOPCUAQPcOXaWem9dKl+L53//931i5cmXMmDHj1FsDAFDqoPNf//Vfxw1bnXXWWfG5z33u587IAgAo66Dz6KOPdn5LAADKIei027t3bzz//PPF44svvrjo1QEA6NHFyIcOHSqGqM4555y4/PLLi2XYsGFx4403xssvvxw9QbqhZ21tbdTV1ZW6KQBAOQWdVIz8+OOPxyOPPFJcJDAtDz/8cLHtE5/4RPQE9fX10djYGA0NDaVuCgBQTkNXDz74YHzjG9+ID3zgAx3brr766jj99NPjQx/6UNxzzz2d2UYAgO7r0UnDU0OGDDlu+9lnn91jhq4AgPydUtCZOHFizJ8/P1555ZWObT/72c/iM5/5TLEPAKDHDl0tWrQorrrqqhg+fHiMHj262Pb0009Hv3794tvf/nZntxEAoPuCzqWXXhpbt26N+++/P7Zs2VJs+/CHPxzXX399UacDANBjg86CBQuKGp1Zs2Yds33p0qXFtXU+/elPd1b7AAC6t0bn3nvvjV/6pV86bvsll1wSS5YsOfXWAACUOujs2rWruFjg66UrI6ebewIA9NigM2LEiHjyySeP2562pSskAwD02BqdVJtzyy23xJEjR+LKK68stq1evTo+9alP9ZgrIwMA+TuloHPrrbfGT37yk/joRz8aLS0txbb+/fsXRchz587t7DYC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", "text/plain": [ "
" ] @@ -806,7 +804,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 182, "metadata": {}, "outputs": [ { @@ -818,6 +816,7 @@ } ], "source": [ + "\n", "def get_binary_fraction(m1, binaryFractions=None, binary_bin_edges=None):\n", " \"\"\"\n", " Get binary fraction for a given primary mass.\n", @@ -844,8 +843,7 @@ " \n", " # Map masses to bins\n", " m1_arr = np.asarray(m1)\n", - " bin_index = np.digitize(m1_arr, binary_bin_edges) - 1\n", - " bin_index = np.clip(bin_index, 0, len(binaryFractions) - 1)\n", + " bin_index = np.digitize(m1_arr, binary_bin_edges, right=True) - 1\n", " \n", " fb_array = np.array(binaryFractions, dtype=float)[bin_index]\n", " if m1_arr.ndim == 0:\n", @@ -861,7 +859,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 183, "metadata": {}, "outputs": [], "source": [ @@ -973,7 +971,7 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": 184, "metadata": {}, "outputs": [ { @@ -982,8 +980,8 @@ "text": [ " old analytical m_rep =253.53899506390485, new analytical m_rep = 253.53899506390468\n", "using constant binaryFractions = 0.7\n", - "MC_results m_rep = 252.73313493239846\n", - "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.0029629506909933496\n" + "MC_results m_rep = 257.0996760119897\n", + "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.0029163857961781803\n" ] } ], @@ -1030,7 +1028,7 @@ }, { "cell_type": "code", - "execution_count": 90, + "execution_count": 185, "metadata": {}, "outputs": [ { @@ -1039,7 +1037,7 @@ "text": [ " new analytical m_rep = 94.10865010969138\n", "using mass-dependent binaryFractions from Offner et al. (2023)\n", - "MC_results m_rep = 171.7809993492964\n" + "MC_results m_rep = 173.0107343824034\n" ] } ], @@ -1063,36 +1061,36 @@ }, { "cell_type": "code", - "execution_count": 91, + "execution_count": 186, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Analytic: N_binaries_in_COMPAS/N_binaries_in_universe: 0.012375649403900452, \n", + " MC: nbin_compas/nbin: 0.012262259119244086\n", "\n", - " fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.012375649403900452, \n", - " MC nbin_compas/nbin: 0.012344955038427191\n", - "1/fbin(m) * Mstellar_sys_univ/Nstellar_sys_univ = 1.1646556596318784\n", - " total_mass_universe/n_primary = 0.5062729612822464\n" + " Analytic: Mstellar_sys_univ/Nstellar_sys_univ = 0.5083080461257276\n", + " MC: total_mass_universe/n_primary = 0.5067484410060595\n" ] } ], "source": [ "# Sanity checks\n", "# fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates\n", - "print(f\"\\n fint = N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']}, \\n \\\n", - " MC nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )\n", - "\n", + "print(f\"Analytic: N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']}, \\n \\\n", + "MC: nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )\n", "\n", + "print()\n", "# yellow and blue terms:\n", - "print(f\"1/fbin(m) * Mstellar_sys_univ/Nstellar_sys_univ = {new_analytical['Average_mass_stellar_sys_per_fbin']}\")\n", - "print(f\" total_mass_universe/n_primary = {MC_results['total_mass_universe']/MC_results['n_primary']}\")\n" + "print(f\" Analytic: Mstellar_sys_univ/Nstellar_sys_univ = {new_analytical['average_mass_stellar_sys']}\")\n", + "print(f\" MC: total_mass_universe/n_primary = {MC_results['total_mass_universe']/MC_results['n_primary']}\")\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 187, "metadata": {}, "outputs": [ { @@ -1101,7 +1099,7 @@ "dict_keys(['n_primary', 'n_binaries', 'n_compas', 'total_mass_universe', 'total_mass_singles', 'total_mass_binaries', 'primary_masses', 'secondary_masses', 'binary_mask', 'compas_mask', 'binary_systems', 'compas_systems', 'fb_for_every_m1'])" ] }, - "execution_count": 77, + "execution_count": 187, "metadata": {}, "output_type": "execute_result" } @@ -1112,16 +1110,16 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 188, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "dict_keys(['M_sf_Univ_per_N_binary_COMPAS', 'fint', 'Average_mass_stellar_sys_per_fbin', 'piece_wise_integral', 'alpha', 'binaryFractions', 'binary_bin_edges', 'imf_mass_bounds'])" + "dict_keys(['M_sf_Univ_per_N_binary_COMPAS', 'fint', 'Average_mass_stellar_sys_per_fbin', 'average_mass_stellar_sys', 'alpha', 'binaryFractions', 'binary_bin_edges', 'imf_mass_bounds'])" ] }, - "execution_count": 78, + "execution_count": 188, "metadata": {}, "output_type": "execute_result" } @@ -1134,15 +1132,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.007368877135189561, MC nbin_compas/nbin: 0.026630534785299677\n" - ] - } - ], + "outputs": [], "source": [ "\n" ] From 79ce13c989fbe4977d4516c16d8d76565116d2c1 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Tue, 30 Dec 2025 21:28:36 +0100 Subject: [PATCH 42/47] quick fix bug in analytic function --- .../binary_population.py | 26 +-- .../totalMassEvolvedPerZ.py | 185 +++++++++++++-- py_tests/test_fbinary_perM_inclMCMC.ipynb | 212 +++++++++++++++--- py_tests/test_total_mass_evolved_per_z.py | 17 +- 4 files changed, 367 insertions(+), 73 deletions(-) diff --git a/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py b/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py index 514d87b6c..7449a2bda 100644 --- a/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py +++ b/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py @@ -11,11 +11,6 @@ from .plotting import plot_binary_population from .stellar_type import BH, NS, WD -# Default IMF limits -M1_MIN = 5 -M1_MAX = 150 -M2_MIN = 0.1 - DCO_GROUPS = dict( BBH=[BH, BH], BNS=[NS, NS], @@ -63,9 +58,9 @@ def __init__( z_zams: np.ndarray, n_systems: int, dcos_included: List[str], - m1_min: float = M1_MIN, - m1_max: float = M1_MAX, - m2_min: float = M2_MIN, + m1_min: float = None, + m1_max: float = None, + m2_min: float = None, binary_fraction: float = 0.7, ): # Population selection @@ -100,9 +95,9 @@ def from_compas_h5( cls, path: str, dcos_included: List[str] = ["BBH"], - m1_min: float = M1_MIN, - m1_max: float = M1_MAX, - m2_min: float = M2_MIN, + m1_min: float = None, + m1_max: float = None, + m2_min: float = None, binary_fraction: float = 0.7, ) -> "BinaryPopulation": mask = cls._generate_mask(path, dcos_included) @@ -289,12 +284,15 @@ def generate_mock_population( frac_bbh: float = 0.7, frac_bns: float = 0.2, frac_bhns: float = 0.1, - m1_min: float = M1_MIN, - m1_max: float = M1_MAX, - m2_min: float = M2_MIN, + m1_min: float = None, + m1_max: float = None, + m2_min: float = None, ): if filename == "": filename = "dco_mock_population.h5" + + if m1_min is None or m1_max is None or m2_min is None: + raise ValueError("m1_min, m1_max, and m2_min must be provided to generate_mock_population") # sample masses and assign types m1, m2 = draw_samples_from_kroupa_imf(n_samples=n_systems, Mlower=m1_min, Mupper=m1_max, m2_low=m2_min) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 235c379af..8c76360bf 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -209,13 +209,16 @@ def draw_samples_from_kroupa_imf( +################################################### +# New version of analytical calculation ################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] ): """ Analytical computation of the mass of stars formed per binary star formed within the - [m1 min, m1 max] and [m2 min, ..] rage, using the Kroupa IMF: + [m1 min, m1 max] and [m2 min, ..] rage, + using the Kroupa IMF: p(M) \propto M^-0.3 for M between m1 and m2 p(M) \propto M^-1.3 for M between m2 and m3; @@ -227,6 +230,7 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max Lieke base on Ilya Mandel's derivation """ + ######### # Kroupa IMF m1, m2, m3, m4 = imf_mass_bounds continuity_constants = [1./(m2*m3), 1./(m3), 1.0] @@ -241,16 +245,9 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # normalize IMF over the complete mass range: alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + # print('alpha', alpha) - # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS - # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe - # = 1/fint * 1/fbin * average mass of a stellar system in the Universe - - # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates - fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) - - # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe - # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction + ######### # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] if fbin == None: @@ -260,20 +257,75 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # otherwise use a constant binary fraction binaryFractions = [fbin] * 5 + ################## + # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS + # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe + def N_binaries_kroupa_fbin(m1_low, m1_high, m2_min): + """ + Computes: + N = alpha * Σ_i Σ_j binaryFractions[i] * + ∫ ( m^{IMF_powers[j]} - m2_min * m^{IMF_powers[j]-1} ) + * continuity_constants[j] dm + + The integral is taken over the overlap of: + - binary-fraction bin i, + - IMF segment j, + - [m1_low, m1_high]. + """ + if m1_high <= m1_low: + raise ValueError("Require m1_high > m1_low") + + total = 0.0 + #Compute double piecewise integral + for i in range(len(binaryFractions)): + # overlap of binary-fraction bin with [m1_low, m1_high] + bin_lo = max(binary_bin_edges[i], m1_low) + bin_hi = min(binary_bin_edges[i + 1], m1_high) + + if bin_hi <= bin_lo: + continue + # split across IMF segments + for j in range(len(IMF_powers)): + m_start = max(bin_lo, imf_mass_bounds[j]) + m_end = min(bin_hi, imf_mass_bounds[j + 1]) + + if m_end <= m_start: + continue + + # ∫ m^{IMF_powers[j]} dm + integral_main = ( + m_end**(IMF_powers[j] + 1) - m_start**(IMF_powers[j] + 1) + ) / (IMF_powers[j] + 1) + + # ∫ m^{IMF_powers[j]-1} dm (only if m2_min > 0) + if m2_min > 0.0: + integral_m2 = ( + m_end**(IMF_powers[j]) - m_start**(IMF_powers[j]) + ) / (IMF_powers[j]) + else: + integral_m2 = 0.0 + + total += binaryFractions[i] * continuity_constants[j] * (integral_main - m2_min * integral_m2) + + return alpha * total + + # Integral for COMPAS sampled binaries + N_compas = N_binaries_kroupa_fbin(m1_low=m1_min, m1_high=m1_max, m2_min=m2_min) + + ################## + # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe + # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction + # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe, # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to: # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins. - piece_wise_integral = 0 + average_mass_stellar_sys_int = 0 # For every binary fraction bin for i in range(len(binary_bin_edges) - 1): - fbin = binaryFractions[i] # Binary fraction for this range - # And every piece of the Kroupa IMF for j in range(len(imf_mass_bounds) - 1): - exponent = IMF_powers[j] # IMF exponent for these masses - # Check if the binary fraction bin overlaps with the IMF mass bin if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]: continue # No overlap @@ -283,16 +335,105 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1]) # Compute the definite integral: - integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j] + integral = (m_end**(IMF_powers[j] + 2) - m_start**(IMF_powers[j] + 2) ) / (IMF_powers[j] + 2) * continuity_constants[j] # Compute the sum term - sum_term = (1 /fbin + 0.5) * integral - piece_wise_integral += sum_term + average_mass_stellar_sys_int += (1 + 0.5 * binaryFractions[i]) * integral - # combining them: - Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral + # Addint normalization + average_mass_stellar_sys = alpha * average_mass_stellar_sys_int # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS - M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin + M_sf_Univ_per_N_binary_COMPAS = (1/N_compas) * average_mass_stellar_sys + + return M_sf_Univ_per_N_binary_COMPAS + + - return M_sf_Univ_per_N_binary_COMPAS \ No newline at end of file +# ################################################### +# def analytical_star_forming_mass_per_binary_using_kroupa_imf( +# m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] +# ): +# """ +# Analytical computation of the mass of stars formed per binary star formed within the +# [m1 min, m1 max] and [m2 min, ..] rage, using the Kroupa IMF: + +# p(M) \propto M^-0.3 for M between m1 and m2 +# p(M) \propto M^-1.3 for M between m2 and m3; +# p(M) = alpha * M^-2.3 for M between m3 and m4; + +# m1_min, m1_max are the min and max sampled primary masses +# m2_min is the min sampled secondary mass + +# This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max +# Lieke base on Ilya Mandel's derivation +# """ +# # Kroupa IMF +# m1, m2, m3, m4 = imf_mass_bounds +# continuity_constants = [1./(m2*m3), 1./(m3), 1.0] +# IMF_powers = [-0.3, -1.3, -2.3] + +# if m1_min < m3: +# raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") +# if m1_min > m1_max: +# raise ValueError(f"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})") +# if m1_max > m4: +# raise ValueError(f"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})") + +# # normalize IMF over the complete mass range: +# alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) + +# # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS +# # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe +# # = 1/fint * 1/fbin * average mass of a stellar system in the Universe + +# # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates +# fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) + +# # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe +# # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction +# # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) +# binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] +# if fbin == None: +# # use a binary fraction that varies with mass +# binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] +# else: +# # otherwise use a constant binary fraction +# binaryFractions = [fbin] * 5 + +# # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe, +# # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to: +# # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. +# # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins. +# piece_wise_integral = 0 + +# # For every binary fraction bin +# for i in range(len(binary_bin_edges) - 1): +# fbin = binaryFractions[i] # Binary fraction for this range + +# # And every piece of the Kroupa IMF +# for j in range(len(imf_mass_bounds) - 1): +# exponent = IMF_powers[j] # IMF exponent for these masses + +# # Check if the binary fraction bin overlaps with the IMF mass bin +# if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]: +# continue # No overlap + +# # Integrate from the most narrow range +# m_start = max(binary_bin_edges[i], imf_mass_bounds[j]) +# m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1]) + +# # Compute the definite integral: +# integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j] + +# # Compute the sum term +# sum_term = (1 /fbin + 0.5) * integral +# piece_wise_integral += sum_term + +# # combining them: +# Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral + +# # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS +# M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin + +# return M_sf_Univ_per_N_binary_COMPAS \ No newline at end of file diff --git a/py_tests/test_fbinary_perM_inclMCMC.ipynb b/py_tests/test_fbinary_perM_inclMCMC.ipynb index 8168c376b..372afc39f 100644 --- a/py_tests/test_fbinary_perM_inclMCMC.ipynb +++ b/py_tests/test_fbinary_perM_inclMCMC.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 189, "metadata": {}, "outputs": [], "source": [ @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 190, "metadata": {}, "outputs": [], "source": [ @@ -460,7 +460,153 @@ }, { "cell_type": "code", - "execution_count": 179, + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "###################################################\n", + "# New version of analytical calculation\n", + "###################################################\n", + "def analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", + " m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200]\n", + "):\n", + " \"\"\"\n", + " Analytical computation of the mass of stars formed per binary star formed within the\n", + " [m1 min, m1 max] and [m2 min, ..] rage,\n", + " using the Kroupa IMF:\n", + "\n", + " p(M) \\propto M^-0.3 for M between m1 and m2\n", + " p(M) \\propto M^-1.3 for M between m2 and m3;\n", + " p(M) = alpha * M^-2.3 for M between m3 and m4;\n", + "\n", + " m1_min, m1_max are the min and max sampled primary masses\n", + " m2_min is the min sampled secondary mass\n", + "\n", + " This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max\n", + " Lieke base on Ilya Mandel's derivation\n", + " \"\"\"\n", + " #########\n", + " # Kroupa IMF \n", + " m1, m2, m3, m4 = imf_mass_bounds\n", + " continuity_constants = [1./(m2*m3), 1./(m3), 1.0] \n", + " IMF_powers = [-0.3, -1.3, -2.3] \n", + "\n", + " if m1_min < m3:\n", + " raise ValueError(f\"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})\")\n", + " if m1_min > m1_max:\n", + " raise ValueError(f\"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})\")\n", + " if m1_max > m4:\n", + " raise ValueError(f\"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})\")\n", + " \n", + " # normalize IMF over the complete mass range:\n", + " alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1)\n", + " # print('alpha', alpha)\n", + "\n", + " #########\n", + " # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023)\n", + " binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] \n", + " if fbin == None:\n", + " # use a binary fraction that varies with mass\n", + " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] \n", + " else:\n", + " # otherwise use a constant binary fraction\n", + " binaryFractions = [fbin] * 5\n", + "\n", + " ##################\n", + " # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", + " # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", + " def N_binaries_kroupa_fbin(m1_low, m1_high, m2_min):\n", + " \"\"\"\n", + " Computes:\n", + " N = alpha * Σ_i Σ_j binaryFractions[i] *\n", + " ∫ ( m^{IMF_powers[j]} - m2_min * m^{IMF_powers[j]-1} )\n", + " * continuity_constants[j] dm\n", + "\n", + " The integral is taken over the overlap of:\n", + " - binary-fraction bin i,\n", + " - IMF segment j,\n", + " - [m1_low, m1_high].\n", + " \"\"\"\n", + " if m1_high <= m1_low:\n", + " raise ValueError(\"Require m1_high > m1_low\")\n", + "\n", + " total = 0.0\n", + " #Compute double piecewise integral\n", + " for i in range(len(binaryFractions)):\n", + " # overlap of binary-fraction bin with [m1_low, m1_high]\n", + " bin_lo = max(binary_bin_edges[i], m1_low)\n", + " bin_hi = min(binary_bin_edges[i + 1], m1_high)\n", + "\n", + " if bin_hi <= bin_lo:\n", + " continue\n", + " # split across IMF segments\n", + " for j in range(len(IMF_powers)):\n", + " m_start = max(bin_lo, imf_mass_bounds[j])\n", + " m_end = min(bin_hi, imf_mass_bounds[j + 1])\n", + "\n", + " if m_end <= m_start:\n", + " continue\n", + "\n", + " # ∫ m^{IMF_powers[j]} dm\n", + " integral_main = (\n", + " m_end**(IMF_powers[j] + 1) - m_start**(IMF_powers[j] + 1)\n", + " ) / (IMF_powers[j] + 1)\n", + "\n", + " # ∫ m^{IMF_powers[j]-1} dm (only if m2_min > 0)\n", + " if m2_min > 0.0:\n", + " integral_m2 = (\n", + " m_end**(IMF_powers[j]) - m_start**(IMF_powers[j])\n", + " ) / (IMF_powers[j])\n", + " else:\n", + " integral_m2 = 0.0\n", + "\n", + " total += binaryFractions[i] * continuity_constants[j] * (integral_main - m2_min * integral_m2)\n", + "\n", + " return alpha * total\n", + "\n", + " # Integral for COMPAS sampled binaries\n", + " N_compas = N_binaries_kroupa_fbin(m1_low=m1_min, m1_high=m1_max, m2_min=m2_min)\n", + "\n", + " ##################\n", + " # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", + " # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction \n", + "\n", + " # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe,\n", + " # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to:\n", + " # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. \n", + " # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins.\n", + " average_mass_stellar_sys_int = 0\n", + "\n", + " # For every binary fraction bin\n", + " for i in range(len(binary_bin_edges) - 1):\n", + " # And every piece of the Kroupa IMF\n", + " for j in range(len(imf_mass_bounds) - 1):\n", + " # Check if the binary fraction bin overlaps with the IMF mass bin\n", + " if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]:\n", + " continue # No overlap\n", + "\n", + " # Integrate from the most narrow range\n", + " m_start = max(binary_bin_edges[i], imf_mass_bounds[j])\n", + " m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1])\n", + "\n", + " # Compute the definite integral:\n", + " integral = (m_end**(IMF_powers[j] + 2) - m_start**(IMF_powers[j] + 2) ) / (IMF_powers[j] + 2) * continuity_constants[j]\n", + "\n", + " # Compute the sum term\n", + " average_mass_stellar_sys_int += (1 + 0.5 * binaryFractions[i]) * integral \n", + "\n", + " # Addint normalization\n", + " average_mass_stellar_sys = alpha * average_mass_stellar_sys_int\n", + "\n", + " # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", + " M_sf_Univ_per_N_binary_COMPAS = (1/N_compas) * average_mass_stellar_sys\n", + "\n", + " return analytical_results" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -617,6 +763,8 @@ "\n", " analytical_results = {\n", " 'M_sf_Univ_per_N_binary_COMPAS': M_sf_Univ_per_N_binary_COMPAS, \n", + " 'N_compas': N_compas,\n", + " 'N_univ': N_univ,\n", " 'fint': fint,\n", " 'Average_mass_stellar_sys_per_fbin': Average_mass_stellar_sys_per_fbin,\n", " 'average_mass_stellar_sys': average_mass_stellar_sys ,\n", @@ -637,7 +785,7 @@ }, { "cell_type": "code", - "execution_count": 180, + "execution_count": 204, "metadata": {}, "outputs": [ { @@ -698,12 +846,12 @@ }, { "cell_type": "code", - "execution_count": 181, + "execution_count": 205, "metadata": {}, "outputs": [ { "data": { - "image/png": "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whe+0JntAwDonqCzffv2aGtriwsuuCDWr18fZ511Vse+vn37xtlnnx29e/c+9dYAAJQq6Jx33nnFz9bW1s5sAwBA6YPOa23dujUeffTR2LNnz3HBZ968eZ3RNgCA7g869913X9x0000xePDgGDp0aFGz0y49FnQAgB4bdG6//fa444474tOf/nTntwgAoJTX0XnppZfiuuuu66w2AACUT9BJIefb3/5257cGAKDUQ1cXXnhh3HbbbbFu3bq49NJL47TTTjtm/8c+9rHoCVdGTku60CEAkKeqtnRhnF/Q+eeff/ITVlXFCy+8ED1F+5WRm5qaorq6ulPPPWrOyk49H0C52nHnNaVuAhWm+U1+f59Sj066cCAAQJY1OgAAPcEp9eh85CMfecP9S5cuPdX2AACUNuik6eWvdeTIkXj22Wdj//79J7zZJwBAjwk6Dz300HHb0m0g0tWS3/nOd3ZGuwAAyqdGp1evXjF79uy4++67O+uUAADlU4z8wx/+MF599dXOPCUAQPcOXaWem9dKl+L53//931i5cmXMmDHj1FsDAFDqoPNf//Vfxw1bnXXWWfG5z33u587IAgAo66Dz6KOPdn5LAADKIei027t3bzz//PPF44svvrjo1QEA6NHFyIcOHSqGqM4555y4/PLLi2XYsGFx4403xssvvxw9QbqhZ21tbdTV1ZW6KQBAOQWdVIz8+OOPxyOPPFJcJDAtDz/8cLHtE5/4RPQE9fX10djYGA0NDaVuCgBQTkNXDz74YHzjG9+ID3zgAx3brr766jj99NPjQx/6UNxzzz2d2UYAgO7r0UnDU0OGDDlu+9lnn91jhq4AgPydUtCZOHFizJ8/P1555ZWObT/72c/iM5/5TLEPAKDHDl0tWrQorrrqqhg+fHiMHj262Pb0009Hv3794tvf/nZntxEAoPuCzqWXXhpbt26N+++/P7Zs2VJs+/CHPxzXX399UacDANBjg86CBQuKGp1Zs2Yds33p0qXFtXU+/elPd1b7AAC6t0bn3nvvjV/6pV86bvsll1wSS5YsOfXWAACUOujs2rWruFjg66UrI6ebewIA9NigM2LEiHjyySeP2562pSskAwD02BqdVJtzyy23xJEjR+LKK68stq1evTo+9alP9ZgrIwMA+TuloHPrrbfGT37yk/joRz8aLS0txbb+/fsXRchz587t7DYC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", + "image/png": "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", "text/plain": [ "
" ] @@ -804,7 +952,7 @@ }, { "cell_type": "code", - "execution_count": 182, + "execution_count": 206, "metadata": {}, "outputs": [ { @@ -844,7 +992,8 @@ " # Map masses to bins\n", " m1_arr = np.asarray(m1)\n", " bin_index = np.digitize(m1_arr, binary_bin_edges, right=True) - 1\n", - " \n", + " bin_index = np.clip(bin_index, 0, len(binaryFractions) - 1)\n", + "\n", " fb_array = np.array(binaryFractions, dtype=float)[bin_index]\n", " if m1_arr.ndim == 0:\n", " return float(fb_array)\n", @@ -859,7 +1008,7 @@ }, { "cell_type": "code", - "execution_count": 183, + "execution_count": 207, "metadata": {}, "outputs": [], "source": [ @@ -971,7 +1120,7 @@ }, { "cell_type": "code", - "execution_count": 184, + "execution_count": 208, "metadata": {}, "outputs": [ { @@ -980,8 +1129,8 @@ "text": [ " old analytical m_rep =253.53899506390485, new analytical m_rep = 253.53899506390468\n", "using constant binaryFractions = 0.7\n", - "MC_results m_rep = 257.0996760119897\n", - "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.0029163857961781803\n" + "MC_results m_rep = 255.03275505787337\n", + "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.0029376568693338452\n" ] } ], @@ -1028,21 +1177,22 @@ }, { "cell_type": "code", - "execution_count": 185, + "execution_count": 219, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - " new analytical m_rep = 94.10865010969138\n", + " new analytical m_rep = 162.56300080093715\n", "using mass-dependent binaryFractions from Offner et al. (2023)\n", - "MC_results m_rep = 173.0107343824034\n" + "MC_results m_rep = 298.13638972568225\n", + "1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] 297.0201523856339\n" ] } ], "source": [ - "m1_min = 10\n", + "m1_min = 15\n", "m1_max = 150\n", "m2_min = 0.1\n", "fbin = None\n", @@ -1056,23 +1206,26 @@ "MC_results = create_mock_universe( n_primary=int(5e6), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", "\n", "print('MC_results m_rep = ', MC_results['total_mass_universe']/MC_results['n_compas'] )\n", - "\n" + "\n", + "print(f\"1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] {1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys']}\")\n" ] }, { "cell_type": "code", - "execution_count": 186, + "execution_count": 216, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Analytic: N_binaries_in_COMPAS/N_binaries_in_universe: 0.012375649403900452, \n", - " MC: nbin_compas/nbin: 0.012262259119244086\n", + "Analytic: N_binaries_in_COMPAS/N_binaries_in_universe: 0.02715404524930118, \n", + " MC: nbin_compas/nbin: 0.02724300246150036\n", "\n", " Analytic: Mstellar_sys_univ/Nstellar_sys_univ = 0.5083080461257276\n", - " MC: total_mass_universe/n_primary = 0.5067484410060595\n" + " MC: total_mass_universe/n_primary = 0.5118063519181145\n", + "1/new_analytical['N_compas'] 154.1701265561456 new_analytical['average_mass_stellar_sys'] 0.5083080461257276\n", + "1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] 78.36591580071051\n" ] } ], @@ -1085,12 +1238,17 @@ "print()\n", "# yellow and blue terms:\n", "print(f\" Analytic: Mstellar_sys_univ/Nstellar_sys_univ = {new_analytical['average_mass_stellar_sys']}\")\n", - "print(f\" MC: total_mass_universe/n_primary = {MC_results['total_mass_universe']/MC_results['n_primary']}\")\n" + "print(f\" MC: total_mass_universe/n_primary = {MC_results['total_mass_universe']/MC_results['n_primary']}\")\n", + "\n", + "\n", + "print(f\"1/new_analytical['N_compas'] {1/new_analytical['N_compas']} new_analytical['average_mass_stellar_sys'] {new_analytical['average_mass_stellar_sys']}\")\n", + "print(f\"1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] {1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys']}\")\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 187, + "execution_count": 217, "metadata": {}, "outputs": [ { @@ -1099,7 +1257,7 @@ "dict_keys(['n_primary', 'n_binaries', 'n_compas', 'total_mass_universe', 'total_mass_singles', 'total_mass_binaries', 'primary_masses', 'secondary_masses', 'binary_mask', 'compas_mask', 'binary_systems', 'compas_systems', 'fb_for_every_m1'])" ] }, - "execution_count": 187, + "execution_count": 217, "metadata": {}, "output_type": "execute_result" } @@ -1110,16 +1268,16 @@ }, { "cell_type": "code", - "execution_count": 188, + "execution_count": 212, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "dict_keys(['M_sf_Univ_per_N_binary_COMPAS', 'fint', 'Average_mass_stellar_sys_per_fbin', 'average_mass_stellar_sys', 'alpha', 'binaryFractions', 'binary_bin_edges', 'imf_mass_bounds'])" + "dict_keys(['M_sf_Univ_per_N_binary_COMPAS', 'N_compas', 'N_univ', 'fint', 'Average_mass_stellar_sys_per_fbin', 'average_mass_stellar_sys', 'alpha', 'binaryFractions', 'binary_bin_edges', 'imf_mass_bounds'])" ] }, - "execution_count": 188, + "execution_count": 212, "metadata": {}, "output_type": "execute_result" } diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index 1c23900b8..f14c459a4 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -51,13 +51,13 @@ def test_compas_fraction(): def test_analytical_function(): - default_case = analytical_star_forming_mass_per_binary_using_kroupa_imf( - m1_max=150, - m1_min=5, - m2_min=0.1, - fbin=1 + result = analytical_star_forming_mass_per_binary_using_kroupa_imf( + m1_min=M1_MIN, + m1_max=M1_MAX, + m2_min=M2_MIN, + fbin=F_BIN ) - assert 79.0 < default_case < 79.2 + assert result > 0 def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output, tmpdir, test_archive_dir): @@ -93,10 +93,7 @@ def plot_star_forming_mass_per_binary_comparison( vals = np.zeros(len(n_samps)) for i, n in enumerate(n_samps): fname = f"{tmpdir}/test_{i}.h5" - - generate_mock_bbh_population_file(filename=fname, n_systems=int(n), - m1_min=m1_min, m1_max=m1_max, m2_min=m2_min) - # generate_mock_bbh_population_file(fname, n_systems=int(n)) + generate_mock_population(fname, n_systems=int(n), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min) vals[i] = (star_forming_mass_per_binary(fname, m1_min, m1_max, m2_min, fbin)) numerical_vals.append(vals) From 1f057a9ebca5415dc973889fcd86d283e59eabd1 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Sat, 3 Jan 2026 22:38:39 +0100 Subject: [PATCH 43/47] Fixed analytical derivation to handle variable fbin values --- .../totalMassEvolvedPerZ.py | 309 ++-- py_tests/test_fbinary_perM_inclMCMC.ipynb | 1473 ----------------- 2 files changed, 101 insertions(+), 1681 deletions(-) delete mode 100644 py_tests/test_fbinary_perM_inclMCMC.ipynb diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index 8c76360bf..df427bc10 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -210,230 +210,123 @@ def draw_samples_from_kroupa_imf( ################################################### -# New version of analytical calculation +# Analytical calculation of star forming mass per binary ################################################### def analytical_star_forming_mass_per_binary_using_kroupa_imf( - m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] -): + m1_min, m1_max, m2_min, fbin=1.0, imf_mass_bounds=(0.01, 0.08, 0.5, 200.0)): """ - Analytical computation of the mass of stars formed per binary star formed within the - [m1 min, m1 max] and [m2 min, ..] rage, - using the Kroupa IMF: - - p(M) \propto M^-0.3 for M between m1 and m2 - p(M) \propto M^-1.3 for M between m2 and m3; - p(M) = alpha * M^-2.3 for M between m3 and m4; - - m1_min, m1_max are the min and max sampled primary masses - m2_min is the min sampled secondary mass - - This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max - Lieke base on Ilya Mandel's derivation + Takes: + m1_min, m1_max, m2_min: COMPAS mass ranges [Msun] + fbin: binary fraction (if None, use piecewise constant fbin(m1)) + imf_mass_bounds: Kroupa IMF mass bounds [Msun] + Computes: + N_bin_in_COMPAS + average_stellar_mass_sys = M_sys,Univ / N_sys,Univ (blue) + M_sf_Univ_per_N_binary_COMPAS = average_stellar_mass_sys / N_bin_in_COMPAS + + Assumes: + Kroupa IMF: P(m1) = alpha * C_cont,i * m1^{gamma_i} on IMF segment i + Piecewise constant on binary-fraction: f_bin(m1) with bins j + Flat mass ratio: P(q)=U(0,1) => P(m2>m2_min | m1) = 1 - m2_min/m1 (for m1 >= m2_min) """ - ######### + # ------------------------- # Kroupa IMF + # ------------------------- m1, m2, m3, m4 = imf_mass_bounds - continuity_constants = [1./(m2*m3), 1./(m3), 1.0] - IMF_powers = [-0.3, -1.3, -2.3] + continuity_constants = [1.0 / (m2 * m3), 1.0 / m3, 1.0] + IMF_powers = [-0.3, -1.3, -2.3] - if m1_min < m3: - raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") if m1_min > m1_max: - raise ValueError(f"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})") + raise ValueError(f"Require m1_min <= m1_max, got {m1_min} > {m1_max}.") + if m1_min < m1: + raise ValueError(f"Require m1_min >= {m1} (Universe lower IMF bound).") if m1_max > m4: - raise ValueError(f"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})") - - # normalize IMF over the complete mass range: - alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) - # print('alpha', alpha) - - ######### - # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) - binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] - if fbin == None: - # use a binary fraction that varies with mass - binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] + raise ValueError(f"Require m1_max <= {m4} (Universe upper IMF bound).") + + # Normalization alpha over full IMF range [m1, m4] + alpha = ( + - (m4 ** (-1.3) - m3 ** (-1.3)) / 1.3 + - (m3 ** (-0.3) - m2 ** (-0.3)) / (m3 * 0.3) + + (m2 ** (0.7) - m1 ** (0.7)) / (m2 * m3 * 0.7) + ) ** (-1) + + # ------------------------- + # Binary-fraction bins + # ------------------------- + binary_bin_edges = [m1, 0.08, 0.5, 1.0, 10.0, m4] + if fbin is None: + binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] else: - # otherwise use a constant binary fraction - binaryFractions = [fbin] * 5 - - ################## - # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS - # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe - def N_binaries_kroupa_fbin(m1_low, m1_high, m2_min): - """ - Computes: - N = alpha * Σ_i Σ_j binaryFractions[i] * - ∫ ( m^{IMF_powers[j]} - m2_min * m^{IMF_powers[j]-1} ) - * continuity_constants[j] dm - - The integral is taken over the overlap of: - - binary-fraction bin i, - - IMF segment j, - - [m1_low, m1_high]. - """ - if m1_high <= m1_low: - raise ValueError("Require m1_high > m1_low") - - total = 0.0 - #Compute double piecewise integral - for i in range(len(binaryFractions)): - # overlap of binary-fraction bin with [m1_low, m1_high] - bin_lo = max(binary_bin_edges[i], m1_low) - bin_hi = min(binary_bin_edges[i + 1], m1_high) - - if bin_hi <= bin_lo: + binaryFractions = [float(fbin)] * (len(binary_bin_edges) - 1) + + # ------------------------- + # Helpers: overlaps and power integrals + # ------------------------- + def overlap(lo1, hi1, lo2, hi2): + lo = max(lo1, lo2) + hi = min(hi1, hi2) + return lo, hi + + def int_power(lo, hi, power): + """∫_lo^hi m^{power} dm, for power != -1.""" + if hi <= lo: + return 0.0 + return (hi ** (power + 1) - lo ** (power + 1)) / (power + 1) + + # ------------------------- + # Average_stellar_mass_sys = ∫_Univ (1+0.5 fbin) m1 P(m1) dm1 + # ------------------------- + av_Mstar_integral_sum = 0.0 + for i in range(len(IMF_powers)): # IMF segment index i + gamma_i = IMF_powers[i] + C_cont_i = continuity_constants[i] + imf_lo, imf_hi = imf_mass_bounds[i], imf_mass_bounds[i + 1] + + for j in range(len(binaryFractions)): # fbin bin index j + fbin_j = binaryFractions[j] + fb_lo, fb_hi = binary_bin_edges[j], binary_bin_edges[j + 1] + + A, B = overlap(imf_lo, imf_hi, fb_lo, fb_hi) + if B <= A: continue - # split across IMF segments - for j in range(len(IMF_powers)): - m_start = max(bin_lo, imf_mass_bounds[j]) - m_end = min(bin_hi, imf_mass_bounds[j + 1]) - - if m_end <= m_start: - continue - - # ∫ m^{IMF_powers[j]} dm - integral_main = ( - m_end**(IMF_powers[j] + 1) - m_start**(IMF_powers[j] + 1) - ) / (IMF_powers[j] + 1) - - # ∫ m^{IMF_powers[j]-1} dm (only if m2_min > 0) - if m2_min > 0.0: - integral_m2 = ( - m_end**(IMF_powers[j]) - m_start**(IMF_powers[j]) - ) / (IMF_powers[j]) - else: - integral_m2 = 0.0 - - total += binaryFractions[i] * continuity_constants[j] * (integral_main - m2_min * integral_m2) - return alpha * total - - # Integral for COMPAS sampled binaries - N_compas = N_binaries_kroupa_fbin(m1_low=m1_min, m1_high=m1_max, m2_min=m2_min) - - ################## - # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe - # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction - - # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe, - # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to: - # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. - # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins. - average_mass_stellar_sys_int = 0 - - # For every binary fraction bin - for i in range(len(binary_bin_edges) - 1): - # And every piece of the Kroupa IMF - for j in range(len(imf_mass_bounds) - 1): - # Check if the binary fraction bin overlaps with the IMF mass bin - if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]: - continue # No overlap - - # Integrate from the most narrow range - m_start = max(binary_bin_edges[i], imf_mass_bounds[j]) - m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1]) + # integrand: (1 + 0.5 fbin_j) * m1 * (alpha*C_cont_i*m1^{gamma_i}) + # => alpha * (1+0.5 fbin_j) * C_cont_i * ∫ m1^{gamma_i+1} dm1 + av_Mstar_integral_sum += (1.0 + 0.5 * fbin_j) * C_cont_i * int_power(A, B, gamma_i + 1) + + average_stellar_mass_sys = alpha * av_Mstar_integral_sum + + # ------------------------- + # N_bin_in_COMPAS = ∫_{m1_min}^{m1_max} P(m1) fbin(m1) (1 - m2_min/m1) dm1 + # ------------------------- + N_bin_compas_sum = 0.0 + for i in range(len(IMF_powers)): # IMF segment index i + gamma_i = IMF_powers[i] + C_cont_i = continuity_constants[i] + imf_lo, imf_hi = imf_mass_bounds[i], imf_mass_bounds[i + 1] + + for j in range(len(binaryFractions)): # fbin bin index j + fbin_j = binaryFractions[j] + fb_lo, fb_hi = binary_bin_edges[j], binary_bin_edges[j + 1] + + # overlap additionally with COMPAS m1-range + A, B = overlap(imf_lo, imf_hi, fb_lo, fb_hi) + A, B = overlap(A, B, m1_min, m1_max) + if B <= A: + continue - # Compute the definite integral: - integral = (m_end**(IMF_powers[j] + 2) - m_start**(IMF_powers[j] + 2) ) / (IMF_powers[j] + 2) * continuity_constants[j] + # integrand: alpha*C_cont_i*m^{gamma_i} * fbin_j * (1 - m2_min/m) + # => alpha*fbin_j*C_cont_i * ∫ (m^{gamma_i} - m2_min*m^{gamma_i-1}) dm + term_main = int_power(A, B, gamma_i) # ∫ m^{gamma_i} dm + term_m2 = int_power(A, B, gamma_i - 1) # ∫ m^{gamma_i-1} dm + N_bin_compas_sum += fbin_j * C_cont_i * (term_main - m2_min * term_m2) - # Compute the sum term - average_mass_stellar_sys_int += (1 + 0.5 * binaryFractions[i]) * integral + N_bin_in_COMPAS = alpha * N_bin_compas_sum - # Addint normalization - average_mass_stellar_sys = alpha * average_mass_stellar_sys_int + if N_bin_in_COMPAS <= 0.0: + raise ValueError("Computed N_bin_in_COMPAS <= 0; check bounds and m2_min.") - # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS - M_sf_Univ_per_N_binary_COMPAS = (1/N_compas) * average_mass_stellar_sys + M_sf_Univ_per_N_binary_COMPAS = average_stellar_mass_sys / N_bin_in_COMPAS return M_sf_Univ_per_N_binary_COMPAS - - -# ################################################### -# def analytical_star_forming_mass_per_binary_using_kroupa_imf( -# m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200] -# ): -# """ -# Analytical computation of the mass of stars formed per binary star formed within the -# [m1 min, m1 max] and [m2 min, ..] rage, using the Kroupa IMF: - -# p(M) \propto M^-0.3 for M between m1 and m2 -# p(M) \propto M^-1.3 for M between m2 and m3; -# p(M) = alpha * M^-2.3 for M between m3 and m4; - -# m1_min, m1_max are the min and max sampled primary masses -# m2_min is the min sampled secondary mass - -# This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max -# Lieke base on Ilya Mandel's derivation -# """ -# # Kroupa IMF -# m1, m2, m3, m4 = imf_mass_bounds -# continuity_constants = [1./(m2*m3), 1./(m3), 1.0] -# IMF_powers = [-0.3, -1.3, -2.3] - -# if m1_min < m3: -# raise ValueError(f"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})") -# if m1_min > m1_max: -# raise ValueError(f"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})") -# if m1_max > m4: -# raise ValueError(f"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})") - -# # normalize IMF over the complete mass range: -# alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1) - -# # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS -# # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe -# # = 1/fint * 1/fbin * average mass of a stellar system in the Universe - -# # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates -# fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3)) - -# # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe -# # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction -# # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023) -# binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] -# if fbin == None: -# # use a binary fraction that varies with mass -# binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] -# else: -# # otherwise use a constant binary fraction -# binaryFractions = [fbin] * 5 - -# # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe, -# # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to: -# # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. -# # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins. -# piece_wise_integral = 0 - -# # For every binary fraction bin -# for i in range(len(binary_bin_edges) - 1): -# fbin = binaryFractions[i] # Binary fraction for this range - -# # And every piece of the Kroupa IMF -# for j in range(len(imf_mass_bounds) - 1): -# exponent = IMF_powers[j] # IMF exponent for these masses - -# # Check if the binary fraction bin overlaps with the IMF mass bin -# if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]: -# continue # No overlap - -# # Integrate from the most narrow range -# m_start = max(binary_bin_edges[i], imf_mass_bounds[j]) -# m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1]) - -# # Compute the definite integral: -# integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j] - -# # Compute the sum term -# sum_term = (1 /fbin + 0.5) * integral -# piece_wise_integral += sum_term - -# # combining them: -# Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral - -# # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS -# M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin - -# return M_sf_Univ_per_N_binary_COMPAS \ No newline at end of file diff --git a/py_tests/test_fbinary_perM_inclMCMC.ipynb b/py_tests/test_fbinary_perM_inclMCMC.ipynb deleted file mode 100644 index 372afc39f..000000000 --- a/py_tests/test_fbinary_perM_inclMCMC.ipynb +++ /dev/null @@ -1,1473 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Notebook to test implementation of mass-dependent binary fraction\n", - "\n", - "in the [compas_python_utils/cosmic_integration](https://github.com/TeamCOMPAS/COMPAS/tree/dev/compas_python_utils/cosmic_integration) folder of `COMPAS`, there are a couple of functions that take the binary fraction `fbin`. \n", - "\n", - "We keep on assuming that fbin is a constant, while really it depends on the primary mass. In this notebook I want to describe the analytical derivation of using $f_{bin}(M_1)$, and test that my derivation gets you the same answer as an MC sampler. \n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 189, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Where is fbinary used? \n", - "\n", - "It is passed to `FastCosmicIntegration.py`, \n", - "\n", - "``` python\n", - "COMPAS = ClassCOMPAS.COMPASData(path, Mlower=m1_min, Mupper=m1_max, m2_min=m2_min, binaryFraction=fbin, suppress_reminder=True)\n", - "```\n", - "\n", - "which passes it to `ClassCOMPAS.py`, where it is part of the `class COMPASData(object):`\n", - "It is specifically used in `def setGridAndMassEvolved(self)`, `recalculateTrueSolarMassEvolved(self, Mlower, Mupper, binaryFraction):` and lastly `find_star_forming_mass_per_binary_sampling(self)`\n", - "\n", - "The functions ClassCOMPAS are again just referring to `totalMassEvolvedPerZ.py` which is where the real magic happens \n", - "\n", - "This is again adding another layer and just pointing to `get_COMPAS_fraction` and `analytical_star_forming_mass_per_binary_using_kroupa_imf`. We really only need/use this last function, so I am going to address that:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 190, - "metadata": {}, - "outputs": [], - "source": [ - "###################################################\n", - "# Old version of analytical calculation\n", - "###################################################\n", - "def OLD_analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200]\n", - "):\n", - " \"\"\"\n", - " Analytical computation of the mass of stars formed per binary star formed within the\n", - " [m1 min, m1 max] and [m2 min, ..] rage,\n", - " using the Kroupa IMF:\n", - "\n", - " p(M) \\propto M^-0.3 for M between m1 and m2\n", - " p(M) \\propto M^-1.3 for M between m2 and m3;\n", - " p(M) = alpha * M^-2.3 for M between m3 and m4;\n", - "\n", - " m1_min, m1_max are the min and max sampled primary masses\n", - " m2_min is the min sampled secondary mass\n", - "\n", - " @Ilya Mandel's derivation\n", - " \"\"\"\n", - " m1, m2, m3, m4 = imf_mass_bounds\n", - " if m1_min < m3:\n", - " raise ValueError(f\"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})\")\n", - " if m1_min > m1_max:\n", - " raise ValueError(f\"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})\")\n", - " if m1_max > m4:\n", - " raise ValueError(f\"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})\")\n", - " \n", - " # normalize IMF over the complete mass range:\n", - " alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1)\n", - " # print('alpha', alpha)\n", - "\n", - " # average mass of stars (average mass of all binaries is a factor of 1.5 larger)\n", - " m_avg = alpha * (-(m4**(-0.3)-m3**(-0.3))/0.3 + (m3**0.7-m2**0.7)/(m3*0.7) + (m2**1.7-m1**1.7)/(m2*m3*1.7))\n", - "\n", - " # fraction of binaries that COMPAS simulates (i.e., N_binaries_in_COMPAS/N_binaries_in_universe) \n", - " # second term assumes a flat mass ratio distribution with m2_max = m1_max\n", - " fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3))\n", - "\n", - " # Average mass of systems (M_rep_by_all_binary_systems/N_binaries_in_universe)\n", - " # 1.5 = Average number of stars in single and binary systems, (1-fbin)/fbin) = ratio of single/binary systems\n", - " average_mass_per_binary = m_avg * (1.5 + (1-fbin)/fbin)\n", - "\n", - " # mass represented by each binary simulated by COMPAS\n", - " # N_binaries_in_universe/N_binaries_in_COMPAS * M_rep_by_all_binary_systems/N_binaries_in_universe\n", - " m_rep = (1/fint) * average_mass_per_binary \n", - " \n", - " analytical_results = {\n", - " 'alpha': alpha, \n", - " 'm_avg': m_avg,\n", - " 'fint': fint,\n", - " 'average_mass_per_binary': average_mass_per_binary,\n", - " 'm_rep': m_rep\n", - " }\n", - " return analytical_results\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Breaking down the analytical calculation of $\\tt{star\\_forming\\_mass\\_per\\_binary\\_using\\_kroupa\\_imf}$\n", - "\n", - "We are calculating the average stellar mass formed in the universe (both singles and binaries) per a binary system formed in COMPAS, in other words:\n", - "\n", - "$$\n", - "\\begin{align}\n", - "\\frac{M_{\\rm stellar\\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}} & =\\\\\n", - " & \\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} \n", - " \\textcolor{orange}{\\times \\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}}\n", - " \\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} }\n", - "\\end{align}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "The first term is just the fraction of the integral over $m1$ and $q$ that is spanned by the COMPAS simulation. \n", - "Importantly, we cannot neglect the fbin(m1) function in this integral! This is the difference between calculating the “fraction of IMF-weighted primaries that land in COMPAS” versus the “fraction of binary-weighted primaries that land in COMPAS” (the latter is what we want). \n", - "\n", - "$$\n", - "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} = \\frac{\\int\\int_{min(UNIV)}^{max(UNIV)} P(m1) f_{bin}(m1) P(q) dm1 dq}{\\int\\int_{min(COMPAS)}^{max(COMPAS)} P(m1) f_{bin}(m1) P(q) dm1 dq}\n", - "$$\n", - "\n", - "\n", - "Now we assume that $p(m1) = \\rm Kroupa$. \n", - "Furthermore, we adopt $p(q) = U(0,1)$ (so flat in q).\n", - "\n", - "With this, the COMPAS integral part:\n", - "\n", - "$$\n", - "\\int_{m1,min}^{m1,max} \\int_{m2,min/m1}^{1} P(m1) f_{bin}(m1) P(q) dm1 dq \n", - "= \n", - "\\int_{m1,min}^{m1,max} P(m1)f_{bin}(m1) \\left( 1 - \\frac{m_{2,min}}{m_1} \\right) dm_1 dq \n", - "$$\n", - "\n", - "So we can re-write the whole thing as\n", - "\n", - "$$\n", - "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}}\n", - "=\n", - "\\frac{ \\int_{UNIV} P(m_1) f_{bin}(m_1) dm_1\n", - "}{\n", - "\\int_{COMPAS} P(m_1) f_{bin}(m_1) \\left( 1 - \\frac{m_{2,min}}{m_1} \\right) dm_1 }.\n", - "$$\n", - "\n", - "\n", - "We assume that the **primary-mass range of interest satisfies $a > 0.5\\,M_\\odot$**, such that only the high-mass Kroupa slope applies.\n", - "\n", - "Hence in this regime the IMF is\n", - "$$\n", - "p(m_1) = \\alpha\\, m_1^{-2.3},\n", - "$$\n", - "with $\\alpha$ the (global) IMF normalization constant.\n", - "\n", - "Let's evaluate the top and bottom separately \n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "\\begin{aligned}\n", - "\\textcolor{green}{N_{\\rm binaries \\ in \\ COMPAS}} & = \n", - "\\int_{m_{1,\\min}}^{m_{1,\\max}} p(m_1)\\, f_{\\mathrm{bin}}(m_1)\\, \\left(1 - \\frac{m_{2,\\min}}{m_1}\\right)\\, \\mathrm{d}m_1. \\\\\n", - " & = \\alpha \\sum_i f_i \\int_{A_i}^{B_i} \\left( m_1^{-2.3} - m_{2,\\min}\\, m_1^{-3.3} \\right) \\mathrm{d}m_1,\n", - "\\end{aligned}\n", - "\n", - "Where the second line has written this out piecewise over the binary-fraction bins, and\n", - "\n", - "$[A_i,B_i]$ is the overlap of the $i$-th binary-fraction bin with\n", - "$[m_{1,\\min}, m_{1,\\max}]$.\n", - "\n", - "Evaluating the integrals:\n", - "\n", - "\n", - "$$\n", - "\\textcolor{green}{N_{\\rm binaries \\ in \\ COMPAS}} = \n", - "\\alpha \\sum_i f_i\n", - "\\left( \n", - " \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{A_i}^{B_i} - m_{2,\\min}\n", - " \\left[ \\frac{m_1^{-2.3}}{-2.3} \\right]_{A_i}^{B_i}\n", - "\\right).\n", - "$$\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, \n", - "\n", - "$$\n", - "\\textcolor{green}{N_{\\rm binaries\\ in \\ Univ}} =\n", - "\\int_{\\mathrm{Univ}} p(m_1)\\, f_{\\mathrm{bin}}(m_1)\\, \\mathrm{d}m_1\n", - "=\n", - "\\alpha \\sum_i f_i \\int_{a_i}^{b_i} m_1^{-2.3}\\, \\mathrm{d}m_1,\n", - "$$\n", - "\n", - "(this is the IMF– and binary-fraction–weighted number of binaries)\n", - "where: $f_i$ is the binary fraction in mass bin $i$, $[a_i,b_i]$ is the overlap of the $i$-th binary-fraction bin with the universal mass range.\n", - "\n", - "Evaluating the integral:\n", - "\n", - "$$\n", - "\\textcolor{green}{N_{\\rm binaries\\ in \\ Univ}} = \n", - "\\alpha \\sum_i f_i \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{a_i}^{b_i}.\n", - "$$\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally \n", - "\n", - "$$\n", - "f_{\\mathrm{int}} =\n", - "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} = \n", - "\\frac{\\alpha \\sum_i f_i \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{a_i}^{b_i}}{\n", - "\\alpha \\sum_i f_i\n", - "\\left( \n", - " \\left[ \\frac{m_1^{-1.3}}{-1.3} \\right]_{A_i}^{B_i} - m_{2,\\min}\n", - " \\left[ \\frac{m_1^{-2.3}}{-2.3} \\right]_{A_i}^{B_i}\n", - "\\right). \n", - "}\n", - "\n", - "$$\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "*** \n", - "^^ !! I AM HERE !! ^^\n", - "\n", - "which, if we normalized the functions, is just 1 over the integral in the COMPAS range:\n", - "\n", - "$$\n", - " p(m2|m1) = \\int_{m2, min COMPAS}^{min(1, m1 max COMPAS)} \\int_{m1, min COMAPS}^{m1, max COMPAS} P(m1) P(q) dm1 dq\n", - "$$\n", - "\n", - "where we have used that $q \\equiv m2/m1$.\n", - "\n", - "If we also enforce/assume that $a > 0.5M_{\\odot}$ (i.e. $a > \\rm \\tt{imf\\_mass\\_bounds[2]}$),\n", - "we get:\n", - "$$\n", - "\\begin{align}\n", - "\\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} & = \\alpha \\int_{a}^{b} m1^{-2.3} (1- \\frac{c}{m1}) dm1 = \\alpha \\int_{a}^{b} m_1^{-2.3} - c m_1^{-3.3} dm_1 \\\\\n", - "& \\alpha \\Bigg[ \\frac{-1}{1.3} m_1^{-1.3} \\Bigg]_{a}^{b} + \\alpha \\Bigg[ \\frac{c}{2.3} m_1^{-2.3} \\Bigg]_{a}^{b}\n", - "\\end{align}\n", - "$$\n", - "\n", - "with $\\alpha$ the normalizing constant for the Kroupa IMF, $a = \\rm min(m1_{COMPAS})$, $b = \\rm max(m1_{COMPAS})$, and $c = \\rm min(m2_{COMPAS})$. \n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**This gets us the following line of code:**\n", - "\n", - "\n", - "``` python\n", - "# fraction of binaries that COMPAS simulates (i.e., N_binaries_in_COMPAS/N_binaries_in_universe) \n", - "# second term assumes a flat mass ratio distribution with m2_max = m1_max\n", - "fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3))\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the second term is just 1 over the binary fraction, which is now a function of $m_1$\n", - "\n", - "$$\n", - " \\textcolor{orange}{\\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}} = \\frac{1}{f_b(m_1)}\n", - "$$\n", - "\n", - "well get back to this (it will be incorporated in the sum of the third term)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "Lastly the third term represents the average (or expected) mass of a stellar system (binaries and singles) in the Universe. \n", - "i.e., the average mass of single stars (weighted by the single fraction) plus the average mass of binary systems (weighted by binary fraction fb)\n", - "\n", - "$$\n", - "\\begin{align}\n", - "\\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } & = \\left< (1 - f_b(m_1)) m_s \\right> + \\left< f_(b) m_{binary} \\right> \\\\\n", - "& = \\left< (1 - f_b(m_1)) \\cdot m_1 \\right> + \\left< f_b(m_1) \\cdot m_{1}(1 + q) \\right> \\\\\n", - "& = \\left< (1 - f_b(m_1)) \\cdot m_1 \\right> + \\left< f_b(m_1) m_{1} \\right> \\left< (1 + q) \\right>\n", - "\\end{align}\n", - "$$\n", - "(we can split this up because $m1$ and $q$ are independent)\n", - "\n", - "$$\n", - "\\left< (1 + q) \\right> = 1 + \\int_0^1 q P(q)dq = 1 + \\frac{1}{2} q^2 \\big]_0^1 = 1.5\n", - "$$\n", - "\n", - "and since averages are linear, we can combine the above to\n", - "\n", - "$$\n", - "\\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } \n", - "= \\left< (1 + 0.5 f_b(m_1)) m_1 \\right>\n", - "$$\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "BUT, we shouldn't forget the $1/f_b$ term from above! Bringing that back in we have: \n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\textcolor{orange}{\\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}} \\times \\textcolor{blue}{ \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } & = \\left< \\frac{1 + 0.5 f_b(m_1)}{f_b(m_1)} m_1 \\right>\n", - " = \\left< \\left(\\frac{1}{f_b(m_1)} + 0.5 \\right) m_1 \\right> \\\\\n", - "& \\int_{A}^{B} \\left(\\frac{1}{f_b(m_1)} + 0.5 \\right) m_1 P(m1) dm_1 \\\\\n", - "\\end{align}\n", - "$$\n", - "\n", - "Where $A$ and $B$ are now the minimum and maximum values of $m_1$ in our Universe, \n", - "(that is $\\tt{imf\\_mass\\_bounds[0]}$, and $\\tt{imf\\_mass\\_bounds[3]}$), \n", - "\n", - "Now, we adopt\n", - "$$\n", - "f_b(m_1) =\n", - "\\begin{cases}\n", - "0.1 & \\text{for } 0.01 < m_1 \\leq 0.08, \\\\\n", - "0.225 & \\text{for } 0.08 < m_1 < 0.5, \\\\\n", - "0.5 & \\text{for } 0.5 < m_1 \\leq 1, \\\\\n", - "0.80 & \\text{for } 1 < m_1 \\leq 10, \\\\\n", - "1 & \\text{for } 10 < m_1 \\leq 200.\n", - "\\end{cases}\n", - "$$\n", - "chosen to approximately follow Figure 1 from Offner et al. (2023).\n", - "This is piecewise constant and discontinuous, so we can break the integral into parts over the intervals where f(x) is constant.\n", - "This means we have some bookkeeping to do, because the $P(m_1)$ is also a piecewise function in $m_1$, so we have to evaluate each piece for both bin ranges:\n", - "\n", - "$$\n", - "\\int_{A}^{B} \\left( \\frac{1}{f(m_1)} + 0.5 \\right) m_1 P(m_1) dm_1\n", - "= \\sum_{i} \\left( \\frac{1}{f_{b,i}(m_1)} + 0.5 \\right) \\int_{A}^{B} m_1 P(m_1) \\, dm_1\n", - "$$\n", - "\n", - "\n", - "$$\n", - "\\sum_{i} \\left( \\frac{1}{f_{b,i}} + 0.5 \\right) \n", - "\\sum_{j} \\int_{m_j}^{m_{j+1}} m_1 P(m_1) \\, dm_1\n", - "$$\n", - "\n", - "Also, keep in mind that the Kroupa IMF needs some normalizing constants to ensure continuity\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**This leads to the below bulk of code**\n", - "\n", - "``` python\n", - "# Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", - "# N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction \n", - "# fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023)\n", - "binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] \n", - "if fbin == None:\n", - " # use a binary fraction that varies with mass\n", - " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] \n", - "else:\n", - " # otherwise use a constant binary fraction\n", - " binaryFractions = [fbin] * 5\n", - "\n", - "# M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe,\n", - "# we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to:\n", - "# int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. \n", - "# This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins.\n", - "piece_wise_integral = 0\n", - "\n", - "# For every binary fraction bin\n", - "for i in range(len(binary_bin_edges) - 1):\n", - " fbin = binaryFractions[i] # Binary fraction for this range\n", - "\n", - " # And every piece of the Kroupa IMF\n", - " for j in range(len(imf_mass_bounds) - 1):\n", - " exponent = IMF_powers[j] # IMF exponent for these masses\n", - "\n", - " # Check if the binary fraction bin overlaps with the IMF mass bin\n", - " if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]:\n", - " continue # No overlap\n", - "\n", - " # Integrate from the most narrow range\n", - " m_start = max(binary_bin_edges[i], imf_mass_bounds[j])\n", - " m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1])\n", - "\n", - " # Compute the definite integral:\n", - " integral = ( m_end**(exponent + 2) - m_start**(exponent + 2) ) / (exponent + 2) * continuity_constants[j]\n", - "\n", - " # Compute the sum term\n", - " sum_term = (1 /fbin + 0.5) * integral\n", - " piece_wise_integral += sum_term\n", - "\n", - "# combining them:\n", - "Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The final analytical function becomes: \n", - "\n", - "$$\n", - "\\begin{align}\n", - "\\frac{M_{\\rm stellar\\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}} & =\\\\\n", - " & \\textcolor{green}{\\frac{N_{\\rm binaries\\ in \\ Univ}}{N_{\\rm binaries \\ in \\ COMPAS}}} \n", - " \\textcolor{orange}{\\times \\frac{N_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm binaries \\ in \\ Univ}}}\n", - " \\textcolor{blue}{\\times \\frac{M_{\\rm stellar \\ sys \\ Univ}}{N_{\\rm stellar \\ sys \\ Univ}} } \\\\\n", - "& = \\left( \\alpha \\Bigg[ \\frac{-1}{1.3} m_1^{-1.3} \\Bigg]_{a}^{b} + \\alpha \\Bigg[ \\frac{c}{2.3} m_1^{-2.3} \\Bigg]_{a}^{b} \\right)\n", - "\\times \n", - "\\alpha \\left( \\sum_{i} \\left( \\frac{1}{f_{b,i}} + 0.5 \\right) \n", - "\\sum_{j} \\int_{m_j}^{m_{j+1}} m_1 P(m_1) \\, dm_1 \\right)\n", - "\\end{align} \n", - "$$\n", - "\n", - "with $\\alpha$ the normalizing constant for the Kroupa IMF, $a = \\rm min(m1_{COMPAS})$, $b = \\rm max(m1_{COMPAS})$, and $c = \\rm min(m2_{COMPAS})$. \n", - "Note that the second term is integrated over the full range of $m_1$ that span the Universe " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "###################################################\n", - "# New version of analytical calculation\n", - "###################################################\n", - "def analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200]\n", - "):\n", - " \"\"\"\n", - " Analytical computation of the mass of stars formed per binary star formed within the\n", - " [m1 min, m1 max] and [m2 min, ..] rage,\n", - " using the Kroupa IMF:\n", - "\n", - " p(M) \\propto M^-0.3 for M between m1 and m2\n", - " p(M) \\propto M^-1.3 for M between m2 and m3;\n", - " p(M) = alpha * M^-2.3 for M between m3 and m4;\n", - "\n", - " m1_min, m1_max are the min and max sampled primary masses\n", - " m2_min is the min sampled secondary mass\n", - "\n", - " This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max\n", - " Lieke base on Ilya Mandel's derivation\n", - " \"\"\"\n", - " #########\n", - " # Kroupa IMF \n", - " m1, m2, m3, m4 = imf_mass_bounds\n", - " continuity_constants = [1./(m2*m3), 1./(m3), 1.0] \n", - " IMF_powers = [-0.3, -1.3, -2.3] \n", - "\n", - " if m1_min < m3:\n", - " raise ValueError(f\"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})\")\n", - " if m1_min > m1_max:\n", - " raise ValueError(f\"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})\")\n", - " if m1_max > m4:\n", - " raise ValueError(f\"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})\")\n", - " \n", - " # normalize IMF over the complete mass range:\n", - " alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1)\n", - " # print('alpha', alpha)\n", - "\n", - " #########\n", - " # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023)\n", - " binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] \n", - " if fbin == None:\n", - " # use a binary fraction that varies with mass\n", - " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] \n", - " else:\n", - " # otherwise use a constant binary fraction\n", - " binaryFractions = [fbin] * 5\n", - "\n", - " ##################\n", - " # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", - " # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", - " def N_binaries_kroupa_fbin(m1_low, m1_high, m2_min):\n", - " \"\"\"\n", - " Computes:\n", - " N = alpha * Σ_i Σ_j binaryFractions[i] *\n", - " ∫ ( m^{IMF_powers[j]} - m2_min * m^{IMF_powers[j]-1} )\n", - " * continuity_constants[j] dm\n", - "\n", - " The integral is taken over the overlap of:\n", - " - binary-fraction bin i,\n", - " - IMF segment j,\n", - " - [m1_low, m1_high].\n", - " \"\"\"\n", - " if m1_high <= m1_low:\n", - " raise ValueError(\"Require m1_high > m1_low\")\n", - "\n", - " total = 0.0\n", - " #Compute double piecewise integral\n", - " for i in range(len(binaryFractions)):\n", - " # overlap of binary-fraction bin with [m1_low, m1_high]\n", - " bin_lo = max(binary_bin_edges[i], m1_low)\n", - " bin_hi = min(binary_bin_edges[i + 1], m1_high)\n", - "\n", - " if bin_hi <= bin_lo:\n", - " continue\n", - " # split across IMF segments\n", - " for j in range(len(IMF_powers)):\n", - " m_start = max(bin_lo, imf_mass_bounds[j])\n", - " m_end = min(bin_hi, imf_mass_bounds[j + 1])\n", - "\n", - " if m_end <= m_start:\n", - " continue\n", - "\n", - " # ∫ m^{IMF_powers[j]} dm\n", - " integral_main = (\n", - " m_end**(IMF_powers[j] + 1) - m_start**(IMF_powers[j] + 1)\n", - " ) / (IMF_powers[j] + 1)\n", - "\n", - " # ∫ m^{IMF_powers[j]-1} dm (only if m2_min > 0)\n", - " if m2_min > 0.0:\n", - " integral_m2 = (\n", - " m_end**(IMF_powers[j]) - m_start**(IMF_powers[j])\n", - " ) / (IMF_powers[j])\n", - " else:\n", - " integral_m2 = 0.0\n", - "\n", - " total += binaryFractions[i] * continuity_constants[j] * (integral_main - m2_min * integral_m2)\n", - "\n", - " return alpha * total\n", - "\n", - " # Integral for COMPAS sampled binaries\n", - " N_compas = N_binaries_kroupa_fbin(m1_low=m1_min, m1_high=m1_max, m2_min=m2_min)\n", - "\n", - " ##################\n", - " # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", - " # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction \n", - "\n", - " # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe,\n", - " # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to:\n", - " # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. \n", - " # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins.\n", - " average_mass_stellar_sys_int = 0\n", - "\n", - " # For every binary fraction bin\n", - " for i in range(len(binary_bin_edges) - 1):\n", - " # And every piece of the Kroupa IMF\n", - " for j in range(len(imf_mass_bounds) - 1):\n", - " # Check if the binary fraction bin overlaps with the IMF mass bin\n", - " if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]:\n", - " continue # No overlap\n", - "\n", - " # Integrate from the most narrow range\n", - " m_start = max(binary_bin_edges[i], imf_mass_bounds[j])\n", - " m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1])\n", - "\n", - " # Compute the definite integral:\n", - " integral = (m_end**(IMF_powers[j] + 2) - m_start**(IMF_powers[j] + 2) ) / (IMF_powers[j] + 2) * continuity_constants[j]\n", - "\n", - " # Compute the sum term\n", - " average_mass_stellar_sys_int += (1 + 0.5 * binaryFractions[i]) * integral \n", - "\n", - " # Addint normalization\n", - " average_mass_stellar_sys = alpha * average_mass_stellar_sys_int\n", - "\n", - " # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", - " M_sf_Univ_per_N_binary_COMPAS = (1/N_compas) * average_mass_stellar_sys\n", - "\n", - " return analytical_results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "###################################################\n", - "# New version of analytical calculation\n", - "###################################################\n", - "def analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=1., imf_mass_bounds=[0.01,0.08,0.5,200]\n", - "):\n", - " \"\"\"\n", - " Analytical computation of the mass of stars formed per binary star formed within the\n", - " [m1 min, m1 max] and [m2 min, ..] rage,\n", - " using the Kroupa IMF:\n", - "\n", - " p(M) \\propto M^-0.3 for M between m1 and m2\n", - " p(M) \\propto M^-1.3 for M between m2 and m3;\n", - " p(M) = alpha * M^-2.3 for M between m3 and m4;\n", - "\n", - " m1_min, m1_max are the min and max sampled primary masses\n", - " m2_min is the min sampled secondary mass\n", - "\n", - " This function further assumes a flat mass ratio distribution with qmin = m2_min/m1, and m2_max = m1_max\n", - " Lieke base on Ilya Mandel's derivation\n", - " \"\"\"\n", - " #########\n", - " # Kroupa IMF \n", - " m1, m2, m3, m4 = imf_mass_bounds\n", - " continuity_constants = [1./(m2*m3), 1./(m3), 1.0] \n", - " IMF_powers = [-0.3, -1.3, -2.3] \n", - "\n", - " if m1_min < m3:\n", - " raise ValueError(f\"This analytical derivation requires IMF break m3 < m1_min ({m3} !< {m1_min})\")\n", - " if m1_min > m1_max:\n", - " raise ValueError(f\"Minimum sampled primary mass cannot be above maximum sampled primary mass: m1_min ({m1_min} !< m1_max {m1_max})\")\n", - " if m1_max > m4:\n", - " raise ValueError(f\"Maximum sampled primary mass cannot be above maximum mass of Kroupa IMF: m1_max ({m1_max} !< m4 {m4})\")\n", - " \n", - " # normalize IMF over the complete mass range:\n", - " alpha = (-(m4**(-1.3)-m3**(-1.3))/1.3 - (m3**(-0.3)-m2**(-0.3))/(m3*0.3) + (m2**0.7-m1**0.7)/(m2*m3*0.7))**(-1)\n", - " # print('alpha', alpha)\n", - "\n", - " #########\n", - " # fbin edges and values are chosen to approximately follow Figure 1 from Offner et al. (2023)\n", - " binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] \n", - " if fbin == None:\n", - " # use a binary fraction that varies with mass\n", - " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] \n", - " else:\n", - " # otherwise use a constant binary fraction\n", - " binaryFractions = [fbin] * 5\n", - "\n", - " ##################\n", - " # we want to compute M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", - " # = N_binaries_in_universe/N_binaries_in_COMPAS * N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", - "\n", - " ### !! PRESUMABLY ERROR IS IN HERE !! ###\n", - " # fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates\n", - " # fint = -alpha / 1.3 * (m1_max ** (-1.3) - m1_min ** (-1.3)) + alpha * m2_min / 2.3 * (m1_max ** (-2.3) - m1_min ** (-2.3))\n", - "\n", - " def N_binaries_kroupa_fbin(m1_low, m1_high, m2_min):\n", - " \"\"\"\n", - " Computes:\n", - " N = alpha * Σ_i Σ_j binaryFractions[i] *\n", - " ∫ ( m^{IMF_powers[j]} - m2_min * m^{IMF_powers[j]-1} )\n", - " * continuity_constants[j] dm\n", - "\n", - " The integral is taken over the overlap of:\n", - " - binary-fraction bin i,\n", - " - IMF segment j,\n", - " - [m1_low, m1_high].\n", - " \"\"\"\n", - " if m1_high <= m1_low:\n", - " raise ValueError(\"Require m1_high > m1_low\")\n", - "\n", - " total = 0.0\n", - " #Compute double piecewise integral\n", - " for i in range(len(binaryFractions)):\n", - " # overlap of binary-fraction bin with [m1_low, m1_high]\n", - " bin_lo = max(binary_bin_edges[i], m1_low)\n", - " bin_hi = min(binary_bin_edges[i + 1], m1_high)\n", - "\n", - " if bin_hi <= bin_lo:\n", - " continue\n", - " # split across IMF segments\n", - " for j in range(len(IMF_powers)):\n", - " m_start = max(bin_lo, imf_mass_bounds[j])\n", - " m_end = min(bin_hi, imf_mass_bounds[j + 1])\n", - "\n", - " if m_end <= m_start:\n", - " continue\n", - "\n", - " # ∫ m^{IMF_powers[j]} dm\n", - " integral_main = (\n", - " m_end**(IMF_powers[j] + 1) - m_start**(IMF_powers[j] + 1)\n", - " ) / (IMF_powers[j] + 1)\n", - "\n", - " # ∫ m^{IMF_powers[j]-1} dm (only if m2_min > 0)\n", - " if m2_min > 0.0:\n", - " integral_m2 = (\n", - " m_end**(IMF_powers[j]) - m_start**(IMF_powers[j])\n", - " ) / (IMF_powers[j])\n", - " else:\n", - " integral_m2 = 0.0\n", - "\n", - " total += binaryFractions[i] * continuity_constants[j] * (integral_main - m2_min * integral_m2)\n", - "\n", - " return alpha * total\n", - "\n", - " # Integral for the full universe, has IMF bound limits, and m2_min = 0\n", - " N_univ = N_binaries_kroupa_fbin(m1_low=m1, m1_high=m4, m2_min=0.0)\n", - " # Integral for COMPAS sampled binaries\n", - " N_compas = N_binaries_kroupa_fbin(m1_low=m1_min, m1_high=m1_max, m2_min=m2_min)\n", - "\n", - " fint = N_compas / N_univ\n", - "\n", - " ##################\n", - " # Next for N_stellar_sys_in_universe/N_binaries_in_universe * M_stellar_sys_in_universe/N_stellar_sys_in_universe\n", - " # N_stellar_sys_in_universe/N_binaries_in_universe = the binary fraction \n", - "\n", - " # M_stellar_sys_in_universe/N_stellar_sys_in_universe = average mass of a stellar system in the Universe,\n", - " # we are computing 1/fbin * M_stellar_sys_in_universe/N_stellar_sys_in_universe, skipping steps this leads to:\n", - " # int_A^B (1/fb(m1) + 0.5) m1 P(m1) dm1. \n", - " # This is a double piecewise integral, i.e. pieces over the binary fraction bins and IMF mass bins.\n", - " piece_wise_integral = 0\n", - " average_mass_stellar_sys_int = 0\n", - "\n", - " # For every binary fraction bin\n", - " for i in range(len(binary_bin_edges) - 1):\n", - " # And every piece of the Kroupa IMF\n", - " for j in range(len(imf_mass_bounds) - 1):\n", - " # Check if the binary fraction bin overlaps with the IMF mass bin\n", - " if binary_bin_edges[i + 1] <= imf_mass_bounds[j] or binary_bin_edges[i] >= imf_mass_bounds[j + 1]:\n", - " continue # No overlap\n", - "\n", - " # Integrate from the most narrow range\n", - " m_start = max(binary_bin_edges[i], imf_mass_bounds[j])\n", - " m_end = min(binary_bin_edges[i + 1], imf_mass_bounds[j + 1])\n", - "\n", - " # Compute the definite integral:\n", - " integral = (m_end**(IMF_powers[j] + 2) - m_start**(IMF_powers[j] + 2) ) / (IMF_powers[j] + 2) * continuity_constants[j]\n", - "\n", - " # Compute the sum term\n", - " piece_wise_integral += (1 /binaryFractions[i] + 0.5) * integral\n", - " average_mass_stellar_sys_int += (1 + 0.5 * binaryFractions[i]) * integral \n", - "\n", - " # combining them:\n", - " Average_mass_stellar_sys_per_fbin = alpha * piece_wise_integral\n", - "\n", - " average_mass_stellar_sys = alpha * average_mass_stellar_sys_int\n", - "\n", - " # Now compute the average mass per binary in COMPAS M_stellar_sys_in_universe / N_binaries_in_COMPAS\n", - " M_sf_Univ_per_N_binary_COMPAS = (1/fint) * Average_mass_stellar_sys_per_fbin\n", - "\n", - "\n", - " analytical_results = {\n", - " 'M_sf_Univ_per_N_binary_COMPAS': M_sf_Univ_per_N_binary_COMPAS, \n", - " 'N_compas': N_compas,\n", - " 'N_univ': N_univ,\n", - " 'fint': fint,\n", - " 'Average_mass_stellar_sys_per_fbin': Average_mass_stellar_sys_per_fbin,\n", - " 'average_mass_stellar_sys': average_mass_stellar_sys ,\n", - " 'alpha': alpha,\n", - " 'binaryFractions': binaryFractions,\n", - " 'binary_bin_edges': binary_bin_edges,\n", - " 'imf_mass_bounds': imf_mass_bounds,\n", - " }\n", - " return analytical_results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Let's test that they lead to the same answer:" - ] - }, - { - "cell_type": "code", - "execution_count": 204, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " old analytical m_rep =253.53899506390485, new analytical m_rep = 253.53899506390468, so well get an change of', 1.0000000000000007\n" - ] - } - ], - "source": [ - "m1_min = 10\n", - "m1_max = 150\n", - "m2_min = 0.1 \n", - "fbin= 0.7\n", - "\n", - "old_analytical = OLD_analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", - "\n", - "new_analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", - "\n", - "\n", - "# print(old_analytical.keys())\n", - "# print(new_analytical.keys())\n", - "print(f\" old analytical m_rep ={old_analytical['m_rep']}, \\\n", - " new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']},\\\n", - " so well get an change of', {old_analytical['m_rep']/new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\")\n", - "\n", - "# Pretty much the same! " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Now lastly we want to test if a Numerical MC sampler also agreed with this\n", - "\n", - "\n", - "### CREATE A MOCK UNIVERSE\n", - "\n", - "- Step 1: Sample N primary masses from the Kroupa IMF\n", - "\n", - "- Step 2: Sample binaries based on a variable binary fraction\n", - "\n", - " > Sum the total mass of stellar systems in the mock universe (= all m1 + m2)\n", - "\n", - "\n", - "- Step 3: figure out which of your systems would be part of your COMPAS simulation\n", - " (I.e. apply m1_min - m1_max, m2_min, and only keep binary systems )\n", - "\n", - "> Count the number of binaries that would be in the COMPAS simulation\n", - "\n", - "\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 205, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def kroupa_imf_sample(n_samples, m1_min=0.01, m1_max=200):\n", - " \"\"\"\n", - " Sample from Kroupa IMF with mass bounds [0.01, 0.08, 0.5, 200]\n", - " using inverse CDF sampling.\n", - "\n", - " IMF convention:\n", - " dN/dm ~ m^alpha\n", - " \"\"\"\n", - "\n", - " # Kroupa IMF mass bounds\n", - " m1, m2, m3, m4 = 0.01, 0.08, 0.5, 200\n", - "\n", - " # IMF slopes\n", - " alpha1, alpha2, alpha3 = -0.3, -1.3, -2.3\n", - "\n", - " # Enforce truncation\n", - " m_min = max(m1_min, m1)\n", - " m_max = min(m1_max, m4)\n", - " if m_max <= m_min:\n", - " raise ValueError(\"Invalid mass bounds\")\n", - "\n", - " # Continuity factors (ensure dN/dm is continuous)\n", - " k1 = 1.0\n", - " k2 = k1 * m2**(alpha1 - alpha2)\n", - " k3 = k2 * m3**(alpha2 - alpha3)\n", - "\n", - " # Integral of k * m^alpha over a segment\n", - " def segment_integral(k, alpha, lo, hi):\n", - " if hi <= lo:\n", - " return 0.0\n", - " return k * (hi**(alpha + 1) - lo**(alpha + 1)) / (alpha + 1)\n", - "\n", - " # Segment limits after truncation\n", - " seg1_lo, seg1_hi = m_min, min(m2, m_max)\n", - " seg2_lo, seg2_hi = max(m_min, m2), min(m3, m_max)\n", - " seg3_lo, seg3_hi = max(m_min, m3), m_max\n", - "\n", - " # Relative weights of each segment\n", - " I1 = segment_integral(k1, alpha1, seg1_lo, seg1_hi)\n", - " I2 = segment_integral(k2, alpha2, seg2_lo, seg2_hi)\n", - " I3 = segment_integral(k3, alpha3, seg3_lo, seg3_hi)\n", - " Itot = I1 + I2 + I3\n", - "\n", - " p1, p2, p3 = I1 / Itot, I2 / Itot, I3 / Itot\n", - "\n", - " # Number of samples per segment\n", - " n1, n2, n3 = np.random.multinomial(n_samples, [p1, p2, p3])\n", - "\n", - " masses = np.zeros(n_samples)\n", - " idx = 0\n", - "\n", - " # Inverse CDF for power-law IMF segment\n", - " def inverse_cdf(alpha, lo, hi, u):\n", - " return (u * (hi**(alpha + 1) - lo**(alpha + 1)) + lo**(alpha + 1))**(1 / (alpha + 1))\n", - "\n", - " # Segment 1: 0.01 – 0.08 Msun\n", - " if n1 > 0:\n", - " u1 = np.random.uniform(0, 1, n1)\n", - " masses[idx:idx+n1] = inverse_cdf(alpha1, seg1_lo, seg1_hi, u1)\n", - " idx += n1\n", - "\n", - " # Segment 2: 0.08 – 0.5 Msun\n", - " if n2 > 0:\n", - " u2 = np.random.uniform(0, 1, n2)\n", - " masses[idx:idx+n2] = inverse_cdf(alpha2, seg2_lo, seg2_hi, u2)\n", - " idx += n2\n", - "\n", - " # Segment 3: 0.5 – 200 Msun\n", - " if n3 > 0:\n", - " u3 = np.random.uniform(0, 1, n3)\n", - " masses[idx:idx+n3] = inverse_cdf(alpha3, seg3_lo, seg3_hi, u3)\n", - " idx += n3\n", - "\n", - " np.random.shuffle(masses)\n", - " return masses\n", - "\n", - "\n", - "\n", - "primary_masses = kroupa_imf_sample(int(5e6), m1_min=0.01, m1_max=200)\n", - "log_masses = np.log10(primary_masses)\n", - "plt.hist( log_masses)\n", - "plt.xlabel('log masses')\n", - "plt.ylabel('count')\n", - "plt.yscale('log')\n", - "plt.show()\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 206, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "using mass-dependent binaryFractions from Offner et al. (2023)\n" - ] - } - ], - "source": [ - "\n", - "def get_binary_fraction(m1, binaryFractions=None, binary_bin_edges=None):\n", - " \"\"\"\n", - " Get binary fraction for a given primary mass.\n", - "\n", - " - If `binaryFractions` is a scalar (float/int), return a constant fraction across all masses.\n", - " - If `binaryFractions` is None or a list, use mass-dependent binary fractions with the provided or default bin edges.\n", - " \"\"\"\n", - " # If a constant fraction is requested, broadcast it to the shape of m1\n", - " if isinstance(binaryFractions, (int, float, np.floating)):\n", - " print(f'using constant binaryFractions = {binaryFractions}')\n", - " m1_arr = np.asarray(m1)\n", - " if m1_arr.ndim == 0:\n", - " return float(binaryFractions)\n", - " return np.full(m1_arr.shape, float(binaryFractions))\n", - "\n", - " # Default mass-dependent fractions from Offner et al. (2023)\n", - " if binaryFractions is None:\n", - " print('using mass-dependent binaryFractions from Offner et al. (2023)')\n", - " binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0]\n", - " \n", - " # Default bin edges\n", - " if binary_bin_edges is None:\n", - " binary_bin_edges = [0.01, 0.08, 0.5, 1, 10, 200]\n", - " \n", - " # Map masses to bins\n", - " m1_arr = np.asarray(m1)\n", - " bin_index = np.digitize(m1_arr, binary_bin_edges, right=True) - 1\n", - " bin_index = np.clip(bin_index, 0, len(binaryFractions) - 1)\n", - "\n", - " fb_array = np.array(binaryFractions, dtype=float)[bin_index]\n", - " if m1_arr.ndim == 0:\n", - " return float(fb_array)\n", - " return fb_array\n", - "\n", - "\n", - "fb_for_every_m1 = get_binary_fraction(primary_masses, binaryFractions=None, binary_bin_edges=None)\n", - "\n", - "# print(binary_fractions, bin_indices)\n", - "# print(fb_for_every_m1)" - ] - }, - { - "cell_type": "code", - "execution_count": 207, - "metadata": {}, - "outputs": [], - "source": [ - "def create_mock_universe(n_primary=10**7, m1_min=10, m1_max=150, m2_min=0.1, \n", - " binaryFractions=None, binary_bin_edges=None, verbose = False):\n", - " \"\"\"\n", - " Create a mock universe with mass-dependent binary fractions\n", - " \n", - " Parameters:\n", - " -----------\n", - " n_primary : int\n", - " Number of primary stars to sample\n", - " m1_min, m1_max : float\n", - " Min and max primary masses for COMPAS simulation\n", - " m2_min : float\n", - " Minimum secondary mass\n", - " binaryFractions : list\n", - " Binary fractions for each mass bin\n", - " binary_bin_edges : list\n", - " Mass bin edges for binary fractions\n", - " \n", - " Returns:\n", - " --------\n", - " dict : Dictionary containing simulation results\n", - " \"\"\"\n", - " \n", - " if verbose: print(\"Step 1: Sampling primary masses from Kroupa IMF...\")\n", - " # Step 1: Sample primary masses from Kroupa IMF\n", - " primary_masses = kroupa_imf_sample(n_primary)\n", - " \n", - " if verbose: print(f\"Sampled {len(primary_masses)} primary masses\")\n", - " if verbose: print(f\"Mass range: {primary_masses.min():.3f} - {primary_masses.max():.3f} M_sun\")\n", - " \n", - " if verbose: print(\"\\nStep 2: Sampling binaries based on mass-dependent binary fraction...\")\n", - " # Step 2: Sample binaries based on mass-dependent binary fraction\n", - " fb_for_every_m1 = get_binary_fraction(primary_masses, binaryFractions, binary_bin_edges)\n", - " \n", - " # Determine which stars are in binaries\n", - " binary_mask = np.random.random(len(primary_masses)) < fb_for_every_m1\n", - " \n", - " # For binary systems, sample secondary masses (flat mass ratio distribution)\n", - " secondary_masses = np.zeros(len(primary_masses))\n", - " binary_systems = []\n", - " \n", - " for i, (m1, is_binary) in enumerate(zip(primary_masses, binary_mask)):\n", - " if is_binary:\n", - " # Sample mass ratio q = m2/m1 from uniform distribution [0, 1]\n", - " q = np.random.uniform(0, 1)\n", - " m2 = q * m1\n", - " secondary_masses[i] = m2\n", - " binary_systems.append((m1, m2))\n", - " \n", - " if verbose: print(f\"Created {len(binary_systems)} binary systems\")\n", - " if verbose: print(f\"Binary fraction varies from {np.min(fb_for_every_m1)} to {np.max(fb_for_every_m1)}\")\n", - " \n", - " if verbose: print(\"\\nStep 3: Calculating total mass of stellar systems...\")\n", - " # Step 3: Sum total mass of stellar systems\n", - " total_mass_singles = np.sum(primary_masses[~binary_mask])\n", - " total_mass_binaries = np.sum(primary_masses[binary_mask] + secondary_masses[binary_mask])\n", - " total_mass_universe = total_mass_singles + total_mass_binaries\n", - " \n", - " if verbose: print(f\"Total mass in single stars: {total_mass_singles:.2e} M_sun\")\n", - " if verbose: print(f\"Total mass in binary systems: {total_mass_binaries:.2e} M_sun\")\n", - " if verbose: print(f\"Total mass in universe: {total_mass_universe:.2e} M_sun\")\n", - " \n", - " if verbose: print(\"\\nStep 4: Identifying systems for COMPAS simulation...\")\n", - " # Step 4: Figure out which systems would be part of COMPAS simulation\n", - " compas_mask = ((primary_masses >= m1_min) & \n", - " (primary_masses <= m1_max) & \n", - " (binary_mask) & # Only binary systems\n", - " (secondary_masses >= m2_min))\n", - " \n", - " compas_systems = []\n", - " for i, (m1, m2, in_compas) in enumerate(zip(primary_masses, secondary_masses, compas_mask)):\n", - " if in_compas:\n", - " compas_systems.append((m1, m2))\n", - " \n", - " if verbose: print(f\"Number of binaries in COMPAS simulation: {len(compas_systems)}\")\n", - " if verbose: print(f\"COMPAS mass range: {m1_min} - {m1_max} M_sun (primary), >= {m2_min} M_sun (secondary)\")\n", - " \n", - " # Calculate statistics\n", - " results = {\n", - " 'n_primary': len(primary_masses),\n", - " 'n_binaries': len(binary_systems),\n", - " 'n_compas': len(compas_systems),\n", - " 'total_mass_universe': total_mass_universe,\n", - " 'total_mass_singles': total_mass_singles,\n", - " 'total_mass_binaries': total_mass_binaries,\n", - " 'primary_masses': primary_masses,\n", - " 'secondary_masses': secondary_masses,\n", - " 'binary_mask': binary_mask,\n", - " 'compas_mask': compas_mask,\n", - " 'binary_systems': binary_systems,\n", - " 'compas_systems': compas_systems,\n", - " 'fb_for_every_m1': fb_for_every_m1\n", - " }\n", - " \n", - " return results\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Compare analytical to MC\n", - "\n", - "### for a fixed value of fbin" - ] - }, - { - "cell_type": "code", - "execution_count": 208, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " old analytical m_rep =253.53899506390485, new analytical m_rep = 253.53899506390468\n", - "using constant binaryFractions = 0.7\n", - "MC_results m_rep = 255.03275505787337\n", - "********** fint = N_binaries_in_COMPAS/N_binaries_in_universe: 0.0029561960422894995, MC nbin_compas/nbin: 0.0029376568693338452\n" - ] - } - ], - "source": [ - "m1_min = 10\n", - "m1_max = 150\n", - "m2_min = 0.1\n", - "fbin = 0.7\n", - "\n", - "# Analytical calculation\n", - "old_analytical = OLD_analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", - "\n", - "new_analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", - "\n", - "print(f\" old analytical m_rep ={old_analytical['m_rep']}, \\\n", - " new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\" )\n", - "\n", - "# MCMC simulation\n", - "MC_results = create_mock_universe( n_primary=int(5e6), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", - "\n", - "print('MC_results m_rep = ', MC_results['total_mass_universe']/MC_results['n_compas'] )\n", - "\n", - "\n", - "print(f\"{10*'*'} fint = N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']},\\\n", - " MC nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "***\n", - "\n", - "They agree!!\n", - "\n", - "***\n", - "\n", - "## Now compare MC to new analytical \n", - "\n", - "### (both with variable fbin)" - ] - }, - { - "cell_type": "code", - "execution_count": 219, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " new analytical m_rep = 162.56300080093715\n", - "using mass-dependent binaryFractions from Offner et al. (2023)\n", - "MC_results m_rep = 298.13638972568225\n", - "1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] 297.0201523856339\n" - ] - } - ], - "source": [ - "m1_min = 15\n", - "m1_max = 150\n", - "m2_min = 0.1\n", - "fbin = None\n", - "\n", - "# Analytical calculation\n", - "new_analytical = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", - "print(f\" new analytical m_rep = {new_analytical['M_sf_Univ_per_N_binary_COMPAS']}\" )\n", - "\n", - "# MCMC simulation\n", - "MC_results = create_mock_universe( n_primary=int(5e6), m1_min=m1_min, m1_max=m1_max, m2_min=m2_min, binaryFractions=fbin, binary_bin_edges=None) \n", - "\n", - "print('MC_results m_rep = ', MC_results['total_mass_universe']/MC_results['n_compas'] )\n", - "\n", - "print(f\"1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] {1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys']}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 216, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Analytic: N_binaries_in_COMPAS/N_binaries_in_universe: 0.02715404524930118, \n", - " MC: nbin_compas/nbin: 0.02724300246150036\n", - "\n", - " Analytic: Mstellar_sys_univ/Nstellar_sys_univ = 0.5083080461257276\n", - " MC: total_mass_universe/n_primary = 0.5118063519181145\n", - "1/new_analytical['N_compas'] 154.1701265561456 new_analytical['average_mass_stellar_sys'] 0.5083080461257276\n", - "1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] 78.36591580071051\n" - ] - } - ], - "source": [ - "# Sanity checks\n", - "# fint = N_binaries_in_COMPAS/N_binaries_in_universe: fraction of binaries that COMPAS simulates\n", - "print(f\"Analytic: N_binaries_in_COMPAS/N_binaries_in_universe: {new_analytical['fint']}, \\n \\\n", - "MC: nbin_compas/nbin: {MC_results['n_compas']/MC_results['n_binaries']}\" )\n", - "\n", - "print()\n", - "# yellow and blue terms:\n", - "print(f\" Analytic: Mstellar_sys_univ/Nstellar_sys_univ = {new_analytical['average_mass_stellar_sys']}\")\n", - "print(f\" MC: total_mass_universe/n_primary = {MC_results['total_mass_universe']/MC_results['n_primary']}\")\n", - "\n", - "\n", - "print(f\"1/new_analytical['N_compas'] {1/new_analytical['N_compas']} new_analytical['average_mass_stellar_sys'] {new_analytical['average_mass_stellar_sys']}\")\n", - "print(f\"1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys'] {1/new_analytical['N_compas'] * new_analytical['average_mass_stellar_sys']}\")\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 217, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "dict_keys(['n_primary', 'n_binaries', 'n_compas', 'total_mass_universe', 'total_mass_singles', 'total_mass_binaries', 'primary_masses', 'secondary_masses', 'binary_mask', 'compas_mask', 'binary_systems', 'compas_systems', 'fb_for_every_m1'])" - ] - }, - "execution_count": 217, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "MC_results.keys()" - ] - }, - { - "cell_type": "code", - "execution_count": 212, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "dict_keys(['M_sf_Univ_per_N_binary_COMPAS', 'N_compas', 'N_univ', 'fint', 'Average_mass_stellar_sys_per_fbin', 'average_mass_stellar_sys', 'alpha', 'binaryFractions', 'binary_bin_edges', 'imf_mass_bounds'])" - ] - }, - "execution_count": 212, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "new_analytical.keys()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "m1_min = 0.5\n", - "m1_min = 1\n", - "m1_min = 5\n", - "m1_min = 10\n", - "m1_min = 20\n", - "m1_min = 50\n", - "m1_min = 100\n" - ] - } - ], - "source": [ - "m1_min = 5\n", - "m1_max = 200\n", - "m2_min = 0.1 \n", - "fbin = 0.7 #None #0.7\n", - "\n", - "\n", - "binaryFractions = None #0.7#[0.1, 0.225, 0.5, 0.8, 1.0]\n", - "\n", - "\n", - "analytical_results_list = []\n", - "MC_results_list = []\n", - "\n", - "m1_mins = [0.5, 1, 5, 10, 20, 50, 100,]\n", - "for m1_min in m1_mins:\n", - " print(f'm1_min = {m1_min}')\n", - "### Analytical\n", - " M_sf_Univ_per_N_binary_COMPAS = analytical_star_forming_mass_per_binary_using_kroupa_imf(\n", - " m1_min, m1_max, m2_min, fbin=fbin, imf_mass_bounds=[0.01,0.08,0.5,200])\n", - "\n", - "\n", - " ### MCMC\n", - " results = create_mock_universe(n_primary=10**7,m1_min=m1_min,m1_max=m1_max,m2_min=m2_min,\n", - " binaryFractions=binaryFractions,binary_bin_edges=None)\n", - " MCMC_Msf_per_Nbin_COMPAS = results['total_mass_universe']/results['n_compas']\n", - "\n", - " analytical_results_list.append(M_sf_Univ_per_N_binary_COMPAS['M_sf_Univ_per_N_binary_COMPAS'])\n", - " MC_results_list.append(MCMC_Msf_per_Nbin_COMPAS)\n", - "\n", - "# print('\\n', 50*'*')\n", - "# print(f'Analytical M_sf_Univ/N_bin_COMPAS = {M_sf_Univ_per_N_binary_COMPAS}')\n", - "# print(f'MCMC M_sf_Univ/N_bin_COMPAS = {MCMC_Msf_per_Nbin_COMPAS}')\n" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[5.616057836080422, 13.007919822132509, 101.32434416603401, 251.1374670796301, 635.9562719126669, 2376.71231751028, 8224.422379739088]\n", - "[np.float64(5.157970165610914), np.float64(9.36058824727188), np.float64(66.91977886848493), np.float64(145.92929321284583), np.float64(372.72197499297187), np.float64(1406.7721893999285), np.float64(4753.33613920003)]\n" - ] - } - ], - "source": [ - "print(analytical_results_list)\n", - "print(MC_results_list)" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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uHTt2tU5FikmJsbt+/TYcU2fNUy5/VB58kh0dLRbmUqVKv3tNRzXXLvoHC5cuGAert1uAAAgOHl0peo//vhDhgwZImXLls1SeV3EsU6dOmZmmrYO6bgfbX3S8UK+NmrUKNMa5XjoQGwAABCcrjoQnT171qxDpDO44uLiZNmyZabFJyt05ph+j6vrr79e9u3bZ76OjY01z4cOHXIro8eOa/qclJTkdv3SpUtm5plrmYxew/U90hs6dKicOHHC+WCxSQAAgleOA9GqVaukR48eJtToytU6Tmfp0qXm/KBBg7L0GjrDTMfxpG9l0tlgSru5NLAsXrzYretKxwY1atTIHOtzcnKyc+abWrJkiWl90rFGjjI68ywlJcVZRmekVa1aNcPuMpU3b14zjd/1AQAAglO2A5FOi69evbq0b9/ehAkNGps2bZKQkBCzllB29O/f3wQo7TLbuXOnTJ8+3UyF79Wrl7mur9mvXz959dVXzQBsfZ8uXbpI6dKlpW3bts4WpZYtW5qutjVr1phB3b179zYDu7Wcevjhh824JF2fSKfnf/nll/L2229nuSULAAAEuexOXwsLC7Oee+4569KlS27n8+TJY23ZsiXb0+HmzZtn1ahRw0ylr1atmvXhhx/+ber9sGHDrJiYGFNGp/Rv377drczRo0etjh07WgULFrSioqKsbt26WadOnXIrs3HjRjPFX1+jTJky1ujRo7NVT6bdAwAQeLL6+R2i/5PdwcY6ZkgXRuzYsaN07tzZbOiqiytu3Ljxb2OCgoV21engah1PRPcZAADB9fmd7S4zHWys43w+/fRTM2Vdx+nUqlXLbLHBmj4AAMBWg6pvu+02+eSTT+TgwYPy1FNPSd26dc25xo0bm0HWAAAAgSLbXWYOumihTm/XvcUcdNDzxx9/bAZHp58KH+joMgMAIPB4rcvs8OHD0qpVKylYsKB54YYNG5oZYkp3qn/rrbfMDvYAAACBItuBSFei3rBhg9m649///rdZA0invLvSAdYAAABBu5eZLmg4depUszmquvvuu81aQNqFposZAgAABH0L0YEDB8ysMocqVaqYIKSDqwEAAGwzyywsLOxvxzkcmw0AABB4XWYafK677jqzrYbD6dOnzW71oaH/y1e6uSoAAEBQBiJdpRoAAMDWgahr167eqQkAAECgrVQNAABg2xaiypUrZ6ncrl27clIfAAAA/w9Ee/bskQoVKsjDDz8sJUuW9E6tAAAA/DkQffnllzJ58mSzgatu4dG9e3e566673GaYAQAABJJsp5gOHTrIggULzP5lusN9//79pVy5cvLss8/Kjh07vFNLAAAAL8pxs06ZMmXk+eefNyFId7dfvXq1VKtWTY4fP+7ZGgIAAPhbl5mr8+fPy1dffWW60DQQaetR/vz5PVc7AAAAfw1EGn4+/vhjmTlzppl1puOIvv76aylatKjnawgAAOBvgah69eqSlJRkZpktW7bMbaNXAACAQBRiZXNXVp1NVqBAAcmTJ4/bfmbpBdteZidPnpTChQvLiRMnJCoqytfVAQAAHvz8Zi8zAABge+xlBgAAbC/Hs8zOnTsnixYtkj/++MMcV61aVZo1ayaRkZGerB8AAIB/BqK5c+dKjx495MiRI27nixcvbmaftWnTxlP1AwAA8L+FGVeuXCnt27eXW2+9VVasWGEGT+tj+fLl0qRJE3Nt1apV3qktAACAP8wy033LdKuODz74IMPr//rXv2T//v3y/fffSzBhlhkAAMH7+Z3tFiJt/endu3em13v16iXx8fHZfVkAAACfCc3JYOorJSxNYbqlBwAAQNAGoipVqsiSJUsyvb548WJTBgAAIGgDUbdu3WTgwIEZjhH67rvvZPDgwfLoo496qn4AAAD+N+3+6aefNjPN7r77brP20PXXXy86Lnvbtm2yY8cOadu2rfTr1887tQUAAPCHFiLdy2zWrFnyxRdfmED0+++/y/bt26VatWry+eefm13vtQwAAEDQTru3K6bdAwAQeLw27f7AgQNmDJG+QXr6ZoMGDZJDhw5lv8YAAAA+ku1ANG7cOBOGMkpZmsBOnTplygAAAARtIFq4cKF06dIl0+t6bf78+VdbLwAAAP8NRLt375by5ctner1s2bKyZ8+eq60XAACA/waiyMjIKwYevaZlAAAAgjYQNWjQQD799NNMr0+bNk3q169/tfUCAADw34UZdYbZnXfeaQZQ64yymJgYc15nlo0ZM0amTp0qP/74ozfqCgAAgk1aqsjelSKnD4kUjBGp0FgkNCww1iH64IMPzIrVKSkpZrZZSEiImXIfHh4ub775pvTs2VOCDesQAQDgYVvniiwcInLywP/ORZUWafm6SNw9ufr5neOFGRMSEmTmzJmyc+dOs3XHdddd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Tp3M19zrq6f/BnzzzTfO46zc061bt5rvW7t2rbPMggULrJCQECshIcHyNFqIctHFixdl3bp1plnQdUsQPY6Pj/dp3YLNiRMnzHN0dLR51vuurUau975atWpSvnx57n0OaGtc69at3e6n4j57zty5c6VevXrSoUMH0w1cu3ZtmTRpkvP67t27JTEx0e1e6/YE2g3Pvc6exo0by+LFi+WPP/4wxxs3bpTly5dLq1atzDH32vOyck/1WbvJ9L8DBy2vn5vaouRp7GWWi44cOWL6qmNiYtzO6/Hvv//us3oF40a8OqZFuxtq1Khhzul/eBEREeY/rvT3Xq8h62bMmGG6e7XLLD3us+fs2rXLdONoF/tzzz1n7nffvn3N/e3atavzfmb07wn3OnueffZZs7mohvewsDDz7/TIkSNNd43iXnteVu6pPusfA67y5Mlj/tD1xn0nECEoWy82b95s/sKDZ+lO1E8//bTpz9dJAfBusNe/jF977TVzrC1E+v9rHW+hgQieM3PmTPn8889l+vTpUr16ddmwYYP5o0oHA3Ov7YMus1xUvHhx89dH+hk3ehwbG+uzegWT3r17m4F3S5culbJlyzrP6/3VLsvk5GS38tz77NEuMZ0AUKdOHfOXmj504LQOjNSv9a877rNn6MybuLg4t3PXX3+97Nu3z3ztuJ/8e3L1Bg0aZFqJHnroITOTr3PnzmZygM5eVdxrz8vKPdXn9BOOLl26ZGaeeeO+E4hykTZ1161b1/RVu/4VqMeNGjXyad0CnY7Z0zD0zTffyJIlS8z0WVd633W2juu912n5+uHCvc+6pk2byqZNm8xf0I6HtmJo14Lja+6zZ2iXb/qlI3SMS4UKFczX+v9x/VBwvdfa7aNjK7jX2XP27FkzLsWV/vGq/z4r7rXnZeWe6rP+caV/iDnov+/6e9GxRh7n8WHauKIZM2aYUfRTp041I+ifeOIJq0iRIlZiYqKvqxbQevbsaRUuXNj66aefrIMHDzofZ8+edZZ58sknrfLly1tLliyxfvnlF6tRo0bmgavjOstMcZ89Y82aNVaePHmskSNHWjt27LA+//xzK3/+/NZnn33mLDN69Gjz78ecOXOs3377zbr33nutSpUqWefOnfNp3QNN165drTJlyljz58+3du/ebc2ePdsqXry4NXjwYGcZ7nXOZqP++uuv5qFxY9y4cebrvXv3ZvmetmzZ0qpdu7a1evVqa/ny5WZ2a8eOHS1vIBD5wDvvvGM+MCIiIsw0/FWrVvm6SgFP/2PL6DFlyhRnGf2P7KmnnrKKFi1qPljuu+8+E5rg2UDEffacefPmWTVq1DB/RFWrVs368MMP3a7r1OVhw4ZZMTExpkzTpk2t7du3+6y+gerkyZPm/8P673K+fPmsypUrW88//7x14cIFZxnudfYtXbo0w3+XNYBm9Z4ePXrUBKCCBQtaUVFRVrdu3UzQ8gZ2uwcAALbHGCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAAGB7BCIAyKaKFSvKW2+95etqAPAgVqoGEND69u0rK1askM2bN5vd4HWTWW87fPiwFChQQPLnz+/19wKQO2ghAhDwunfvLg8++GCuvV+JEiUIQ0CQIRAB8Bu333679OnTR/r16ydFixaVmJgYmTRpkpw5c0a6desmhQoVkmuvvVYWLFjg/J7x48dLr169pHLlytl+v6lTp0qRIkVk/vz5UrVqVRNy2rdvL2fPnpVPPvnEdI1pPbQVKjU1NdMus5CQEPnoo4/kvvvuM69RpUoVmTt3rvP68ePHpVOnTiZIRUZGmutTpky5qnsFwLMIRAD8igaR4sWLy5o1a0w46tmzp3To0EEaN24s69evl+bNm0vnzp1NaPEEfR0NVTNmzJCFCxfKTz/9ZILN999/bx6ffvqpfPDBB/LVV19d8XVGjBghDzzwgPz2229y1113mQB07Ngxc23YsGGydetWE+S2bdsmEydOND8jAP9BIALgV2rVqiUvvPCCaUUZOnSo5MuXz4SHxx9/3JwbPny4HD161AQPT0hJSTEBpXbt2nLrrbeaFqLly5fLxx9/LHFxcXL33XfLHXfcIUuXLr3i6zz66KPSsWNH04L12muvyenTp02oU/v27TOvX69ePdO61KxZM2nTpo1H6g/AMwhEAPzKDTfc4Pw6LCxMihUrJjVr1nSe0240lZSU5JH30y6ua665xu31NbQULFjQ7dw/vZ9rvXXAdVRUlPN7tJVLW6BuvPFGGTx4sKxcudIjdQfgOQQiAH4lPDzc7VjH57ie02OVlpaWK+/nOPdP73el72nVqpXs3btX+vfvLwcOHJCmTZvKwIEDPVJ/AJ5BIAKAXKADqrt27SqfffaZGZD94Ycf+rpKAFzkcT0AgECzc+dOM14nMTFRzp0751yHSMf/REREiD/QcU9169aV6tWry4ULF8ysNl0zCYD/IBABCGg9evSQZcuWOY918LLavXu3GQvkDzSY6QDxPXv2mGn3TZo0MWOKAPgPVqoGAAC2xxgiAABgewQiAEFLZ3fp9PmMHrpWEAA40GUGIGglJCSYgdYZiY6ONg8AUAQiAABge3SZAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAA2yMQAQAAsbv/D1sWiOpJ0WQXAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(m1_mins, analytical_results_list, label = 'analytical')\n", - "plt.scatter(m1_mins, MC_results_list, label = 'MC')\n", - "\n", - "plt.xlabel('m1_mins')\n", - "plt.ylabel('M_sf_Univ/N_bin_COMPAS')\n", - "\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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AFst4pcRXSVYF9ZLjToWAADgT25PBT9//rw8/fTTUrJkSbnppptMPU7qDQAAIKjCzfPPP2/u5zRt2jSz5o3e32nMmDESHR3tvAcUAABA0AxL6arEGmLatGkjffv2ldtvv12qVasmFStWlLlz50rPnj2901IAAABv9NzoVO8qVao462scU79btWola9ascffLAQAA+DfcaLCJj483j2vWrCnz58939ugUKVLEs60DAADwdrjRoajt27ebxyNGjJCpU6dKRESEDBkyRIYPH+7ulwMAAPCoHN9+4bfffpMtW7aYupt69epJoOP2CwAABB+P334hK1pIrBsAAEBQDksNGjRIpkyZct3+f/7zn/Lss896ql0AAAC+CTdfffWVtGzZ8rr9LVq0kC+//DJ7rQAAAPBXuDl16pQZ80pPx79OnjzpqXYBAAD4Jtxo4fCSJUuu2//dd985178BAADwF7cLiocOHWruLfXHH3/InXfeafYtX75c3nnnHZk0aZI32ggAAOC9cNOvXz+5fPmyvP766/Laa6+ZfZUqVTL3murVq5e7Xw4AACBw1rnR3pv8+fNLwYIFJViwzg0AAMHHZ+vclChRIiefDgAA4HEuhZtGjRqZupqiRYtKw4YNJSwsLNNzt27d6sn2AQAAeD7cdOnSRcLDw52Pswo3AAAAQX1vqWBDzQ0AAPZ+/3Z7nRtdy0YX8kvvzJkzrHMDAAD8zu1w8+uvv8q1a9eu26/Tw3///XdPtQsAACBbXJ4ttXDhQufj77//Ps0tGDTsaMFx5cqVs9cKAAAAX4ebrl27mo9aTNy7d+80x/LmzWsW8tNVigEAAIIi3KSkpJiP2juzadMmKV68uDfbBQAA4JuaGx1+ItgAAABb3RW8bdu28sknn8ilS5e80yoAAABfhRtdgbhevXrm7uClS5eWv//977Jx48bsvj4AAIB/w02DBg1k8uTJcvToUfnwww8lMTFRWrVqJXXq1JEJEyaYm2kCAAAETbhxyJMnj9x///3yxRdfyJtvvikHDhyQ5557TsqXLy+9evUyoQcAACBows3mzZvlqaeekjJlypgeGw02Bw8elGXLlpleHb0HFQAAQMBOBXfQIDNr1izZu3evdOzYUT766CPzMVeuXM6p4rNnzzbr3gAAAAR8uJk2bZr069dP+vTpY3ptMlKyZEmZOXOmJ9oHAADgFu4K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MTExEh0dLWFhYbJgwYIsz//3v/8td911l5QoUUIKFy4szZs3l++//95n7QUAAIHPr+Hm/PnzUr9+fZk6darLYUjDzeLFi2XLli3Stm1bE462bdvm9bYCAIDgEGZZliUBQHtuYmNjpWvXrm59Xu3ateXBBx+U0aNHu3R+UlKSREZGytmzZ03vDwAACHzuvH8Hdc1NSkqKnDt3TooVK+bvpgAAgACRR4LY22+/LcnJydK9e/dMz7l8+bLZUic/AABgX0Hbc/Ppp5/KmDFjZP78+VKyZMlMzxs7dqzpxnJs5cuX92k7AQCAbwVluJk3b57079/fBJv27dtnee7IkSPN+JxjO3LkiM/aCQAAfC/ohqU+++wz6devnwk4nTp1uuH54eHhZgMAAKHBr+FG62UOHDjgfB4fHy9xcXGmQLhChQqm1yUhIUE++ugj51BU7969ZfLkydKsWTM5duyY2Z8/f34z5AQAAODXYanNmzdLw4YNzaaGDh1qHjumdScmJsrhw4ed50+fPl3++usvGThwoJQpU8a5DR482G/fAwAACCwBs86Nr7DODQAAwSdk1rkBAABIj3ADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABshXADAABsJY+/GwAAyDnLsuSvv/6Sa9eu+bspQLblzZtXcufOLTlFuAGAIHflyhVJTEyUCxcu+LspQI6EhYVJuXLlpGDBgjn6OoQbAAhiKSkpEh8fb/7ajY6Olnz58pk3CCAYex//+OMP+f3336V69eo56sEh3ABAkPfaaMApX768FChQwN/NAXKkRIkS8uuvv8rVq1dzFG4oKAYAG8iVi1/nCH5hHup19Ov/hjVr1khMTIzpStVvaMGCBVmer2PKDz/8sNSoUcP8R3722Wd91lYAABAc/Bpuzp8/L/Xr15epU6e6dP7ly5dNl9VLL71kPg8AAFWpUiWZNGmSBDodctE/5uPi4nL0dV555RVp0KCBx9o1e/ZsKVKkiNiFX8NNhw4d5B//+Id069bN5X+8kydPll69eklkZKTX2wcACCyZvQlv2rRJHn/8cbGjjEY2nnvuOVm+fLnXX3vOnDnSqlUr87hNmzamLePGjbvuvE6dOpljGrpSO3DggPTt29fMgAoPD5fKlStLjx49ZPPmzV5tN4O0AAC5lmLJ+oOn5Ou4BPNRn/u6MDontFc/lAqqdap0VFSU11/n66+/lvvuu8/5XAvXNWCmlpCQYIJWmTJl0uzXANO4cWPZt2+fvP/++7Jr1y6JjY2VmjVryrBhw7zabtuHGx3KSkpKSrMBAP6/JTsSpdWbK6THB/+RwfPizEd9rvu9RXsBnn76aVM7Wbx4cbnnnnvM/gkTJkjdunXlpptuMm+kTz31lCQnJ5tjq1atMr0AZ8+eNb0EqXsK0g9LHT58WLp06WJCQOHChaV79+5y/PjxLNv0wgsvmJpODUlVqlSRUaNGmVk76YeCPv74Y/N6OoLw0EMPyblz55znLFmyxPR0aO+Sho/OnTvLwYMHM536XK1aNXn77bfT7NchK/3etNdDX0fpCIfuczzPaFjqww8/lNq1a5seEg0aen0dsrqumbl06ZIsXbo0TbjR7+fkyZPy008/pendufvuu6VkyZJpvrc+ffqYKd0//vij6dmpWrWqafPLL79sQpM32T7cjB071vwDdGz6QwUA/JcGmCc/2SqJZy+l2X/s7CWz35sBR98UdV0efaN87733zD6dLDJlyhTZuXOnOb5ixQp5/vnnzbEWLVqYAKNhRSeY6KbDM+np1HgNNqdPn5bVq1fLsmXL5NChQ/Lggw9m2Z5ChQqZXgntYdASiA8++EAmTpyY5hwNKjpE9M0335hNv37qYRqtJR06dKjptdDeDP1+NJhom9LTsNKvXz+ZNWtWmv36/I477jDBR4fbHPv0+3U8T2/atGkycOBAMzT3yy+/yMKFC83nO2R1XTOj7S9btqzpaXHQn1fPnj3TtFmvmX4f6QOavpb20GQ0k8/r9T1WgNCmxMbGunx+69atrcGDB9/wvEuXLllnz551bkeOHDGvpY896a9rKda6AyetBdt+Nx/1OQB428WLF61du3aZj+7S31O3vfGDVfGFbzLcKr3wjTnujd9n+ju8YcOGNzzviy++sKKiopzPZ82aZUVGRl53XsWKFa2JEyeax0uXLrVy585tHT582Hl8586d5nf/xo0bXW7j+PHjrcaNGzufv/zyy1aBAgWspKQk577hw4dbzZo1y/Rr/PHHH+Z1f/nlF/M8Pj7ePN+2bZt5npCQYNq6YcMG8/zKlStW8eLFrdmzZ2f5/qhtqV+/vvN5dHS09eKLL7r8vX3hwnUdMGCA9dxzz133vhsXF2cVKlTISk5OtlavXm2VLFnSunr1qmmPtkt9/vnnpt1bt261PPXvWd+3XX3/tv0ifto9p5s36V82YxbtSvOXT5nICHk5ppbcWyftGCQABIqN8aev67FJTd9J9Lie17yq5+s7tB4jvR9++MH0uO/Zs8eUEej9snR4RG8t4WpNze7du00vfeqe+lq1apneAj126623Zvh5n3/+uend0N4ZHbLR19ZeotR0WEh7eBx0+OfEiRPO5/v375fRo0fLhg0bzPCNo8dGh8nq1Klz3WvqUig6ZKNDSk2bNpVFixaZcooHHnhAXKWvf/ToUWnXrl2m5/zg5nXVTKVtmT9//nXHdLayDjd9+eWXsnLlSnn00UclT560ceK/mcx//Dospf94tOvKMSVOlxDXx/qPQI0cOdLMjErNcb5+ri7TrI+1CzEUu3QBICdOnLvk0fPcpfUf6adJa01HvXr15KuvvpItW7Y4lwrJacHxjaxfv94Mt3Ts2NEMN23btk1efPHF615Xb+yYfmgp9ZCTrt2mw2E6pKUBR7cbtb9///4yb948uXjxohnu0eEzd4qj8+fPn+XxX7NxXTdu3GgCkA4FZkSHofRraMBJPySltHZJaZjyB7/23OiYZNu2bZ3PdZxS9e7d24zh6fiiI+g4NGzY0PlYf0CffvqpVKxY0fzwfE1nE2iPTUb5VPfpOot6/K5apSV3Lu71AiCwlCwU4dHzckp/p2tQeOedd5x1Gul7DrTm40Z3Pr/lllvkyJEjZnP03ugfwWfOnDE9OBlZt26deS/RQOPw22+/udX+U6dOyd69e02wuf32282+tWvX3vDzNFBp0NO6GS1I1gVu0weqrL5n7UnSHiWtkUn9nurOdU1PC361RymzWyDogrpa76S9OBldUy0c1v36mhrW0tfd6M/Cm3U3fg03Wi2fVddV+ulmgdDVFUhdugCQE00rFzND6NrTnNFvVv2TrHRkhDnPF7QAVmcnvfvuu6YHJHWhsYO+iWvPvb6R6xur9nCk7+Vo3769mRmkPTFagKw9EDo7qHXr1tKkSZMMX1uHWfSPae1B0WGrb7/91kxbdkfRokXNDKnp06eb4Sr9eiNGjLjh52mA0JlFOlqh7WjevPl137N+vy1btjRlFvo66ensqSeeeMLMWOrQoYOZwaXX75lnnnHpuqanBcmvvvpqlt+rdkCk78lK3aOlvVD6s9Cgp6FRC5P1Z6fDXToLS4uxvcX2s6Xs3KULADmhPcpaG6jS9y07nutxX/U8a1jRKctvvvmmqU+ZO3euqRNJTYdJ9E1cewN0bZu33norwzdW7XnQN2CddaRvsDq1W2tqMqPTnYcMGWKmT2uvg/bk6FRwd2jvhIYj7SnR9uvXGz9+vEuf+9hjj5khIp3qnp72fuiML+2FSj16kZqOeGiQ+9e//mWmg+swlNb/uHpdU9OaI52G7pienxnteUk/tJia1hDpCI2GqwEDBpgeNb3OOovK26tJh2lVsYQQLaTSKeG6TkL6QjF36UJXuh7EjXw24DZ6bgB4hRaFar2irvwaEZG94SMmRfifrgWjBcE6lFaqVCm/tmXChAmmAHnx4sUB9e/Znfdv28+WCqUuXQDIDg0wWhuoQ+ja06w1Nvp7i1pB79OZUTo5RoeVdIaUv4ON0lsl6BBZMGNYykZdugCQXfp7SnuYuzQoaz7ye8s3PvvsM1PIrAW2GQ2x+UP37t2dBdHBinDjgb94pj3SyPTQpKbPdT9dugCAzGghsc6E0jodXQ0YnsGwlAfQpQsAQOAg3Hi4SxcAAPgXw1IAYAMhNvEVNmV56N8x4QYAgphjETW9RxAQ7K783+0gMlsZ2VUMSwFAENM3AV1MzXHzRl2tVxexA4KN3iJCp8Xrv+H0N+J0F+EGAIJc6dKlzcfUd6cGgpGu8lyhQoUcB3TCDQAEOX0j0HsZ6X2F9B5CQLDSG6Omv8lmdhBuAMBGQ1Q5rVUA7ICCYgAAYCuEGwAAYCuEGwAAYCt5QnWBIL11OgAACA6O921XFvoLuXBz7tw587F8+fL+bgoAAMjG+3hkZGSW54RZIbZmty4SdPToUSlUqJDHFrrSNKlh6ciRI1K4cGGPfE1cj+vsO1xr3+Fa+wbXOfivtcYVDTbR0dE3nC4ecj03ekHKlSvnla+tP0T+03gf19l3uNa+w7X2Da5zcF/rG/XYOFBQDAAAbIVwAwAAbIVw4wHh4eHy8ssvm4/wHq6z73CtfYdr7Rtc59C61iFXUAwAAOyNnhsAAGArhBsAAGArhBsAAGArhJscmjp1qlSqVEkiIiKkWbNmsnHjRn83KeiNHTtWbr31VrPQYsmSJaVr166yd+/eNOdcunRJBg4cKFFRUVKwYEH5n//5Hzl+/Ljf2mwH48aNMwtbPvvss859XGfPSUhIkEceecRcy/z580vdunVl8+bNzuNa/jh69GgpU6aMOd6+fXvZv3+/X9scbK5duyajRo2SypUrm2tYtWpVee2119Is1891zp41a9ZITEyMWUBPf08sWLAgzXFXruvp06elZ8+eZu2bIkWKyGOPPSbJycniFVpQjOyZN2+elS9fPuvDDz+0du7caQ0YMMAqUqSIdfz4cX83Lajdc8891qxZs6wdO3ZYcXFxVseOHa0KFSpYycnJznOeeOIJq3z58tby5cutzZs3W7fddpvVokULv7Y7mG3cuNGqVKmSVa9ePWvw4MHO/Vxnzzh9+rRVsWJFq0+fPtaGDRusQ4cOWd9//7114MAB5znjxo2zIiMjrQULFljbt2+37rvvPqty5crWxYsX/dr2YPL6669bUVFR1jfffGPFx8dbX3zxhVWwYEFr8uTJznO4ztmzePFi68UXX7T+/e9/a1K0YmNj0xx35bree++9Vv369a3//Oc/1o8//mhVq1bN6tGjh+UNhJscaNq0qTVw4EDn82vXrlnR0dHW2LFj/douuzlx4oT5z7R69Wrz/MyZM1bevHnNLy6H3bt3m3PWr1/vx5YGp3PnzlnVq1e3li1bZrVu3doZbrjOnvPCCy9YrVq1yvR4SkqKVbp0aWv8+PHOfXr9w8PDrc8++8xHrQx+nTp1svr165dm3/3332/17NnTPOY6e0b6cOPKdd21a5f5vE2bNjnP+e6776ywsDArISHB8jSGpbLpypUrsmXLFtP1lvrWDvp8/fr1fm2b3Zw9e9Z8LFasmPmo1/3q1atprn3NmjWlQoUKXPts0GGnTp06pbmeiuvsOQsXLpQmTZrIAw88YIZaGzZsKB988IHzeHx8vBw7dizNtdZl5nWom2vtuhYtWsjy5ctl37595vn27dtl7dq10qFDB/Oc6+wdrlxX/ahDUfr/wEHP1/fNDRs2eLxNIXdvKU85efKkGd8tVapUmv36fM+ePX5rlx1vdKo1IC1btpQ6deqYffqfKF++fOY/Svprr8fgunnz5snWrVtl06ZN1x3jOnvOoUOHZNq0aTJ06FD53//9X3O9Bw0aZK5v7969ndczo98nXGvXjRgxwty0UUN47ty5ze/o119/3dR5KK6zd7hyXfWjBvvU8uTJY/5o9ca1J9wg4HsVduzYYf76gmfpHXsHDx4sy5YtMwXx8G5I179Y33jjDfNce2703/V7771nwg08Y/78+TJ37lz59NNPpXbt2hIXF2f+ONIiWK5zaGFYKpuKFy9u/jJIP3NEn5cuXdpv7bKTp59+Wr755htZuXJlmju56/XVYcEzZ86kOZ9r7x4ddjpx4oQ0atTI/AWl2+rVq2XKlCnmsf7VxXX2DJ1BUqtWrTT7brnlFjl8+LB57Lie/D7JmeHDh5vem4ceesjMRnv00UdlyJAhZgam4jp7hyvXVT/q75vU/vrrLzODyhvXnnCTTdqd3LhxYzO+m/qvM33evHlzv7Yt2Gm9mgab2NhYWbFihZnWmZpe97x586a59jpVXN8ouPaua9eunfzyyy/mr1vHpr0L2oXveMx19gwdVk2/nIHWhVSsWNE81n/j+gs+9bXW4RWtReBau+7ChQumhiM1/SNUfzcrrrN3uHJd9aP+oaR/VDno73f92Whtjsd5vEQ5xKaCazX47NmzTSX4448/bqaCHzt2zN9NC2pPPvmkmVK4atUqKzEx0blduHAhzRRlnR6+YsUKM0W5efPmZkPOpJ4tpbjOnptqnydPHjNVef/+/dbcuXOtAgUKWJ988kmaqbT6++Prr7+2fv75Z6tLly5MUXZT7969rbJlyzqnguu05eLFi1vPP/+88xyuc/ZnVW7bts1sGh0mTJhgHv/2228uX1edCt6wYUOzHMLatWvNLE2mggeod9991/zy1/VudGq4zt9Hzuh/nIw2XfvGQf/DPPXUU1bRokXNm0S3bt1MAIJnww3X2XMWLVpk1alTx/xBVLNmTWv69Olpjut02lGjRlmlSpUy57Rr187au3ev39objJKSksy/X/2dHBERYVWpUsWszXL58mXnOVzn7Fm5cmWGv5c1ULp6XU+dOmXCjK49VLhwYatv374mNHkDdwUHAAC2Qs0NAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINgJBWqVIlmTRpkr+bAcCDWKEYQMAYNGiQ/PTTT7Jjxw5z12y9gae3/fHHH3LTTTdJgQIFvP5aAHyDnhsAAaVfv37y4IMP+uz1SpQoQbABbIZwA8Ar2rRpI88884w8++yzUrRoUSlVqpR88MEHcv78eenbt68UKlRIqlWrJt99953zc6ZMmSIDBw6UKlWquP16s2fPliJFisg333wjN998swksf/vb3+TChQsyZ84cM/yk7dDeoWvXrmU6LBUWFiYzZsyQbt26ma9RvXp1WbhwofP4n3/+KT179jShKH/+/Ob4rFmzcnStAHgW4QaA12ioKF68uGzcuNEEnSeffFIeeOABadGihWzdulXuvvtuefTRR00A8QT9OhqQ5s2bJ0uWLJFVq1aZkLJ48WKzffzxx/L+++/Ll19+meXXGTNmjHTv3l1+/vln6dixowkzp0+fNsdGjRolu3btMqFs9+7dMm3aNPM9AggchBsAXlO/fn156aWXTO/GyJEjJSIiwgSBAQMGmH2jR4+WU6dOmRDhCVevXjVho2HDhnLHHXeYnpu1a9fKzJkzpVatWtK5c2dp27atrFy5Msuv06dPH+nRo4fpWXrjjTckOTnZBDR1+PBh8/WbNGlien3at28vMTExHmk/AM8g3ADwmnr16jkf586dW6KioqRu3brOfTpUpU6cOOGR19NhpKpVq6b5+hpAChYsmGbfjV4vdbu12Lhw4cLOz9HeJ+0ZatCggTz//POybt06j7QdgOcQbgB4Td68edM813qW1Pv0uUpJSfHJ6zn23ej1svqcDh06yG+//SZDhgyRo0ePSrt27eS5557zSPsBeAbhBgDcpMXEvXv3lk8++cQUI0+fPt3fTQKQSp7UTwDAnw4cOGDqW44dOyYXL150rnOj9TL58uWTQKB1Qo0bN5batWvL5cuXzewsXZMHQOAg3AAIGP3795fVq1c7n2vhroqPjze1M4FAQ5YWR//6669mKvjtt99uanAABA5WKAYAALZCzQ0AALAVwg2AoKCzlHRKd0abrkUDAA4MSwEICgkJCabIOCPFihUzGwAowg0AALAVhqUAAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAICtEG4AAIDYyf8D44EUvaWaKhwAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(m1_mins, np.array(analytical_results_list)/np.array(MC_results_list) , label = 'ratio analytical/MC')\n", - "\n", - "plt.xlabel('m1_mins')\n", - "plt.ylabel('ratio analytical/MC')\n", - "\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "COMPAS", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 708d0656fae4bc9b7b99e4c128068d7dda8134ef Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 5 Jan 2026 17:17:06 +0100 Subject: [PATCH 44/47] added some comments and fixed small error in integrand_compas in get_COMPAS_fraction --- .../totalMassEvolvedPerZ.py | 47 ++++++++++++------- py_tests/test_total_mass_evolved_per_z.py | 4 +- py_tests/test_values.py | 2 +- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index df427bc10..e895b97f6 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -73,36 +73,49 @@ def get_COMPAS_fraction(m1_low, m1_upp, m2_low, f_bin=None, mi, aij : float IMF breakpoints and slopes """ - binary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] + fbinary_bin_edges = [m1, 0.08, 0.5, 1, 10, m4] def get_binary_fraction(mass): binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] - for i in range(len(binary_bin_edges) - 1): - if binary_bin_edges[i] <= mass < binary_bin_edges[i + 1]: + for i in range(len(fbinary_bin_edges) - 1): + if mass < fbinary_bin_edges[0]: + # Mass below lowest binary fraction bin edge (shouldnt happen) + return binaryFractions[0] + if fbinary_bin_edges[i] <= mass < fbinary_bin_edges[i + 1]: return binaryFractions[i] - return 0 # catch-all - - def IMF_mass(mass): - return IMF(mass, m1, m2, m3, m4, a12, a23, a34) * mass + if mass >= fbinary_bin_edges[-1]: + # Mass above highest binary fraction bin edge (shouldnt happen) + return binaryFractions[-1] def integrand_full(mass, f_bin): local_f_bin = get_binary_fraction(mass) if f_bin is None else f_bin expected_q = quad(lambda q: q * mass_ratio_pdf_function(q), 0, 1)[0] - return (1 - local_f_bin + local_f_bin * (1 + expected_q)) * IMF_mass(mass) + # mass of single stars = (1 - f_bin) * m1 + # mass of binaries = f_bin * (1 + ) * m1 + expected_mass_all_stellar_sys =(1 + local_f_bin * expected_q) * mass * IMF(mass, m1, m2, m3, m4, a12, a23, a34) + return expected_mass_all_stellar_sys def integrand_compas(mass, f_bin): local_f_bin = get_binary_fraction(mass) if f_bin is None else f_bin + # Only binaries contribute in COMPAS population + # Integrand is (1 + q) * f_bin * m1 * P(m1) * P(q), q_min = m2_low / mass if q_min >= 1: return 0 # No valid secondaries - f_q = quad(mass_ratio_pdf_function, q_min, 1)[0] + # Integrate (1 + q)P(q) dq over q from q_min to 1, + # we get p(q)dq: + p_qdq = quad(mass_ratio_pdf_function, q_min, 1)[0] + # and q P(q) dq (= expected_q) expected_q = quad(lambda q: q * mass_ratio_pdf_function(q), q_min, 1)[0] - return local_f_bin * f_q * (1 + expected_q) * IMF_mass(mass) + + expected_mass_compas_binaries = (p_qdq + expected_q) * local_f_bin * mass * IMF(mass, m1, m2, m3, m4, a12, a23, a34) + return expected_mass_compas_binaries + # split integral at binary fraction steps if f_bin is None (i.e. variable and like a step function) def split_integral(func, a, b, f_bin): total = 0 - for edge_start, edge_end in zip(binary_bin_edges[:-1], binary_bin_edges[1:]): + for edge_start, edge_end in zip(fbinary_bin_edges[:-1], fbinary_bin_edges[1:]): left = max(a, edge_start) right = min(b, edge_end) if left < right: @@ -117,7 +130,8 @@ def split_integral(func, a, b, f_bin): full_mass = quad(integrand_full, m1, m4, args=(f_bin,))[0] compas_mass = quad(integrand_compas, m1_low, m1_upp, args=(f_bin,))[0] - return compas_mass / full_mass + fraction = compas_mass / full_mass + return fraction def retrieveMassEvolvedPerZ(path): @@ -143,6 +157,7 @@ def totalMassEvolvedPerZ(path, Mlower, Mupper, m2_low, binaryFraction, mass_rati fraction = get_COMPAS_fraction(m1_low=Mlower, m1_upp=Mupper, m2_low=m2_low, f_bin=binaryFraction, mass_ratio_pdf_function=mass_ratio_pdf_function, m1=m1, m2=m2, m3=m3, m4=m4, a12=a12, a23=a23, a34=a34) + multiplicationFactor = 1 / fraction # Warning: This is slow and error prone! esp if you sample metallicities smoothly @@ -253,11 +268,11 @@ def analytical_star_forming_mass_per_binary_using_kroupa_imf( # ------------------------- # Binary-fraction bins # ------------------------- - binary_bin_edges = [m1, 0.08, 0.5, 1.0, 10.0, m4] + fbinary_bin_edges = [m1, 0.08, 0.5, 1.0, 10.0, m4] if fbin is None: binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] else: - binaryFractions = [float(fbin)] * (len(binary_bin_edges) - 1) + binaryFractions = [float(fbin)] * (len(fbinary_bin_edges) - 1) # ------------------------- # Helpers: overlaps and power integrals @@ -284,7 +299,7 @@ def int_power(lo, hi, power): for j in range(len(binaryFractions)): # fbin bin index j fbin_j = binaryFractions[j] - fb_lo, fb_hi = binary_bin_edges[j], binary_bin_edges[j + 1] + fb_lo, fb_hi = fbinary_bin_edges[j], fbinary_bin_edges[j + 1] A, B = overlap(imf_lo, imf_hi, fb_lo, fb_hi) if B <= A: @@ -307,7 +322,7 @@ def int_power(lo, hi, power): for j in range(len(binaryFractions)): # fbin bin index j fbin_j = binaryFractions[j] - fb_lo, fb_hi = binary_bin_edges[j], binary_bin_edges[j + 1] + fb_lo, fb_hi = fbinary_bin_edges[j], fbinary_bin_edges[j + 1] # overlap additionally with COMPAS m1-range A, B = overlap(imf_lo, imf_hi, fb_lo, fb_hi) diff --git a/py_tests/test_total_mass_evolved_per_z.py b/py_tests/test_total_mass_evolved_per_z.py index f14c459a4..d15cffafc 100644 --- a/py_tests/test_total_mass_evolved_per_z.py +++ b/py_tests/test_total_mass_evolved_per_z.py @@ -62,8 +62,8 @@ def test_analytical_function(): def test_analytical_vs_numerical_star_forming_mass_per_binary(fake_compas_output, tmpdir, test_archive_dir): np.random.seed(42) - m1_max = M1_MAX m1_min = M1_MIN + m1_max = M1_MAX m2_min = M2_MIN fbin = F_BIN @@ -106,6 +106,8 @@ def plot_star_forming_mass_per_binary_comparison( linewidth=0 ) plt.plot(n_samps, np.median(numerical_vals, axis=0), color='tab:orange', label="numerical") + plt.text(n_samps[-1], np.median(numerical_vals, axis=0)[-1]*0.9, f"median numerical = {np.round(np.median(numerical_vals, axis=0)[-1],3)}", va='bottom', ha='right', color='tab:orange') + plt.text(n_samps[-1], analytical*1.1, f"analytical = {np.round(analytical,3)}", va='bottom', ha='right', color='tab:blue') plt.xscale("log") plt.ylabel(r"Star forming mass per binary [M$_{\odot}$]") plt.xlabel("Number of samples") diff --git a/py_tests/test_values.py b/py_tests/test_values.py index f38ef0c28..e7cf37f91 100644 --- a/py_tests/test_values.py +++ b/py_tests/test_values.py @@ -1,6 +1,6 @@ # Testvalues used in test_total_mass_evolved_per_z.py MAKE_PLOTS = True -M1_MIN = 5 +M1_MIN = 6 M1_MAX = 150 M2_MIN = 0.1 F_BIN = None # None = variable f_bin, otherwise fixed value \ No newline at end of file From bd26fde5d50e153009ceaa097964e078e256a60d Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 5 Jan 2026 17:54:15 +0100 Subject: [PATCH 45/47] testing different values --- .../binned_cosmic_integrator/binary_population.py | 1 - py_tests/test_values.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py b/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py index 7449a2bda..d3a0ed5b1 100644 --- a/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py +++ b/compas_python_utils/cosmic_integration/binned_cosmic_integrator/binary_population.py @@ -277,7 +277,6 @@ def _load_data(path: str, group: str, var_names: List[str], mask: Optional[xp.nd # Mock generation utility - def generate_mock_population( filename: str = "", n_systems: int = 2000, diff --git a/py_tests/test_values.py b/py_tests/test_values.py index e7cf37f91..3a64f7590 100644 --- a/py_tests/test_values.py +++ b/py_tests/test_values.py @@ -1,6 +1,6 @@ # Testvalues used in test_total_mass_evolved_per_z.py MAKE_PLOTS = True -M1_MIN = 6 -M1_MAX = 150 +M1_MIN = 5 +M1_MAX = 100 M2_MIN = 0.1 F_BIN = None # None = variable f_bin, otherwise fixed value \ No newline at end of file From aeeedad2b7fad943654a381c045588386578d3d1 Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 2 Feb 2026 13:40:24 +0100 Subject: [PATCH 46/47] fixed typo --- compas_python_utils/cosmic_integration/ClassCOMPAS.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compas_python_utils/cosmic_integration/ClassCOMPAS.py b/compas_python_utils/cosmic_integration/ClassCOMPAS.py index ef15200f4..4a40dcb9d 100644 --- a/compas_python_utils/cosmic_integration/ClassCOMPAS.py +++ b/compas_python_utils/cosmic_integration/ClassCOMPAS.py @@ -99,7 +99,7 @@ def setCOMPASDCOmask( "BHNS": np.logical_or(np.logical_and(stellar_type_1 == 13, stellar_type_2 == 14),np.logical_and(stellar_type_1 == 14, stellar_type_2 == 13)), "NSWD": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 13), np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 13)), - "WDBH": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 14), + "BHWD": np.logical_or(np.logical_and(np.isin(stellar_type_1,[10,11,12]),stellar_type_2 == 14), np.logical_and(np.isin(stellar_type_2,[10,11,12]),stellar_type_1 == 14)), } @@ -139,8 +139,8 @@ def setCOMPASDCOmask( self.NSNSmask = type_masks["NSNS"] * hubble_mask * rlof_mask * pessimistic_mask self.WDWDmask = type_masks["WDWD"] * hubble_mask * rlof_mask * pessimistic_mask self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask - self.WDWDmask = type_masks["NSWD"] * hubble_mask * rlof_mask * pessimistic_mask - self.WDWDmask = type_masks["WDBH"] * hubble_mask * rlof_mask * pessimistic_mask + self.NSWDmask = type_masks["NSWD"] * hubble_mask * rlof_mask * pessimistic_mask + self.BHWDmask = type_masks["BHWD"] * hubble_mask * rlof_mask * pessimistic_mask self.CHE_BHBHmask = type_masks["CHE_BHBH"] * hubble_mask * rlof_mask * pessimistic_mask self.NonCHE_BHBHmask = type_masks["NON_CHE_BHBH"] * hubble_mask * rlof_mask * pessimistic_mask self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask From bf599906734e42c2e3e54fdf9c99122f72582e7e Mon Sep 17 00:00:00 2001 From: Lieke van Son Date: Mon, 2 Feb 2026 13:45:19 +0100 Subject: [PATCH 47/47] fixed spelling mistakes --- .../cosmic_integration/FastCosmicIntegration.py | 2 +- .../cosmic_integration/totalMassEvolvedPerZ.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py index 8da39e2c5..ef9d18554 100644 --- a/compas_python_utils/cosmic_integration/FastCosmicIntegration.py +++ b/compas_python_utils/cosmic_integration/FastCosmicIntegration.py @@ -246,7 +246,7 @@ def compute_snr_and_detection_grids(dco_type, sensitivity="O1", snr_threshold=8. """ # If DCO type includes a WD, return empty arrays since we currently only support LVK sensitivity if dco_type in ["WDWD", "NSWD", "WDBH"]: - warnings.warn("!! Detected rate is not computed since DCO type {} doesnt work with LVK sensitivity {}".format(dco_type, sensitivity)) + warnings.warn("!! Detected rate is not computed since DCO type {} doesn't work with LVK sensitivity {}".format(dco_type, sensitivity)) # get interpolator given sensitivity interpolator = selection_effects.SNRinterpolator(sensitivity) diff --git a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py index e895b97f6..78907e88d 100644 --- a/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py +++ b/compas_python_utils/cosmic_integration/totalMassEvolvedPerZ.py @@ -79,12 +79,12 @@ def get_binary_fraction(mass): binaryFractions = [0.1, 0.225, 0.5, 0.8, 1.0] for i in range(len(fbinary_bin_edges) - 1): if mass < fbinary_bin_edges[0]: - # Mass below lowest binary fraction bin edge (shouldnt happen) + # Mass below lowest binary fraction bin edge (shouldn't happen) return binaryFractions[0] if fbinary_bin_edges[i] <= mass < fbinary_bin_edges[i + 1]: return binaryFractions[i] if mass >= fbinary_bin_edges[-1]: - # Mass above highest binary fraction bin edge (shouldnt happen) + # Mass above highest binary fraction bin edge (shouldn't happen) return binaryFractions[-1] def integrand_full(mass, f_bin):