|
| 1 | +#!/usr/bin/env python3 |
| 2 | +import numpy as np |
| 3 | +import h5py as h5 |
| 4 | +import os |
| 5 | +from . import totalMassEvolvedPerZ as MPZ |
| 6 | + |
| 7 | + |
| 8 | +class COMPASData(object): |
| 9 | + def __init__( |
| 10 | + self, |
| 11 | + path=None, |
| 12 | + lazyData=True, |
| 13 | + Mlower=None, |
| 14 | + Mupper=None, |
| 15 | + m2_min=None, |
| 16 | + binaryFraction=None, |
| 17 | + suppress_reminder=False, |
| 18 | + ): |
| 19 | + self.path = path |
| 20 | + if self.path is None: |
| 21 | + print("Template COMPASData object created with no data path") |
| 22 | + elif not os.path.isfile(path): |
| 23 | + raise ValueError( "h5 file not found. Wrong path given? {}".format(path)) |
| 24 | + |
| 25 | + # Crucial values to be able to calculate MSSFR |
| 26 | + self.metallicityGrid = None |
| 27 | + self.metallicitySystems = None |
| 28 | + self.delayTimes = None # Myr |
| 29 | + # Crucial values I need for selection effects |
| 30 | + self.mass1 = None # Msun |
| 31 | + self.mass2 = None # Msun |
| 32 | + self.DCOmask = None |
| 33 | + self.allTypesMask = None |
| 34 | + self.BBHmask = None |
| 35 | + self.DNSmask = None |
| 36 | + self.BHNSmask = None |
| 37 | + self.CHE_mask = None |
| 38 | + self.CHE_BBHmask = None |
| 39 | + self.NonCHE_BBHmask = None |
| 40 | + self.initialZ = None |
| 41 | + self.sw_weights = None |
| 42 | + self.n_systems = None |
| 43 | + |
| 44 | + # Additional arrays that might be nice to store |
| 45 | + # to more quickly make some plots. |
| 46 | + # If you need more memory might help a tiny bit to not do |
| 47 | + self.lazyData = lazyData |
| 48 | + self.mChirp = None # Msun |
| 49 | + self.q = None |
| 50 | + self.optimisticmask = None |
| 51 | + |
| 52 | + # Needed to recover true solar mass evolved |
| 53 | + self.Mlower = Mlower # Msun |
| 54 | + self.Mupper = Mupper # Msun |
| 55 | + self.m2_min = m2_min # Msun |
| 56 | + self.binaryFraction = binaryFraction |
| 57 | + self.totalMassEvolvedPerZ = None # Msun |
| 58 | + self.mass_evolved_per_binary = None # Msun |
| 59 | + |
| 60 | + if not suppress_reminder: |
| 61 | + print("ClassCOMPAS: Remember to self.setCOMPASDCOmask()") |
| 62 | + print(" then self.setCOMPASData()") |
| 63 | + print(" and optionally self.setGridAndMassEvolved() if using a metallicity grid") |
| 64 | + |
| 65 | + def setCOMPASDCOmask( |
| 66 | + self, types="BBH", withinHubbleTime=True, pessimistic=True, noRLOFafterCEE=True |
| 67 | + ): |
| 68 | + # By default, we mask for BBHs that merge within a Hubble time, assuming |
| 69 | + # the pessimistic CEE prescription (HG donors cannot survive a CEE) and |
| 70 | + # not allowing immediate RLOF post-CEE |
| 71 | + |
| 72 | + stellar_type_1, stellar_type_2, hubble_flag, dco_seeds = \ |
| 73 | + self.get_COMPAS_variables("BSE_Double_Compact_Objects", ["Stellar_Type(1)", "Stellar_Type(2)", "Merges_Hubble_Time", "SEED"]) |
| 74 | + dco_seeds = dco_seeds.flatten() |
| 75 | + |
| 76 | + if types == "CHE_BBH" or types == "NON_CHE_BBH": |
| 77 | + stellar_type_1_zams, stellar_type_2_zams, che_ms_1, che_ms_2, sys_seeds = \ |
| 78 | + self.get_COMPAS_variables("BSE_System_Parameters", ["Stellar_Type@ZAMS(1)", "Stellar_Type@ZAMS(2)", "CH_on_MS(1)", "CH_on_MS(2)", "SEED"]) |
| 79 | + |
| 80 | + che_mask = np.logical_and.reduce((stellar_type_1_zams == 16, stellar_type_2_zams == 16, che_ms_1 == True, che_ms_2 == True)) |
| 81 | + che_seeds = sys_seeds[()][che_mask] |
| 82 | + |
| 83 | + 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)) |
| 84 | + |
| 85 | + # 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 |
| 86 | + hubble_mask = hubble_flag.astype(bool) if withinHubbleTime else np.repeat(True, len(dco_seeds)) |
| 87 | + |
| 88 | + # mask on stellar types (where 14=BH and 13=NS), BHNS can be BHNS or NSBH |
| 89 | + type_masks = { |
| 90 | + "all": np.repeat(True, len(dco_seeds)), |
| 91 | + "BBH": np.logical_and(stellar_type_1 == 14, stellar_type_2 == 14), |
| 92 | + "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)), |
| 93 | + "BNS": np.logical_and(stellar_type_1 == 13, stellar_type_2 == 13), |
| 94 | + } |
| 95 | + type_masks["CHE_BBH"] = np.logical_and(self.CHE_mask, type_masks["BBH"]) if types == "CHE_BBH" else np.repeat(False, len(dco_seeds)) |
| 96 | + 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)) |
| 97 | + |
| 98 | + # if the user wants to make RLOF or optimistic CEs |
| 99 | + if noRLOFafterCEE or pessimistic: |
| 100 | + |
| 101 | + # get the flags and unique seeds from the Common Envelopes file |
| 102 | + ce_seeds = self.get_COMPAS_variables("BSE_Common_Envelopes", "SEED") |
| 103 | + dco_from_ce = np.in1d(ce_seeds, dco_seeds) |
| 104 | + dco_ce_seeds = ce_seeds[dco_from_ce] |
| 105 | + |
| 106 | + # if masking on RLOF, get flag and match seeds to dco seeds |
| 107 | + if noRLOFafterCEE: |
| 108 | + rlof_flag = self.get_COMPAS_variables("BSE_Common_Envelopes", "Immediate_RLOF>CE")[dco_from_ce].astype(bool) |
| 109 | + rlof_seeds = np.unique(dco_ce_seeds[rlof_flag]) |
| 110 | + rlof_mask = np.logical_not(np.in1d(dco_seeds, rlof_seeds)) |
| 111 | + else: |
| 112 | + rlof_mask = np.repeat(True, len(dco_seeds)) |
| 113 | + |
| 114 | + # if masking on pessimistic CE, get flag and match seeds to dco seeds |
| 115 | + if pessimistic: |
| 116 | + pessimistic_flag = self.get_COMPAS_variables("BSE_Common_Envelopes", "Optimistic_CE")[dco_from_ce].astype(bool) |
| 117 | + pessimistic_seeds = np.unique(dco_ce_seeds[pessimistic_flag]) |
| 118 | + pessimistic_mask = np.logical_not(np.in1d(dco_seeds, pessimistic_seeds)) |
| 119 | + else: |
| 120 | + pessimistic_mask = np.repeat(True, len(dco_seeds)) |
| 121 | + else: |
| 122 | + rlof_mask = np.repeat(True, len(dco_seeds)) |
| 123 | + pessimistic_mask = np.repeat(True, len(dco_seeds)) |
| 124 | + |
| 125 | + # create a mask for each dco type supplied |
| 126 | + self.DCOmask = type_masks[types] * hubble_mask * rlof_mask * pessimistic_mask |
| 127 | + self.BBHmask = type_masks["BBH"] * hubble_mask * rlof_mask * pessimistic_mask |
| 128 | + self.BHNSmask = type_masks["BHNS"] * hubble_mask * rlof_mask * pessimistic_mask |
| 129 | + self.DNSmask = type_masks["BNS"] * hubble_mask * rlof_mask * pessimistic_mask |
| 130 | + self.CHE_BBHmask = type_masks["CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask |
| 131 | + self.NonCHE_BBHmask = type_masks["NON_CHE_BBH"] * hubble_mask * rlof_mask * pessimistic_mask |
| 132 | + self.allTypesMask = type_masks["all"] * hubble_mask * rlof_mask * pessimistic_mask |
| 133 | + self.optimisticmask = pessimistic_mask |
| 134 | + |
| 135 | + def setGridAndMassEvolved(self): |
| 136 | + # The COMPAS simulation does not evolve all stars |
| 137 | + # give me the correction factor for the total mass evolved |
| 138 | + # I assume each metallicity has the same limits, and does correction |
| 139 | + # factor, but the total mass evolved might be different. |
| 140 | + # This does not change when we change types and other masks this is |
| 141 | + # general to the entire simulation so calculate once |
| 142 | + _, self.totalMassEvolvedPerZ = MPZ.totalMassEvolvedPerZ( |
| 143 | + path=self.path, |
| 144 | + Mlower=self.Mlower, |
| 145 | + Mupper=self.Mupper, |
| 146 | + binaryFraction=self.binaryFraction, |
| 147 | + ) |
| 148 | + # Want to recover entire metallicity grid, assume that every metallicity |
| 149 | + # evolved shows in all systems again should not change within same run |
| 150 | + # so dont redo if we reset the data |
| 151 | + Data = h5.File(self.path, "r") |
| 152 | + if self.initialZ is None: |
| 153 | + self.initialZ = Data["BSE_System_Parameters"]["Metallicity@ZAMS(1)"][()] |
| 154 | + self.metallicityGrid = np.unique(self.initialZ) |
| 155 | + Data.close() |
| 156 | + |
| 157 | + def setCOMPASData(self): |
| 158 | + |
| 159 | + primary_masses, secondary_masses, formation_times, coalescence_times, dco_seeds = \ |
| 160 | + self.get_COMPAS_variables("BSE_Double_Compact_Objects", ["Mass(1)", "Mass(2)", "Time", "Coalescence_Time", "SEED"]) |
| 161 | + |
| 162 | + initial_seeds, initial_Z = self.get_COMPAS_variables("BSE_System_Parameters", ["SEED", "Metallicity@ZAMS(1)"]) |
| 163 | + |
| 164 | + # Get metallicity grid of DCOs |
| 165 | + self.seedsDCO = dco_seeds[self.DCOmask] |
| 166 | + if self.initialZ is None: |
| 167 | + self.initialZ = initial_Z |
| 168 | + maskMetallicity = np.in1d(initial_seeds, self.seedsDCO) |
| 169 | + self.metallicitySystems = self.initialZ[maskMetallicity] |
| 170 | + self.n_systems = len(initial_seeds) |
| 171 | + |
| 172 | + self.delayTimes = np.add(formation_times[self.DCOmask], coalescence_times[self.DCOmask]) |
| 173 | + self.mass1 = primary_masses[self.DCOmask] |
| 174 | + self.mass2 = secondary_masses[self.DCOmask] |
| 175 | + |
| 176 | + # Stuff of data I dont need for integral |
| 177 | + # but I might be to laze to read in myself |
| 178 | + # and often use. Might turn it of for memory efficiency |
| 179 | + if self.lazyData: |
| 180 | + self.q = np.divide(self.mass2, self.mass1) |
| 181 | + boolq = self.mass2 > self.mass1 |
| 182 | + self.q[boolq] = np.divide(self.mass1[boolq], self.mass2[boolq]) |
| 183 | + self.mChirp = np.divide( |
| 184 | + (np.multiply(self.mass2, self.mass1) ** (3.0 / 5.0)), |
| 185 | + (np.add(self.mass2, self.mass1) ** (1.0 / 5.0)), |
| 186 | + ) |
| 187 | + self.Hubble = self.get_COMPAS_variables("BSE_Double_Compact_Objects", "Merges_Hubble_Time")[self.DCOmask] |
| 188 | + |
| 189 | + def recalculateTrueSolarMassEvolved(self, Mlower, Mupper, binaryFraction): |
| 190 | + # Possibility to test assumptions of True solar mass evolved |
| 191 | + self.Mlower = Mlower |
| 192 | + self.Mupper = Mupper |
| 193 | + self.binaryFraction = binaryFraction |
| 194 | + _, self.totalMassEvolvedPerZ = MPZ.totalMassEvolvedPerZ( |
| 195 | + pathCOMPASh5=self.path, |
| 196 | + Mlower=self.Mlower, |
| 197 | + Mupper=self.Mupper, |
| 198 | + binaryFraction=self.binaryFraction, |
| 199 | + ) |
| 200 | + |
| 201 | + def get_COMPAS_variables(self, hdf5_file, var_names): |
| 202 | + """ |
| 203 | + Get a variable or variables from a COMPAS file |
| 204 | +
|
| 205 | + Args: |
| 206 | + hdf5_file --> [string] Name of HDF5 subfile (e.g. "BSE_Double_Compact_Objects") |
| 207 | + var_names --> [string or string list] A variable name or list of variables names to return |
| 208 | +
|
| 209 | + Returns: |
| 210 | + var_list --> [list of lists] A list of variables (or a single variable if only one name supplied) |
| 211 | + """ |
| 212 | + # open the COMPAS file |
| 213 | + with h5.File(self.path, "r") as compas_file: |
| 214 | + # if the list is only a string (i.e. one variable) then don't return a list |
| 215 | + if isinstance(var_names, str): |
| 216 | + return compas_file[hdf5_file][var_names][...].squeeze() |
| 217 | + # else return each variable in a list |
| 218 | + else: |
| 219 | + return [compas_file[hdf5_file][var_name][...].squeeze() for var_name in var_names] |
| 220 | + |
| 221 | + def set_sw_weights(self, column_name): |
| 222 | + """ Set STROOPWAFEL adaptive sampling weights given a column name in the BSE_Double_Compact_Objects file """ |
| 223 | + if column_name is not None: |
| 224 | + self.sw_weights = self.get_COMPAS_variables("BSE_Double_Compact_Objects", column_name)[self.DCOmask] |
| 225 | + |
| 226 | + def find_star_forming_mass_per_binary_sampling(self): |
| 227 | + self.mass_evolved_per_binary = MPZ.analytical_star_forming_mass_per_binary_using_kroupa_imf( |
| 228 | + m1_max=self.Mupper.value, |
| 229 | + m1_min=self.Mlower.value, |
| 230 | + m2_min=self.m2_min.value, |
| 231 | + fbin=self.binaryFraction, |
| 232 | + ) |
| 233 | + |
| 234 | + |
0 commit comments