|
| 1 | +import h5py as h5 |
| 2 | +import numpy as np |
| 3 | +import pandas as pd |
| 4 | +from numpy.dtypes import StringDType |
| 5 | + |
| 6 | + |
| 7 | +######################################################################## |
| 8 | +# ## Function to print the data from a given COMPAS HDF5 group in a readable pandas template |
| 9 | + |
| 10 | +def printCompasDetails(data, *seeds, mask=()): |
| 11 | + """ |
| 12 | + Function to print the full Compas output for given seeds, optionally with an additional mask |
| 13 | + """ |
| 14 | + list_of_keys = list(data.keys()) |
| 15 | + |
| 16 | + # Check if seed parameter exists - if not, just print without (e.g RunDetails) |
| 17 | + if ('SEED' in list_of_keys) | ('SEED>MT' in list_of_keys): # Most output files |
| 18 | + #SEED>MT is a relic from older versions, but we leave this in for backwards compatibility |
| 19 | + |
| 20 | + # Set the seed name parameter, mask on seeds as needed, and set the index |
| 21 | + seedVariableName='SEED' if ('SEED' in list_of_keys) else 'SEED>MT' |
| 22 | + list_of_keys.remove(seedVariableName) # this is the index above, don't want to include it |
| 23 | + |
| 24 | + allSeeds = data[seedVariableName][()] |
| 25 | + seedsMask = np.isin(allSeeds, seeds) |
| 26 | + if len(seeds) == 0: # if any seed is included, do not reset the mask |
| 27 | + seedsMask = np.ones_like(allSeeds).astype(bool) |
| 28 | + if mask == (): |
| 29 | + mask = np.ones_like(allSeeds).astype(bool) |
| 30 | + mask &= seedsMask |
| 31 | + |
| 32 | + df = pd.DataFrame.from_dict({param: data[param][()][mask] for param in list(data.keys())}).set_index(seedVariableName).T |
| 33 | + |
| 34 | + else: # No seed parameter, so do custom print for Run Details |
| 35 | + |
| 36 | + # Get just the keys without the -Derivation suffix - those will be a second column |
| 37 | + keys_not_derivations = [] |
| 38 | + for key in list_of_keys: |
| 39 | + if '-Derivation' not in key: |
| 40 | + keys_not_derivations.append(key) |
| 41 | + |
| 42 | + # Some parameter values are string types, formatted as np.bytes_, need to convert back |
| 43 | + def convert_strings(param_array): |
| 44 | + if isinstance(param_array[0], np.bytes_): |
| 45 | + return param_array.astype(str) |
| 46 | + else: |
| 47 | + return param_array |
| 48 | + |
| 49 | + df_keys = pd.DataFrame.from_dict({param: convert_strings(data[param][()]) for param in keys_not_derivations }).T |
| 50 | + nCols = df_keys.shape[1] # Required only because if we combine RDs, we get many columns (should fix later) |
| 51 | + df_keys.columns = ['Parameter']*nCols |
| 52 | + df_drvs = pd.DataFrame.from_dict({param: convert_strings(data[param+'-Derivation'][()]) for param in keys_not_derivations }).T |
| 53 | + df_drvs.columns = ['Derivation']*nCols |
| 54 | + df = pd.concat([df_keys, df_drvs], axis=1) |
| 55 | + |
| 56 | + # Add units as first col |
| 57 | + units_dict = {key:data[key].attrs['units'].astype(str) for key in list_of_keys} |
| 58 | + df.insert(loc=0, column='(units)', value=pd.Series(units_dict)) |
| 59 | + return df |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +######################################################################## |
| 64 | +# ## Get event histories of MT data, SN data, and combined MT, SN data |
| 65 | + |
| 66 | +def getMtEvents(MT): |
| 67 | + """ |
| 68 | + This function takes in the `BSE_RLOF` output category from COMPAS, and returns the information |
| 69 | + on the Mass Transfer (MT) events that happen for each seed. The events do not have to be in order, |
| 70 | + either chronologically or by seed, this function will reorder them as required. |
| 71 | + |
| 72 | + OUT: |
| 73 | + returnedSeeds (list): ordered list of the unique seeds in the MT file |
| 74 | + returnedEvents (list): list of sublists, where each sublist contains all the MT events for a given seed. |
| 75 | + MT event tuples take the form : |
| 76 | + (stellarTypePrimary, stellarTypeSecondary, isRlof1, isRlof2, isCEE, isMrg) |
| 77 | + returnTimes (list): is a list of sublists of times of each of the MT events |
| 78 | + """ |
| 79 | + mtSeeds = MT['SEED'][()] |
| 80 | + mtTimes = MT['Time<MT'][()] |
| 81 | + mtPrimaryStype = MT['Stellar_Type(1)<MT'][()] |
| 82 | + mtSecondaryStype = MT['Stellar_Type(2)<MT'][()] |
| 83 | + mtIsRlof1 = MT['RLOF(1)>MT'][()] == 1 |
| 84 | + mtIsRlof2 = MT['RLOF(2)>MT'][()] == 1 |
| 85 | + mtIsCEE = MT['CEE>MT'][()] == 1 |
| 86 | + mtIsMrg = MT['Merger'][()] == 1 |
| 87 | + |
| 88 | + # We want the return arrays sorted by seed, so sort here. |
| 89 | + mtSeedsInds = np.lexsort((mtTimes, mtSeeds)) # sort by seeds then times - lexsort sorts by the last column first... |
| 90 | + mtSeeds = mtSeeds[mtSeedsInds] |
| 91 | + mtTimes = mtTimes[mtSeedsInds] |
| 92 | + mtPrimaryStype = mtPrimaryStype[mtSeedsInds] |
| 93 | + mtSecondaryStype = mtSecondaryStype[mtSeedsInds] |
| 94 | + mtIsRlof1 = mtIsRlof1[mtSeedsInds] |
| 95 | + mtIsRlof2 = mtIsRlof2[mtSeedsInds] |
| 96 | + mtIsCEE = mtIsCEE[mtSeedsInds] |
| 97 | + mtIsMrg = mtIsMrg[mtSeedsInds] |
| 98 | + |
| 99 | + # Process the MT events |
| 100 | + returnedSeeds = [] # array of seeds - will only contain seeds that have MT events |
| 101 | + returnedEvents = [] # array of MT events for each seed in returnedSeeds |
| 102 | + returnedTimes = [] # array of times for each event in returnedEvents (for each seed in returnedSeeds) |
| 103 | + |
| 104 | + lastSeed = -1 # initialize most recently processed seed |
| 105 | + for seedIndex, thisSeed in enumerate(mtSeeds): # iterate over all RLOF file entries |
| 106 | + thisTime = mtTimes[seedIndex] # time for this RLOF file entry |
| 107 | + thisEvent = (mtPrimaryStype[seedIndex], mtSecondaryStype[seedIndex], |
| 108 | + mtIsRlof1[seedIndex], mtIsRlof2[seedIndex], mtIsCEE[seedIndex], mtIsMrg[seedIndex]) # construct event tuple |
| 109 | + |
| 110 | + # If this is an entirely new seed: |
| 111 | + if thisSeed != lastSeed: # same seed as last seed processed? |
| 112 | + returnedSeeds.append(thisSeed) # no - new seed, record it |
| 113 | + returnedTimes.append([thisTime]) # initialize the list of event times for this seed |
| 114 | + returnedEvents.append([thisEvent]) # initialize the list of events for this seed |
| 115 | + lastSeed = thisSeed # update the latest seed |
| 116 | + |
| 117 | + # Add event, if it is not a duplicate |
| 118 | + try: |
| 119 | + eventIndex = returnedEvents[-1].index(thisEvent) # find eventIndex of this particular event tuple in the array of events for this seed |
| 120 | + if thisTime > returnedTimes[-1][eventIndex]: # ^ if event is not a duplicate, this will throw a ValueError |
| 121 | + returnedTimes[-1][eventIndex] = thisTime # if event is duplicate, update time to the later of the duplicates |
| 122 | + except ValueError: # event is not a duplicate: |
| 123 | + returnedEvents[-1].append(thisEvent) # record new event tuple for this seed |
| 124 | + returnedTimes[-1].append(thisTime) # record new event time for this seed |
| 125 | + |
| 126 | + return returnedSeeds, returnedEvents, returnedTimes # see above for description |
| 127 | + |
| 128 | + |
| 129 | +def getSnEvents(SN): |
| 130 | + """ |
| 131 | + This function takes in the `BSE_Supernovae` output category from COMPAS, and returns the information |
| 132 | + on the Supernova (SN) events that happen for each seed. The events do not have to be in order chronologically, |
| 133 | + this function will reorder them as required. |
| 134 | + |
| 135 | + OUT: |
| 136 | + returnedSeeds (list): ordered list of all the unique seeds in the SN file |
| 137 | + returnedEvents (list): list of sublists, where each sublist contains all the SN events for a given seed. |
| 138 | + SN event tuples take the form : |
| 139 | + (stellarTypeProgenitor, stellarTypeRemnant, whichStarIsProgenitor, isBinaryUnbound) |
| 140 | + returnedTimes (list): is a list of sublists of times of each of the SN events |
| 141 | + """ |
| 142 | + snSeeds = SN['SEED'][()] |
| 143 | + snTimes = SN['Time'][()] |
| 144 | + snProgStype = SN['Stellar_Type_Prev(SN)'][()] |
| 145 | + snRemnStype = SN['Stellar_Type(SN)'][()] |
| 146 | + snWhichProg = SN['Supernova_State'][()] |
| 147 | + snIsUnbound = SN['Unbound'][()] == 1 |
| 148 | + |
| 149 | + # We want the return arrays sorted by seed, so sort here. |
| 150 | + snSeedsInds = np.lexsort((snTimes, snSeeds)) # sort by seeds then times - lexsort sorts by the last column first... |
| 151 | + snSeeds = snSeeds[snSeedsInds] |
| 152 | + snTimes = snTimes[snSeedsInds] |
| 153 | + snProgStype = snProgStype[snSeedsInds] |
| 154 | + snRemnStype = snRemnStype[snSeedsInds] |
| 155 | + snWhichProg = snWhichProg[snSeedsInds] |
| 156 | + snIsUnbound = snIsUnbound[snSeedsInds] |
| 157 | + |
| 158 | + # Process the SN events |
| 159 | + returnedSeeds = [] # array of seeds - will only contain seeds that have SN events |
| 160 | + returnedEvents = [] # array of SN events for each seed in returnedSeeds |
| 161 | + returnedTimes = [] # array of times for each event in returnedEvents (for each seed in returnedSeeds) |
| 162 | + |
| 163 | + lastSeed = -1 # initialize most recently processed seed |
| 164 | + for seedIndex, thisSeed in enumerate(snSeeds): # iterate over all SN file entries |
| 165 | + thisTime = snTimes[seedIndex] # time for this SN file entry |
| 166 | + thisEvent = (snProgStype[seedIndex], snRemnStype[seedIndex], |
| 167 | + snWhichProg[seedIndex], snIsUnbound[seedIndex]) # construct event tuple |
| 168 | + |
| 169 | + # If this is an entirely new seed: |
| 170 | + if thisSeed != lastSeed: # same seed as last seed processed? |
| 171 | + returnedSeeds.append(thisSeed) # no - new seed, record it |
| 172 | + returnedTimes.append([thisTime]) # initialize the list of event times for this seed |
| 173 | + returnedEvents.append([thisEvent]) # initialize the list of events for this seed |
| 174 | + lastSeed = thisSeed # update the latest seed |
| 175 | + else: # yes - second SN event for this seed |
| 176 | + returnedTimes[-1].append(thisTime) # append time at end of array |
| 177 | + returnedEvents[-1].append(thisEvent) # append event at end of array |
| 178 | + |
| 179 | + return returnedSeeds, returnedEvents, returnedTimes # see above for description |
| 180 | + |
| 181 | + |
| 182 | +def getEventHistory(h5file, exclude_null=False): |
| 183 | + """ |
| 184 | + Get the event history for all seeds, including both RLOF and SN events, in chronological order. |
| 185 | + IN: |
| 186 | + h5file (h5.File() type): COMPAS HDF5 output file |
| 187 | + exclude_null (bool): whether or not to exclude seeds which undergo no RLOF or SN events |
| 188 | + OUT: |
| 189 | + returnedSeeds (list): ordered list of all seeds in the output |
| 190 | + returnedEvents (list): a list (corresponding to the ordered seeds above) of the |
| 191 | + collected SN and MT events from the getMtEvents and getSnEvents functions above, |
| 192 | + themselves ordered chronologically |
| 193 | + """ |
| 194 | + SP = h5file['BSE_System_Parameters'] |
| 195 | + MT = h5file['BSE_RLOF'] |
| 196 | + SN = h5file['BSE_Supernovae'] |
| 197 | + allSeeds = SP['SEED'][()] # get all seeds |
| 198 | + mtSeeds, mtEvents, mtTimes = getMtEvents(MT) # get MT events |
| 199 | + snSeeds, snEvents, snTimes = getSnEvents(SN) # get SN events |
| 200 | + |
| 201 | + numMtSeeds = len(mtSeeds) # number of MT events |
| 202 | + numSnSeeds = len(snSeeds) # number of SN events |
| 203 | + |
| 204 | + if numMtSeeds < 1 and numSnSeeds < 1: return [] # no events - return empty history |
| 205 | + |
| 206 | + returnedSeeds = [] # array of seeds - will only contain seeds that have events (of any type) |
| 207 | + returnedEvents = [] # array of events - same size as returnedSeeds (includes event times) |
| 208 | + |
| 209 | + eventOrdering = ['RL', 'CE', 'SN', 'MG'] # order of preference for simultaneous events |
| 210 | + |
| 211 | + mtIndex = 0 # index into MT events arrays |
| 212 | + snIndex = 0 # index into SN events arrays |
| 213 | + |
| 214 | + if exclude_null: |
| 215 | + seedsToIterate = np.sort(np.unique(np.append(mtSeeds, snSeeds))) # iterate over all the seeds that have either MT or SN events |
| 216 | + else: |
| 217 | + seedsToIterate = allSeeds |
| 218 | + |
| 219 | + idxOrdered = np.argsort(seedsToIterate) |
| 220 | + returnedSeeds = [None] * np.size(seedsToIterate) # array of seeds - will only contain seeds that have events (of any type) |
| 221 | + returnedEvents = [None] * np.size(seedsToIterate) # array of events - same size as returnedSeeds (includes event times) |
| 222 | + |
| 223 | + for idx in idxOrdered: |
| 224 | + seed = seedsToIterate[idx] |
| 225 | + seedEvents = [] # initialise the events for the seed being processed |
| 226 | + |
| 227 | + # Collect any MT events for this seed, add the time of the event and the event type |
| 228 | + while mtIndex < numMtSeeds and mtSeeds[mtIndex] == seed: |
| 229 | + for eventIndex, event in enumerate(mtEvents[mtIndex]): |
| 230 | + _, _, isRL1, isRL2, isCEE, isMrg = event |
| 231 | + eventKey = 'MG' if isMrg else 'CE' if isCEE else 'RL' # event type: Mrg, CEE, RLOF 1->2, RLOF 2->1 |
| 232 | + seedEvents.append((eventKey, mtTimes[mtIndex][eventIndex], *mtEvents[mtIndex][eventIndex])) |
| 233 | + mtIndex += 1 |
| 234 | + |
| 235 | + # Collect any SN events for this seed, add the time of the event and the event type |
| 236 | + while snIndex < numSnSeeds and snSeeds[snIndex] == seed: |
| 237 | + for eventIndex, event in enumerate(snEvents[snIndex]): |
| 238 | + eventKey = 'SN' |
| 239 | + seedEvents.append((eventKey, snTimes[snIndex][eventIndex], *snEvents[snIndex][eventIndex])) |
| 240 | + snIndex += 1 |
| 241 | + |
| 242 | + seedEvents.sort(key=lambda ev:(ev[1], eventOrdering.index(ev[0]))) # sort the events by time and event type (MT before SN if at the same time) |
| 243 | + |
| 244 | + returnedSeeds[idx] = seed # record the seed in the seeds array being returned |
| 245 | + returnedEvents[idx] = seedEvents # record the events for this seed in the events array being returned |
| 246 | + |
| 247 | + return returnedSeeds, returnedEvents # see above for details |
| 248 | + |
| 249 | + |
| 250 | + |
| 251 | +########################################### |
| 252 | +# ## Produce strings of the event histories |
| 253 | + |
| 254 | +stellarTypeDict = { |
| 255 | + 0: 'MS', |
| 256 | + 1: 'MS', |
| 257 | + 2: 'HG', |
| 258 | + 3: 'GB', |
| 259 | + 4: 'GB', |
| 260 | + 5: 'GB', |
| 261 | + 6: 'GB', |
| 262 | + 7: 'HE', |
| 263 | + 8: 'HE', |
| 264 | + 9: 'HE', |
| 265 | + 10: 'WD', |
| 266 | + 11: 'WD', |
| 267 | + 12: 'WD', |
| 268 | + 13: 'NS', |
| 269 | + 14: 'BH', |
| 270 | + 15: 'MR', |
| 271 | + 16: 'MS', |
| 272 | +} |
| 273 | + |
| 274 | +def buildEventString(events, useIntStypes=False): |
| 275 | + """ |
| 276 | + Function to produce a string representing the event history of a single binary for quick readability. |
| 277 | + NOTE: unvectorized, do the vectorization later for a speed up |
| 278 | + IN: |
| 279 | + events (list of tuples): events output from getEventHistory() |
| 280 | + OUT: |
| 281 | + eventString (string): string representing the event history of the binary |
| 282 | + |
| 283 | + MT strings look like: |
| 284 | + P>S, P<S, or P=S where P is primary type, S is secondary type, |
| 285 | + and >, < is RLOF (1->2 or 1<-2) or otherwise = for CEE or & for Merger |
| 286 | +
|
| 287 | + SN strings look like: |
| 288 | + P*SR for star1 the SN progenitor,or |
| 289 | + R*SP for star2 the SN progenitor, |
| 290 | + where P is progenitor type, R is remnant type, |
| 291 | + S is state (I for intact, U for unbound) |
| 292 | +
|
| 293 | + Event strings for the same seed are separated by the undesrcore character ('_') |
| 294 | + """ |
| 295 | + def _remap_stype(int_stype): |
| 296 | + if useIntStypes: |
| 297 | + return str(int_stype) |
| 298 | + else: |
| 299 | + return stellarTypeDict[int_stype] |
| 300 | + |
| 301 | + # Empty event |
| 302 | + if len(events) == 0: |
| 303 | + return 'NA' |
| 304 | + |
| 305 | + eventStr = '' |
| 306 | + for event in events: |
| 307 | + if event[0] == 'SN': # SN event |
| 308 | + _, time, stypeP, stypeC, whichSN, isUnbound = event |
| 309 | + if whichSN == 1: # Progenitor or Remnant depending upon which star is the SN |
| 310 | + charL = _remap_stype(stypeP) |
| 311 | + charR = _remap_stype(stypeC) |
| 312 | + else: |
| 313 | + charL = _remap_stype(stypeC) |
| 314 | + charR = _remap_stype(stypeP) |
| 315 | + charM = '*u' if isUnbound else '*i' # unbound or intact |
| 316 | + else: # Any of the MT events |
| 317 | + _, time, stype1, stype2, isRL1, isRL2, isCEE, isMrg = event |
| 318 | + charL = _remap_stype(stype1) # primary stellar type |
| 319 | + charR = _remap_stype(stype2) # secondary stellar type |
| 320 | + charM = '&' if isMrg \ |
| 321 | + else '=' if isCEE \ |
| 322 | + else '<' if isRL2 \ |
| 323 | + else '>' # event type: CEE, RLOF 2->1, RLOF 1->2 |
| 324 | + eventStr += "{}{}{}_".format(charL, charM, charR) # event string for this star, _ is event separator |
| 325 | + |
| 326 | + return np.array(eventStr[:-1], dtype=np.str_) # return event string for this star (pop the last underscore first) |
| 327 | + |
| 328 | +def getEventStrings(h5file=None, allEvents=None, useIntStypes=False): |
| 329 | + """ |
| 330 | + Function to calculate the event history strings for either the entire Compas output, or some list of events |
| 331 | + IN: One of |
| 332 | + h5file (h5.File() type): COMPAS HDF5 output file |
| 333 | + allEvents (list of tuples) |
| 334 | + OUT: |
| 335 | + eventStrings (list): list of strings of the event history of each seed |
| 336 | + """ |
| 337 | + |
| 338 | + # If output is |
| 339 | + if (h5file == None) & (allEvents == None): |
| 340 | + return |
| 341 | + elif (allEvents == None): |
| 342 | + _, allEvents = getEventHistory(h5file) |
| 343 | + |
| 344 | + eventStrings = np.zeros(len(allEvents), dtype=StringDType()) |
| 345 | + for ii, eventsForGivenSeed in enumerate(allEvents): |
| 346 | + eventString = buildEventString(eventsForGivenSeed, useIntStypes=useIntStypes) |
| 347 | + eventStrings[ii] = eventString # append event string for this star (pop the last underscore first) |
| 348 | + |
| 349 | + return eventStrings |
| 350 | + |
| 351 | +"" |
| 352 | + |
| 353 | + |
| 354 | +"" |
| 355 | + |
0 commit comments