def __init__(self, args): """Load star data of entire simulation.""" # Get birth time and mass of all stars in this halo. vr_snap = hd.read_attribute(f'{args.wdir}{args.bh_file}', 'Haloes', 'VR_Snapshot') if vr_snap is None: self.is_set_up = False return snap_file = f'{args.wdir}{args.snap_name}_{vr_snap:04d}.hdf5' stellar_aexp = hd.read_data(snap_file, 'PartType4/BirthScaleFactors') self.birth_times = aexp_to_time(stellar_aexp) self.mass = hd.read_data(snap_file, 'PartType4/InitialMasses') * 1e10 self.ids = hd.read_data(snap_file, 'PartType4/ParticleIDs') coordinates = hd.read_data(snap_file, 'PartType4/Coordinates') coordinates *= (hd.read_attribute(snap_file, 'Header', 'Scale-factor')[0]) # Look up star IDs in VR vr_file = f'{args.wdir}{args.vr_file}_{vr_snap:04d}' self.haloes, vr_zred, vr_aexp = xl.connect_ids_to_vr(self.ids, vr_file, require=True) # Get radii of stars halo_centres = hd.read_data(f'{vr_file}.hdf5', 'MinimumPotential/Coordinates') self.radii = np.linalg.norm(coordinates - halo_centres[self.haloes, :], axis=1) * 1e3 ind_not_in_halo = np.nonzero(self.haloes < 0)[0] self.radii[ind_not_in_halo] = -1 self.is_set_up = True
def __init__(self, vr_file, sim_type='Hydro'): self.vr_file = f'{vr_file}.hdf5' self.vr_part_file = f'{vr_file}_particles.hdf5' self.ids = hd.read_data(self.vr_part_file, 'Haloes/IDs') if sim_type.lower().startswith('dm'): self.ids *= 2 self.ptypes = hd.read_data(self.vr_part_file, 'Haloes/PartTypes') self.offsets = hd.read_data(self.vr_part_file, 'Haloes/Offsets') self.n_haloes = len(self.offsets) - 1
def process_sim(isim, args): """Process one individual simulation""" if isinstance(isim, int): wdirs = glob.glob(f'{args.basedir}/ID{isim}*/') if len(wdirs) != 1: set_trace() wdir = wdirs[0] else: wdir = args.basedir + '/' + isim + '/' print(f"Analysing simulation {wdir}...") bfile = wdir + args.catloc if not os.path.isfile(bfile): print(f"Could not find BH data file for simulation {isim}.") return # Copy out the desired fields for field in args.bh_fields: data = hd.read_data(bfile, field) if data is None: print(f"Could not load data set '{field}'!") set_trace() hd.write_data(args.outfile, f'ID{isim}/{field}', data) # Copy out metadata fields times = hd.read_data(bfile, 'Times') redshifts = hd.read_data(bfile, 'Redshifts') first_indices = hd.read_data(bfile, 'FirstIndices') vr_haloes = hd.read_data(bfile, 'Haloes') vr_halo_mstar = hd.read_data(bfile, 'Halo_MStar') vr_halo_sfr = hd.read_data(bfile, 'Halo_SFR') vr_halo_m200c = hd.read_data(bfile, 'Halo_M200c') vr_halo_types = hd.read_data(bfile, 'HaloTypes') vr_halo_flag = hd.read_data(bfile, 'Flag_MostMassiveInHalo') hd.write_data(args.outfile, f'ID{isim}/Times', times) hd.write_data(args.outfile, f'ID{isim}/Redshifts', redshifts) hd.write_data(args.outfile, f'ID{isim}/FirstIndices', first_indices) if vr_haloes is not None: hd.write_data(args.outfile, f'ID{isim}/Haloes', vr_haloes) hd.write_data(args.outfile, f'ID{isim}/Halo_MStar', vr_halo_mstar) hd.write_data(args.outfile, f'ID{isim}/Halo_SFR', vr_halo_sfr) hd.write_data(args.outfile, f'ID{isim}/Halo_M200c', vr_halo_m200c) hd.write_data(args.outfile, f'ID{isim}/Halo_Types', vr_halo_types) hd.write_data(args.outfile, f'ID{isim}/Halo_FlagMostMassiveBH', vr_halo_flag)
def connect_ids_to_vr(ids, vr_file, require=False): """Core function to find the VR halo of a set of IDs.""" num_ids = len(ids) vr_particle_file = f'{vr_file}_particles.hdf5' vr_main_file = f'{vr_file}.hdf5' # Abort if VR catalogue could not be found if ((not os.path.isfile(vr_particle_file)) or (not os.path.isfile(vr_main_file))): print(f"VR catalogue {vr_file} does not exist...") if require: set_trace() return None # Find redshift of VR catalogue aexp = float(hd.read_attribute(vr_main_file, 'SimulationInfo', 'ScaleFactor')) zred = 1/aexp - 1 print(f"Connecting to VR catalogue {vr_file} at redshift {zred}...") # Load VR particle IDs vr_ids = hd.read_data(vr_particle_file, 'Haloes/IDs') vr_offsets = hd.read_data(vr_particle_file, 'Haloes/Offsets') # Locate 'our' IDss in the VR ID list print("Locating IDs in VR list...") stime = time.time() ind_in_vr, found_in_vr = hx.find_id_indices(ids, vr_ids) print(f"... took {(time.time() - stime):.3f} sec., located " f"{len(found_in_vr)} " f"/ {num_ids} IDs in VR list ({len(found_in_vr)/num_ids*100:.3f}%).") # Now convert VR particle indices to halo indices halo_guess = np.searchsorted(vr_offsets, ind_in_vr[found_in_vr], side='right') - 1 ind_good = np.nonzero(ind_in_vr[found_in_vr] < vr_offsets[halo_guess+1])[0] vr_halo = np.zeros(num_ids, dtype=int) - 1 vr_halo[found_in_vr[ind_good]] = halo_guess[ind_good] print(f"... could match {len(ind_good)} / {num_ids} IDs to haloes. " f"({len(ind_good)/num_ids*100:.3f}%).") return vr_halo, aexp, zred
def add_eagle(ax, eagle_name, aexp, linestyle='-', legend_y=None): """Add distribution from an EAGLE simulation.""" eagle_aexps = hd.read_data('./comparison_data/EAGLE_birth_densities.hdf5', f'{eagle_name}/ScaleFactors') isnap_eagle = np.argmin(np.abs(aexp - eagle_aexps)) print(f"Adding EAGLE snap {isnap_eagle}...") eagle_x = hd.read_data('./comparison_data/EAGLE_birth_densities.hdf5', f'{eagle_name}/S{isnap_eagle}/nH_by_nCrit') eagle_y = hd.read_data('./comparison_data/EAGLE_birth_densities.hdf5', f'{eagle_name}/S{isnap_eagle}/CumulativeMassFraction') plt.plot(eagle_x, eagle_y, color='grey', linestyle=linestyle) if legend_y is not None: xl.legend_item(ax, [0.03, 0.1], legend_y, eagle_name.replace('_', '\_'), color='grey', ls=linestyle, fontsize=9)
def get_birth_densities(args, isnap): """Get the birth densities of all stars in a given snapshot.""" snap_file = f'{args.wdir}{args.snap_name}_{isnap:04d}.hdf5' birth_densities = hd.read_data(snap_file, 'PartType4/BirthDensities') masses = hd.read_data(snap_file, 'PartType4/InitialMasses') # Get conversion factor to n_H [cm^-3] m_p = 1.673e-24 # Proton mass in g X_H = 0.752 # Hydrogen mass fraction (primordial) rho_to_cgs_factor = hd.read_attribute( snap_file, 'PartType4/BirthDensities', 'Conversion factor to physical CGS (including cosmological ' 'corrections)') rho_to_nH_cgs = (X_H / m_p) * (rho_to_cgs_factor) # Convert the densities to the "critical density" (see Crain+15) n_crit = 10.0 * (1.81)**(-1/2) # in n_H [cm^-3] return np.log10(birth_densities * rho_to_nH_cgs / n_crit), masses
def plot_sfr(loc, color, label, simtype='Swift', boxsize=25, linewidth=1.0, linestyle='-', alpha=1.0): print(f"Plotting SFR for {label}...") if not os.path.exists(loc): print(f"Could not find '{loc}'!") return if simtype == 'Swift': ls = '-' else: ls = ':' if loc.endswith('.hdf5'): aexp = hd.read_data(loc, 'aExp') sfr = hd.read_data(loc, 'SFR') / (boxsize**3) elif loc.endswith('.txt'): sfrdata = ascii.read(loc) if simtype == 'Swift': aexp = np.array(sfrdata['col3']) sfr = np.array(sfrdata['col8']) / (boxsize**3) * 1.022690e-2 else: aexp = np.array(sfrdata['col1']) sfr = np.array(sfrdata['col3']) / (boxsize**3) else: print(f'Incompatible file ending "{loc}"') set_trace() ind_plot = np.linspace(0, len(aexp), 2000, endpoint=False).astype(int) plt.plot(aexp[ind_plot], sfr[ind_plot], color=color, linewidth=linewidth, label=label, linestyle=linestyle, alpha=alpha) print("... done!")
def write_vr_plot(writer, ibh, isnap, bh_list, plotdata_file, iiplot, iplot, plot_prefix): """Write one single VR plot.""" plotloc = (f'{plot_prefix}_{iplot[0]}-{iplot[1]}-{iplot[2]}_' f'BH-{ibh}_snap-{isnap}.png') writer.write(f'<img src="{plotloc}" alt="{iplot[0]}-{iplot[1]}-' f'{iplot[2]}" ') if os.path.isfile(plotdata_file): writer.write(f'usemap="#map-{iplot[0]}-{iplot[1]}" ') writer.write(f'width=450>\n') # Interactive map is only possible if we have the data file if not os.path.isfile(plotdata_file): return writer.write('\n') writer.write(f'<map name="map-{iplot[0]}-{iplot[1]}">') writer.write('\n') imx = hd.read_data(plotdata_file, f'S{isnap}/{iplot[0]}-{iplot[1]}/xpt') imy = hd.read_data(plotdata_file, f'S{isnap}/{iplot[0]}-{iplot[1]}/ypt') # Add a link for each individual BH to the map... for ixbh, xbh in enumerate(bh_list): if imx[ixbh] * 0 != 0 or imy[ixbh] * 0 != 0: continue x_this = int(imx[ixbh] * 450) y_this = int(imy[ixbh] * 450) rad = 5 writer.write(f'<area shape="circle" coords="{x_this}, {y_this}, ' f'{rad}" alt="BH {xbh}" href="index_bh-{xbh}_' f'snap-{isnap}.html">\n') writer.write(f'</map>\n')
def get_star_haloes(args, isnap): """Get the halo ID of all stars in a given snapshot.""" snap_file = f'{args.wdir}{args.snap_name}_{isnap:04d}.hdf5' star_ids = hd.read_data(snap_file, 'PartType4/ParticleIDs') star_coordinates = hd.read_data(snap_file, 'PartType4/Coordinates') star_coordinates *= ( hd.read_attribute(snap_file, 'Header', 'Scale-factor')[0]) vr_file = f'{args.wdir}{args.vr_name}_{isnap:04d}' star_haloes, aexp, zred = ( xl.connect_ids_to_vr(star_ids, vr_file, require=True)) # Get radii of stars halo_centres = hd.read_data(f'{vr_file}.hdf5', 'MinimumPotential/Coordinates') star_radii = np.linalg.norm(star_coordinates - halo_centres[star_haloes, :], axis=1) * 1e3 ind_not_in_halo = np.nonzero(star_haloes < 0)[0] star_radii[ind_not_in_halo] = -1 args.aexp = aexp args.zred = zred return star_haloes, star_radii, aexp
def add_coda_to_offsets(vr_part_file): """Add the coda to particle offsets.""" num_name = { 'Haloes': 'NumberOfBoundParticles_Total', 'Unbound': 'NumberOfUnboundParticles_Total', 'Groups': 'NumberOfSOParticles_Total' } for grp in ['Haloes', 'Unbound', 'Groups']: offsets = read_data(vr_part_file, f'{grp}/Offsets') num_ids = read_attribute(vr_part_file, 'Header', num_name[grp]) offsets = np.concatenate((offsets, [num_ids])) write_data(vr_part_file, f'{grp}/Offsets', offsets)
def extract_bh_data(snapfile, isim, isnap, args): """Extract BH data from one particular file""" if isinstance(isim, int): pre = f'ID{isim}/S{isnap}/BH/' else: pre = f'{isim}/S{isnap}/BH/' zred = hd.read_attribute(snapfile, 'Header', 'Redshift')[0] hd.write_attribute(args.outfile, pre, 'Redshift', zred) for field in args.bh_fields: data = hd.read_data(snapfile, 'PartType5/' + field) if data is not None: hd.write_data(args.outfile, pre + '/' + field, data)
def extract_vr_data(vrfile, isim, isnap, args): """Extract VR data from one particular file""" if isinstance(isim, int): pre = f'ID{isim}/S{isnap}/' else: pre = f'{isim}/S{isnap}/' for field in args.vr_fields: data = hd.read_data(vrfile, field[0]) if data is None: print(f"Could not find field '{field[0]}' in VR file '{vrfile}'!") #set_trace() else: hd.write_data(args.outfile, f'{pre}/{field[1]}', data)
def setup_output(args): """Set up a dict of arrays to hold the various black hole data.""" # Get the names of all existing BH data sets snapfile = args.wdir + args.snap_name + f'_{args.first_snap:04d}.hdf5' bh_datasets = hd.list_datasets(snapfile, 'PartType5') print(f"There are {len(bh_datasets)} BH data sets...") # Starting from empty dict, add one array for each data set (except IDs) output_dict = {} comment_dict = {} for dset in bh_datasets: # We don't need to load particle IDs, have these already if dset == 'ParticleIDs': continue if args.include is not None and dset not in args.include: continue if args.exclude is not None and dset in args.exclude: continue # For simplicity, read the data set in to get its shape/type data = hd.read_data(snapfile, f'PartType5/{dset}') comment = hd.read_attribute(snapfile, f'PartType5/{dset}', 'Description') if data is None: print(f"Strange -- could not read BH data set {dset}?!") set_trace() outshape = list(data.shape) outshape[0] = args.num_bhs outshape.append(args.num_bh_snaps) array = np.zeros(tuple(outshape), data.dtype) + np.nan # Add array to overall dict output_dict[dset] = array comment_dict[dset] = comment print("... finished creating output arrays.") args.times = np.zeros(args.num_bh_snaps) args.redshifts = np.zeros(args.num_bh_snaps) return output_dict, comment_dict
def process_snapshot(isim, isnap, args): """Process one snapshot.""" print("") print(f"Processing snapshot {isnap}...") snap_this = Snapshot(f'{args.wdir}{args.vr_name}_{isnap:04d}') snap_dmo = Snapshot(f'{args.dmo_dir}{args.vr_name}_{isnap:04d}', sim_type='dm-only') # Find the best match in the DMO snapshot for each halo in this sim print("\nMatching from this to DMO...") match_in_dmo, gate_this_dmo, match_frac_in_dmo = ( match_haloes(snap_this, snap_dmo)) # ... and do the same in reverse print("\nMatching from DMO to this...") match_in_this, gate_dmo_this, match_frac_in_this = ( match_haloes(snap_dmo, snap_this)) # Reject matches that are not bijective ind_non_bijective = np.nonzero((match_in_dmo < 0) | (match_in_this[match_in_dmo] != np.arange(snap_this.n_haloes)))[0] match_in_dmo[ind_non_bijective] = -1 ind_not_matched = np.nonzero(match_in_dmo < 0)[0] # Write out results vr_file_this = f'{args.wdir}{args.vr_name}_{isnap:04d}.hdf5' vr_file_dmo = f'{args.dmo_dir}{args.vr_name}_{isnap:04d}.hdf5' hd.write_data(vr_file_this, 'MatchInDMO/Haloes', match_in_dmo) hd.write_data(vr_file_this, 'MatchInDMO/MatchFractionInDMO', match_frac_in_dmo) match_frac_in_this_aligned = match_frac_in_this[match_in_dmo] match_frac_in_this_aligned[ind_not_matched] = np.nan hd.write_data(vr_file_this, 'MatchInDMO/MatchFractionInThis', match_frac_in_this_aligned) for iset in ['M200crit', 'Masses', 'MaximumCircularVelocities']: data_dmo = hd.read_data(vr_file_dmo, iset) data_dmo_aligned = data_dmo[match_in_dmo] data_dmo_aligned[ind_not_matched] = np.nan hd.write_data(vr_file_this, f'MatchInDMO/{iset}', data_dmo_aligned)
def process_output(iisnap, isnap, output_dict, bpart_ids, args, bpart_rev_ids=None): """Transcribe black hole data from one simulation output file. Parameters: ----------- iisnap : int Index of currently processed output in collective array. isnap : int Simulation index of currently processed output. output_dict : dict of ndarrays Dictionary containing arrays to be filled with data. bpart_ids : ndarray The IDs of black holes to fill into output lists. args : dict of values Configuration parameters. rev : bool, optional If True, assume that bpart_ids is actually the reverse list of BH IDs. """ if iisnap % 50 == 0: print(f"Transcribing BH data for snapshot {isnap}...") stime = time.time() snapfile = args.wdir + args.snap_name + f'_{isnap:04d}.hdf5' # Get the names of all data sets to transcribe dataset_list = list(output_dict.keys()) cstime = time.time() # Load IDs of particles in current output snapshot: bpart_ids_curr = hd.read_data(snapfile, 'PartType5/ParticleIDs') # Convert them to 'Black-IDs', i.e. their index in the output list if bpart_rev_ids is not None: rstime = time.time() bh_ids = bpart_rev_ids.query(bpart_ids_curr) #print(f"Querying {isnap} took {(time.time()-rstime):.3f} sec.") ind_matched = np.nonzero(bh_ids >= 0)[0] rstime = time.time() #print(f"Checking {isnap} took {(time.time()-rstime):.3f} sec.") else: fstime = time.time() bh_ids, ind_matched = hx.find_id_indices(bpart_ids_curr, bpart_ids) print(f"FII {isnap} took {(time.time()-fstime):.3f} sec.") if len(ind_matched) != len(bpart_ids_curr): print(f"Why can't we match all BHs from output {isnap}?!?") set_trace() cetime = time.time() if iisnap % 50 == 0: print(f"... lookup took {cetime - cstime:.3f} sec.") # Load the time and redshift of current output redshift = hd.read_attribute(snapfile, 'Header', 'Redshift')[0] sim_time = hd.read_attribute(snapfile, 'Header', 'Time')[0] utime = hd.read_attribute(snapfile, 'Units', 'Unit time in cgs (U_t)')[0] utime /= (3600.0 * 24 * 365.24 * 1e9) # Convert from sec to Gyr sim_time *= utime args.times[iisnap] = sim_time args.redshifts[iisnap] = redshift # Go through all to-transcribe data sets and copy them out for dset in dataset_list: # Make sure that the output data set has the expected shape if output_dict[dset].shape[0] != len(bpart_ids): print(f"Inconsistent shape of BH output array '{dset}'.") set_trace() # Load the data, make sure this actually worked data = hd.read_data(snapfile, 'PartType5/' + dset) if data is None: print(f"Oh my goodness, why can we now not find data set " f"'{dset}' for black holes in output {isnap}?") set_trace() output_dict[dset][bh_ids, ..., iisnap] = data if iisnap % 50 == 0: print(f"... finished in {time.time() - stime:.3f} sec.")
def main(): print("Parsing input arguments...") parser = argparse.ArgumentParser(description="Parse input parameters.") parser.add_argument('sim', help='Simulation index or name to analyse') parser.add_argument('--base_dir', help=f'Simulation base directory, default: ' f'{local.BASE_DIR}', default=local.BASE_DIR) parser.add_argument('--bh_mmax', type=float, help='Maximum BH mass, for the scaling in the images ' 'in log (M/M_Sun), default: 8.5.', default=8.5) parser.add_argument('--numpix', type=int, help='Size of images in pixels, default: 1000', default=1000) parser.add_argument('--gallery_dir', default='gallery') parser.add_argument('--plot_prefix', default='vr_plots', help='Prefix for plot files (within gallery_dir), ' 'default: vr_plots') parser.add_argument('--snapshots', nargs='+', type=int, help='Snapshots for which to set up websites.') parser.add_argument('--snap_name', default='snapshot', help='Snapshot prefix, default: "snapshot".') parser.add_argument('--mstar_min', type=float, help='Minimum stellar mass of the host galaxy for ' 'a black hole to be included, in M_Sun ' '(default: 3e10)', default=3e10) parser.add_argument('--m200_min', type=float, help='Minimum M200c of the host galaxy for ' 'a black hole to be included, in M_Sun ' '(default: 0, i.e. select on stellar mass only)', default=0.0) parser.add_argument('--bh_data_file', default='black_hole_data.hdf5', help='Name of the file containing the BH data, ' 'default: "black_hole_data.hdf5"') parser.add_argument('--vr_prefix', default='vr', help='Prefix of (combined) VR catalogue, default: vr.') parser.add_argument('--snap_frontpage', type=int, help='Snapshot for images on frontpage (default: 36)', default=36) parser.add_argument('--size_frontpage', type=float, help='Size of image for front page, in pMpc ' '(default: 0.03)', default=0.03) args = parser.parse_args() if len(args.snapshots) == 0: print("No snapshots selected, aborting.") # Adjust selected front page size to closest match in list frontpage_sizeind = np.argmin( np.abs(np.array(ap_list) - args.size_frontpage)) args.size_frontpage = ap_list[frontpage_sizeind] # Construct the full working directory of the simulation args.wdir = xl.get_sim_dir(args.base_dir, args.sim) # Look up the snapshot redshifts get_snapshot_redshifts(args) args.plotdata_file = (f'{args.wdir}{args.gallery_dir}/' f'{args.plot_prefix}.hdf5') if os.path.isfile(args.plotdata_file): bh_list = hd.read_data(args.plotdata_file, 'BlackHoleBIDs') select_list = None print(f"Using pre-created BH list with {len(bh_list)} BHs.") else: # Find BHs we are intereste in, load data select_list = [["Halo_M200c", '>=', args.m200_min], ["Halo_MStar", '>=', args.mstar_min], ["Flag_MostMassiveInHalo", '==', 1], ["HaloTypes", '==', 10]] bh_list = None bh_data, bh_sel = xl.lookup_bh_data(args.wdir + args.bh_data_file, bh_props_list, select_list) if bh_list is None: bh_list = bh_sel if len(bh_list) == 0: print("No black holes selected, aborting.") return # Generate the script to auto-generate all the required images generate_image_script(args, bh_list) # Generate the script to auto-generate all the tracks generate_track_script(args, bh_list) generate_website(args, bh_data, bh_list)
def process_sim(args, isim, have_full_sim_dir): """Generate the images for one particular simulation.""" if have_full_sim_dir: args.wdir = isim else: args.wdir = xl.get_sim_dir(args.base_dir, isim) # Name of the input BH data catalogue args.catloc = f'{args.wdir}{args.bh_file}' # Find BHs we are intereste in, load data (of internal VR match) select_list = [["Halo_MStar", '>=', args.halo_mstar_range[0]], ["Halo_MStar", '<', args.halo_mstar_range[1]], ["DMO_M200c", '>=', args.halo_m200_range[0]], ["DMO_M200c", '<', args.halo_m200_range[1]]] if not args.include_subdominant_bhs: select_list.append(['Flag_MostMassiveInHalo', '==', 1]) if not args.include_satellites: select_list.append(['HaloTypes', '==', 10]) if args.bh_mass_range is not None: zreds = hd.read_data(args.wdir + args.bh_file, 'Redshifts') best_index = np.argmin(np.abs(zreds - args.bh_selection_redshift)) print(f"Best index for redshift {args.bh_selection_redshift} is " f"{best_index}.") # Subgrid masses are in 10^10 M_sun, so need to adjust selection range select_list.append( ['SubgridMasses', '>=', args.bh_mass_range[0] / 1e10, best_index]) select_list.append( ['SubgridMasses', '<=', args.bh_mass_range[1] / 1e10, best_index]) bh_props_list = ['SubgridMasses', 'Redshifts', 'ParticleIDs'] bh_data, bh_list = xl.lookup_bh_data(args.wdir + args.bh_file, bh_props_list, select_list) if len(bh_list) == 0: print("No BHs selected, aborting.") return args.sim_pointdata_loc = args.wdir + args.plot_prefix + '.hdf5' if not os.path.isdir(os.path.dirname(args.sim_pointdata_loc)): os.makedirs(os.path.dirname(args.sim_pointdata_loc)) if os.path.isfile(args.sim_pointdata_loc): shutil.move(args.sim_pointdata_loc, args.sim_pointdata_loc + '.old') hd.write_data(args.sim_pointdata_loc, 'BlackHoleBIDs', bh_list, comment='BIDs of all BHs selected for this simulation.') # Go through snapshots for iisnap, isnap in enumerate(args.snapshots): print("") print(f"Processing snapshot {isnap}...") # Need to explicitly connect BHs to VR catalogue from this snap vr_data = xl.connect_to_galaxies( bh_data['ParticleIDs'][bh_list], f'{args.wdir}{args.vr_file}_{isnap:04d}', extra_props=[('ApertureMeasurements/Projection1/30kpc/' 'Stars/HalfMassRadii', 'StellarHalfMassRad'), ('MatchInDMO/M200crit', 'DMO_M200c')]) # Add subgrid mass of BHs themselves ind_bh_cat = np.argmin( np.abs(bh_data['Redshifts'] - vr_data['Redshift'])) print(f"Using BH masses from index {ind_bh_cat}") vr_data['BH_SubgridMasses'] = ( bh_data['SubgridMasses'][bh_list, ind_bh_cat] * 1e10) n_good_m200 = np.count_nonzero(vr_data['DMO_M200c'] * 0 == 0) print(f"Have {n_good_m200} BHs with DMO_M200c, out of " f"{len(bh_list)}.") # Make plots for each BH, and "general" overview plot print("Overview plot") generate_vr_plots(args, vr_data, isnap) if not args.summary_only: for iibh, ibh in enumerate(bh_list): print(f"Make plot for BH-BID {ibh} ({iibh}/{len(bh_list)})") generate_vr_plots(args, vr_data, isnap, iibh, ibh) print("Done!")
def main(): print("Parsing input arguments...") parser = argparse.ArgumentParser(description="Parse input parameters.") parser.add_argument('sim', help='Simulation index/name to analyse') parser.add_argument('--bh_bid', type=int, help='BID of the black hole to highlight.') parser.add_argument('--base_dir', default=local.BASE_DIR, help='Base directory of the simulation, default: ' f'{local.BASE_DIR}') parser.add_argument('--bh_file', default='black_hole_data.hdf5', help='Name of the file containing the BH data, ' 'default: "black_hole_data.hdf5"') parser.add_argument('--plot_prefix', default='gallery/bh_growth_tracks', help='Prefix of output files, default: ' '"gallery/bh_growth_tracks') parser.add_argument('--vrplot_prefix', default='gallery/vr_plots', help='Prefix of VR plots, default: ' '"gallery/vr_plots".') parser.add_argument('--plot_mass_range', type=float, nargs='+', help='Min and max mass of plot range, default: ' '5.0 8.5', default=[5.0, 8.5]) parser.add_argument('--bh_mass_range', type=float, nargs='+', help='Min and max BH mass for selection, at target z ' '[M_Sun], default: no selection.') parser.add_argument('--bh_selection_redshift', type=float, default=0.0, help='Redshift at which to make BH mass selection ' '(default: 0.0)') parser.add_argument('--include_subdominant_bhs', action='store_true', help='Only show black holes that are the most massive ' 'in their galaxy, at the linked snapshot.') parser.add_argument('--halo_mstar_range', default=[3e10, 4e11], type=float, help='Min and max stellar mass of host galaxy ' '[M_Sun], default: 3e10, 4e11.', nargs='+') parser.add_argument('--halo_m200_range', default=[0, 1e16], type=float, help='Min and max M200 mass of host galaxy ' '[M_Sun], default: 0, 1e16.', nargs='+') parser.add_argument('--include_satellites', action='store_true', help='Only show BHs in central haloes, not satellites.') parser.add_argument('--show_target_only', action='store_true', help='Show only the target BH, not possible others ' 'that also match the selection criteria.') parser.add_argument('--show_median', action='store_true', help='Add the median for all selected BHs.') parser.add_argument('--summary_only', action='store_true', help='Only generate summary plot, not ones for ' 'individual BHs.') parser.add_argument('--alpha_others', type=float, default=0.25, help='Alpha value for non-target BHs, default: 0.25') args = parser.parse_args() args.wdir = xl.get_sim_dir(args.base_dir, args.sim) # Name of the input catalogue, containing all the data to plot args.catloc = f'{args.wdir}{args.bh_file}' args.plotdata_file = f'{args.wdir}{args.vrplot_prefix}.hdf5' if os.path.isfile(args.plotdata_file): bh_list = hd.read_data(args.plotdata_file, 'BlackHoleBIDs') select_list = None else: # Find BHs we are intereste in, load data select_list = [ ["Halo_MStar", '>=', args.halo_mstar_range[0]], ["Halo_MStar", '<', args.halo_mstar_range[1]], ["Halo_M200c", '>=', args.halo_m200_range[0]], ["Halo_M200c", '<', args.halo_m200_range[1]] ] if not args.include_subdominant_bhs: select_list.append(['Flag_MostMassiveInHalo', '==', 1]) if not args.include_satellites: select_list.append(['HaloTypes', '==', 10]) if args.bh_mass_range is not None: zreds = hd.read_data(args.wdir + args.bh_file, 'Redshifts') best_index = np.argmin(np.abs(zreds - args.bh_selection_redshift)) print(f"Best index for redshift {args.bh_selection_redshift} is " f"{best_index}.") # Subgrid masses are in 10^10 M_sun, so need to adjust selection range select_list.append( ['SubgridMasses', '>=', args.bh_mass_range[0]/1e10, best_index]) select_list.append( ['SubgridMasses', '<=', args.bh_mass_range[1]/1e10, best_index]) bh_list = None bh_data, bh_sel = xl.lookup_bh_data(args.wdir + args.bh_file, bh_props_list, select_list) if bh_list is None: bh_list = bh_sel generate_track_image(args, bh_data, bh_list) if args.bh_bid is None: # No target specified: process all selected BHs for iibh, ibh in enumerate(bh_list): generate_track_image(args, bh_data, bh_list, iibh) else: # Only plot the selected BH. iibh = np.nonzero(bh_list == args.bh_bid)[0] if len(iibh) == 1: generate_track_image(args, bh_data, bh_list, iibh[0]) else: print(f"Could not (unambiguously) find BH-BID {args.bh_bid} " f"in BH list.") print("Done!")
def transcribe_data(data_list, vrfile_in, outfile, kind='simple', form='scalar', mixed_source=False): """Transcribe data sets. Parameters ---------- data_list : tuple A list of the transcription keys to process. Each key is a tuple of (VR_name, Out_name, Comment, Conversion_Factor, Type_list). vrfile_in : str The VR file to transcribe data from. outfile : str The output file to store transcribed data in. kind : str The kind of transcription we are doing. Options are - 'main' --> main data transcription - 'profiles' --> transcribe profile data - 'apertures' --> transcribe aperture measurements form : str Form of data elements. Options are - 'scalar' --> simple scalar data quantity - '3darray' --> transcribe 3d array quantities - '3x3matrix' --> transcribe 3x3 matrix quantities mixed_source : bool, optional If True, index 6 specifies the source VR file and 'vrfile' is assumed to be the common base instead. """ for ikey in data_list: if len(ikey) < 5: set_trace() #if len(ikey) > 5 and kind != 'apertures': set_trace() # Deal with possibility of 'None' in Type_list (no type iteration) if ikey[4] is None: types = [None] else: types = ikey[4] # Deal with possibility of mixed-source input if mixed_source: if len(ikey) < 7: print("Need to specify source file in index 6 for " "mixed source transcription!") vrfile = vrfile_in + ikey[6] else: vrfile = vrfile_in if not os.path.isfile(vrfile): print("Could not find input VR file...") set_trace() # Some quantities use capital X/Y/Z in VR... if ikey[0] in [ 'V?c', 'V?cmbp', 'V?cminpot', '?c', '?cmbp', '?cminpot' ]: dimsyms = dimsymbols_cap else: dimsyms = dimsymbols # Iterate over aperture types (only relevant for apertures) if kind == 'apertures': n_proj = 4 ap_list = aperture_list else: n_proj = 1 ap_list = [None] for iap in ap_list: for iproj in range(n_proj): # Some special treatment for apertures if kind == 'apertures': if iproj > 0 and ikey[5] is False: break if iproj == 0: ap_prefix = 'Aperture_' ap_outfix = '/' else: ap_prefix = f'Projected_aperture_{iproj}_' ap_outfix = f'/Projection{iproj}/' else: ap_prefix = '' ap_outfix = '' # Iterate over required types for itype in types: # Construct type specifiers in in- and output if itype is None: typefix_in = '' typefix_out = '' else: typefix_in = type_in[itype] typefix_out = type_out[itype] # Adjust comment if itype is None: comment = ikey[2].replace('#', '') else: comment = ikey[2].replace('#', type_self[itype]) # Construct the full data set names in in- and output vrname = ikey[0] + typefix_in outname = typefix_out + ikey[1] # Deal with names for special cases if kind == 'profiles': outname = f'Profiles/{outname}' elif kind == 'apertures': vrname = f'{ap_prefix}{vrname}_{iap}_kpc' outname = (f'ApertureMeasurements{ap_outfix}{iap}kpc/' f'{outname}') # Transcribe data if args.verbose: print(f"{vrname} --> {outname}") if form == '3darray': outdata = np.zeros( (num_haloes, 3), dtype=np.float32) - 1 # Load individual dimensions' data sets into output for idim in range(3): vrname_dim = vrname.replace('?', dimsyms[idim]) outdata[:, idim] = read_data(vrfile, vrname_dim, require=True) elif form == '3x3matrix': outdata = np.zeros( (num_haloes, 3, 3), dtype=np.float32) - 1 for idim1 in range(3): for idim2 in range(3): vrname_dim = (vrname.replace( '?', dimsyms[idim1]).replace( '*', dimsyms[idim2])) outdata[:, idim1, idim2] = read_data(vrfile, vrname_dim, require=True) else: # Standard case (scalar quantity) outdata = read_data(vrfile, vrname, require=True) if ikey[3] is not None: outdata *= ikey[3] write_data(outfile, outname, outdata, comment=comment)
def transcribe_metadata(vrfile_base, outfile, outfile_particles): """Transcribe the metadata""" # Set up the required VR file paths vrfile = vrfile_base + 'properties' vrfile_CPU = vrfile_base + 'catalog_particles.unbound' vrfile_CP = vrfile_base + 'catalog_particles' vrfile_prof = vrfile_base + 'profiles' vrfile_CS = vrfile_base + 'catalog_SOlist' # Copy the Header groups. # In future, this should be done properly, converting strings to numbers... f_in = h5.File(vrfile, 'r') f_out = h5.File(outfile, 'w') f_in.copy('Configuration', f_out) f_in.copy('SimulationInfo', f_out) f_in.copy('UnitInfo', f_out) f_in.close() f_out.close() # Read metadata stored in datasets num_haloes_total = read_data(vrfile, 'Total_num_of_groups')[0] num_haloes = read_data(vrfile, 'Num_of_groups')[0] # Note confusing nomenclature num_groups = read_data(vrfile_prof, 'Num_of_halos')[0] # .... num_groups_total = read_data(vrfile_prof, 'Total_num_of_halos')[0] # .... file_id = read_data(vrfile, 'File_id')[0] file_num = read_data(vrfile, 'Num_of_files')[0] num_bound = read_data(vrfile_CP, 'Num_of_particles_in_groups')[0] num_bound_total = read_data(vrfile_CP, 'Total_num_of_particles_in_all_groups')[0] num_unbound = read_data(vrfile_CPU, 'Num_of_particles_in_groups')[0] num_unbound_total = read_data(vrfile_CPU, 'Total_num_of_particles_in_all_groups')[0] num_part_so = read_data(vrfile_CS, 'Num_of_particles_in_SO_regions')[0] num_part_so_total = read_data(vrfile_CS, 'Total_num_of_particles_in_SO_regions')[0] flag_inclusive_profiles = read_data(vrfile_prof, 'Inclusive_profiles_flag')[0] num_bin_edges = read_data(vrfile_prof, 'Num_of_bin_edges')[0] radial_bin_edges = read_data(vrfile_prof, 'Radial_bin_edges') type_radial_bins = read_data(vrfile_prof, 'Radial_norm')[0] # Write general metadata to 'Header' write_attribute(outfile, 'Header', 'NumberOfHaloes', num_haloes) write_attribute(outfile, 'Header', 'NumberOfHaloes_Total', num_haloes_total) write_attribute(outfile, 'Header', 'NumberOfGroups', num_groups) write_attribute(outfile, 'Header', 'NumberOfGroups_Total', num_groups_total) write_attribute(outfile, 'Header', 'NumberOfFiles', file_num) write_attribute(outfile, 'Header', 'FileID', file_id) write_attribute(outfile, 'Header', 'NumberOfBoundParticles', num_bound) write_attribute(outfile, 'Header', 'NumberOfBoundParticles_Total', num_bound_total) write_attribute(outfile, 'Header', 'NumberOfUnboundParticles', num_unbound) write_attribute(outfile, 'Header', 'NumberOfUnboundParticles_Total', num_unbound_total) write_attribute(outfile, 'Header', 'NumberOfSOParticles', num_part_so) write_attribute(outfile, 'Header', 'NumberOfSOParticles_Total', num_part_so_total) # Write profile-specific metadata directly to that group write_attribute(outfile, 'Profiles', 'Flag_Inclusive', flag_inclusive_profiles) write_attribute(outfile, 'Profiles', 'NumberOfBinEdges', num_bin_edges) write_attribute(outfile, 'Profiles', 'TypeOfBinEdges', type_radial_bins) write_attribute(outfile, 'Profiles', 'RadialBinEdges', radial_bin_edges) # Copy Header to particles file f_out = h5.File(outfile, 'r') f_part = h5.File(outfile_particles, 'w') f_out.copy('Header', f_part) f_out.close() f_part.close() return num_haloes, num_groups
def connect_to_galaxies(bpart_ids, vr_file, combined_vr=True, extra_props=None): """Connect black holes to galaxies at a specified redshift. Parameters ---------- bpart_ids : ndarray(int) The IDs of the black hole particle to match. vr_file : string VR file to connect to (None for no matching) combined_vr : bool, optional Flag for using transcribed VR, must currently be True. extra_props : list of tuples, optional Extra properties to be retrieved from the VR catalogue. If None (default), none are extracted. Entries must be of the form ('VR_dataset', 'Output_name'). Returns ------- gal_props : dict Dict with the results of the VR matching: - Haloes --> VR halo indices for each BH, -1 if unmatched - MStar --> Stellar mass of the host halo [M_Sun] within 30 pkpc, np.nan if not matched - SFR --> SFR of the host halo [M_Sun/yr] within 30 pkpc, np.nan if not matched - M200c --> M200c of the host halo [M_Sun], np.nan if not matched - HaloTypes --> Type index of the host halo, -1 if not matched - [other properties as given in extra_props dict] - Redshift --> Redshift of VR catalogue """ num_bhs = len(bpart_ids) gal_props = {} if vr_file is None: print("Skipping galaxy linking on your request...") return # Look up BH IDs in VR gal_props['Haloes'], gal_props['ScaleFactor'], gal_props['Redshift'] = ( connect_ids_to_vr(bpart_ids, vr_file, require=False)) # Add a few key properties of the haloes, for convenience ind_in_halo = np.nonzero(gal_props['Haloes'] >= 0)[0] vr_halo = gal_props['Haloes'] vr_main_file = f'{vr_file}.hdf5' vr_mstar = hd.read_data(vr_main_file, 'ApertureMeasurements/30kpc/Stars/Masses') vr_sfr = hd.read_data(vr_main_file, 'ApertureMeasurements/30kpc/SFR/') vr_m200c = hd.read_data(vr_main_file, 'M200crit') vr_haloTypes = hd.read_data(vr_main_file, 'StructureTypes') gal_props['MStar'] = np.zeros(num_bhs) + np.nan gal_props['SFR'] = np.zeros(num_bhs) + np.nan gal_props['M200c'] = np.zeros(num_bhs) + np.nan gal_props['HaloTypes'] = np.zeros(num_bhs, dtype=int) - 1 gal_props['MStar'][ind_in_halo] = vr_mstar[vr_halo[ind_in_halo]] gal_props['SFR'][ind_in_halo] = vr_sfr[vr_halo[ind_in_halo]] gal_props['M200c'][ind_in_halo] = vr_m200c[vr_halo[ind_in_halo]] gal_props['HaloTypes'][ind_in_halo] = vr_haloTypes[vr_halo[ind_in_halo]] if extra_props is not None: for iextra in extra_props: print(f"Extracting extra quantity '{iextra[0]}' --> " f"'{iextra[1]}'") vr_quant = hd.read_data(vr_main_file, iextra[0]) dtype = vr_quant.dtype nan_value = np.nan if issubclass(dtype.type, np.integer): nan_value = -999999 gal_props[iextra[1]] = np.zeros(num_bhs, dtype) + nan_value gal_props[iextra[1]][ind_in_halo] = vr_quant[vr_halo[ind_in_halo]] return gal_props
def lookup_bh_data(bh_data_file, bh_props_list, selection_list=None, bh_list=None): """Load info from BH file into arrays and find target BHs.""" bh_data = {} # Make sure all selectors are part of the bh_props_list if selection_list is not None: for isel in selection_list: if isel[0] not in bh_props_list: print(f"Selector '{isel[0]}' was not in requested BH props " f"list, adding it now...") bh_props_list.append(isel[0]) # Load the required data from the evolution tables num_bhs = None for idata in bh_props_list: data = hd.read_data(bh_data_file, idata) # May not get all data, such as VR links if data is not None: bh_data[idata] = data num_bhs = data.shape[0] # Now find the list of BHs we are interested in (based on z=0 props) if bh_list is not None: sel = bh_list elif selection_list is None: sel = None else: if num_bhs is None: # Did not load any properties, so cannot select anything. Doh. print("Cannot select any BHs, because we did not load any data.") set_trace() sel = np.arange(num_bhs, dtype=int) for selector in selection_list: # Apply requested comparison operation cmp = ops[selector[1]] try: selector_data = bh_data[selector[0]][sel] if len(selector_data.shape) == 1: ind_subsel = np.nonzero((cmp(bh_data[selector[0]][sel], selector[2])) & (bh_data[selector[0]][sel] * 0 == 0))[0] elif len(selector_data.shape) == 2: ind_subsel = np.nonzero((cmp(selector_data[:, selector[3]], selector[2])) & (selector_data[:, selector[3]] * 0 == 0))[0] else: print("3+ dimensional selector arrays not yet handled.") set_trace() except KeyError: print(f"Could not locate selector '{selector[0]}' in BH data " "file.") if 'Halo' in selector[0]: print("Assuming that halo linking was not done, skipping " "this comparison.") continue else: set_trace() # Adjust selection list sel = sel[ind_subsel] # Sort BHs by index sel = np.sort(sel) print(f"There are {len(sel)} BHs in selection list.") return bh_data, sel
dirs1 = glob.glob(f'{local.BASE_DIR}/ID{args.sim1}*/') dirs2 = glob.glob(f'{local.BASE_DIR}/ID{args.sim2}*/') if len(dirs1) != 1 or len(dirs2) != 1: set_trace() wdir1 = dirs1[0] wdir2 = dirs2[0] print(f"Analysing simulation {wdir1}") print(f"Matching into simulation {wdir2}") # Look up info about target BH in REF simulation and snapshot datafile_ref = wdir1 + f'/{args.name1}_{args.snapshot:04d}.hdf5' if args.index is not None: bh_id = hd.read_data(datafile_ref, 'PartType5/ParticleIDs', read_index=args.index) else: if args.id is None: print("Well, we need either an index or an ID!") set_trace() bh_ids = hd.read_data(datafile_ref, 'PartType5/ParticleIDs') args.index = np.nonzero(bh_ids == args.id)[0] if len(args.index) != 1: print(f"Could not unambiguously find BH ID {args.id}...") set_trace() args.index = args.index[0] bh_id = args.id bh_ft = hd.read_data(datafile_ref, 'PartType5/FormationScaleFactors',
def connect_to_galaxies(bpart_ids, args): """Connect black holes to galaxies at z = 0.""" if args.vr_snap is None: print("Skipping galaxy linking on your request...") return if args.combined_vr: args.vr_particles = args.wdir + f'{args.vr_file}_{args.vr_snap:04d}_particles.hdf5' args.vr_outfile = args.wdir + f'{args.vr_file}_{args.vr_snap:04d}.hdf5' else: print("Please transcribe VR catalogue...") set_trace() aexp = float( hd.read_attribute(args.vr_outfile, 'SimulationInfo', 'ScaleFactor')) args.vr_zred = 1 / aexp - 1 args.vr_aexp = aexp print(f"Connecting to VR snapshot {args.vr_snap} at redshift " f"{args.vr_zred}...") # Load VR particle IDs vr_ids = hd.read_data(args.vr_particles, 'Haloes/IDs') vr_nums = hd.read_data(args.vr_particles, 'Haloes/Numbers') vr_offsets = hd.read_data(args.vr_particles, 'Haloes/Offsets') # Locate 'our' BHs in the VR ID list print("Locating BHs in VR list...") stime = time.time() ind_in_vr, found_in_vr = hx.find_id_indices(bpart_ids, vr_ids) print(f"... took {(time.time() - stime):.3f} sec., located " f"{len(found_in_vr)} " f"/ {len(bpart_ids)} BHs in VR list " f"({len(found_in_vr)/len(bpart_ids)*100:.3f}%).") # Now convert VR index to halo bh_halo = np.zeros(len(bpart_ids), dtype=int) - 1 halo_guess = np.searchsorted( vr_offsets, ind_in_vr[found_in_vr], side='right') - 1 ind_good = np.nonzero(ind_in_vr[found_in_vr] < (vr_offsets[halo_guess] + vr_nums[halo_guess]))[0] bh_halo[found_in_vr[ind_good]] = halo_guess[ind_good] print(f"... could match {len(ind_good)} / {len(bpart_ids)} BHs to haloes. " f"({len(ind_good)/len(bpart_ids)*100:.3f}%).") gal_props = {'halo': bh_halo} # Add a few key properties of the haloes, for convenience ind_in_halo = found_in_vr[ind_good] vr_mstar = hd.read_data(args.vr_outfile, 'ApertureMeasurements/30kpc/Stars/Masses') vr_sfr = hd.read_data(args.vr_outfile, 'ApertureMeasurements/30kpc/SFR/') vr_m200c = hd.read_data(args.vr_outfile, 'M200crit') vr_haloTypes = hd.read_data(args.vr_outfile, 'StructureTypes') gal_props['MStar'] = np.zeros(len(bpart_ids)) gal_props['SFR'] = np.zeros(len(bpart_ids)) gal_props['M200'] = np.zeros(len(bpart_ids)) gal_props['HaloTypes'] = np.zeros(len(bpart_ids), dtype=int) gal_props['MStar'][ind_in_halo] = vr_mstar[bh_halo[ind_in_halo]] gal_props['SFR'][ind_in_halo] = vr_sfr[bh_halo[ind_in_halo]] gal_props['M200'][ind_in_halo] = vr_m200c[bh_halo[ind_in_halo]] gal_props['HaloTypes'][ind_in_halo] = vr_haloTypes[bh_halo[ind_in_halo]] return gal_props
def process_sim(isim, args): """Process one individual simulation.""" if args.have_full_sim_dir: args.wdir = isim else: args.wdir = xl.get_sim_dir(args.base_dir, isim) print(f"Analysing simulation {args.wdir}...") # Name of the input catalogue, containing all the data to plot args.catloc = f'{args.wdir}{args.bh_file}' bh_props_list = ['ParticleIDs'] # Select BHs in this sim args.plotdata_file = f'{args.wdir}{args.vrplot_prefix}.hdf5' if os.path.isfile(args.plotdata_file): bh_list = hd.read_data(args.plotdata_file, 'BlackHoleBIDs') select_list = None elif args.bh_bid is None: # Find BHs we are intereste in, load data select_list = [ ["Halo_MStar", '>=', args.halo_mstar_range[0]], ["Halo_MStar", '<', args.halo_mstar_range[1]], ["Halo_M200c", '>=', args.halo_m200_range[0]], ["Halo_M200c", '<', args.halo_m200_range[1]] ] if not args.include_subdominant_bhs: select_list.append(['Flag_MostMassiveInHalo', '==', 1]) if not args.include_satellites: select_list.append(['HaloTypes', '==', 10]) if args.bh_mass_range is not None: zreds = hd.read_data(args.wdir + args.bh_file, 'Redshifts') best_index = np.argmin(np.abs(zreds - args.bh_selection_redshift)) print(f"Best index for redshift {args.bh_selection_redshift} is " f"{best_index}.") # Subgrid masses are in 10^10 M_sun, so need to adjust selection # range select_list.append( ['SubgridMasses', '>=', args.bh_mass_range[0]/1e10, best_index]) select_list.append( ['SubgridMasses', '<=', args.bh_mass_range[1]/1e10, best_index]) bh_list = None else: bh_list = args.bh_bid select_list = None bh_file = args.wdir + args.bh_file bh_data, bh_sel = xl.lookup_bh_data(bh_file, bh_props_list, select_list) if bh_list is None: bh_list = bh_sel if len(bh_list) == 0: print("No black holes selected, aborting.") return # Overwrite selection with input, if specific BID(s) provided if args.bh_bid is not None: bh_list = args.bh_bid for isnap in args.snapshots: process_snapshot(args, bh_list, bh_data, isim, isnap)
if args.imtype in ['temp', 'diffusion_parameters', 'sfr'] and args.ptype != 0: print(f"{args.imtype} is only available for gas.") set_trace() if args.nosave: save_maps = False if zsize is None: args.zsize = args.imsize else: args.zsize = zsize if args.cambhbid is not None: black_file = args.rootdir + args.bh_data_file args.cambhid = hd.read_data(black_file, 'ParticleIDs', read_index=args.cambhbid) if not save_maps and args.noplot: print("If we don't want any output, we can stop right here.") set_trace() args.realimsize = args.imsize args.realzsize = args.zsize def image_snap(isnap): """Main function to image one specified snapshot.""" print(f"Beginning imaging snapshot {isnap}...") stime = time.time()
def image_snap(isnap): """Main function to image one specified snapshot.""" print(f"Beginning imaging snapshot {isnap}...") stime = time.time() plotloc = (args.rootdir + f'{args.outdir}/image_pt{args.ptype}_{args.imtype}_' f'{args.coda}_') if args.cambhbid is not None: plotloc = plotloc + f'BH-{args.cambhbid}_' if not os.path.isdir(os.path.dirname(plotloc)): os.makedirs(os.path.dirname(plotloc)) if not args.replot_existing and os.path.isfile( f'{plotloc}{isnap:04d}.png'): print(f"Image {plotloc}{isnap:04d}.png already exists, skipping.") return snapdir = args.rootdir + f'{args.snap_name}_{isnap:04d}.hdf5' mask = sw.mask(snapdir) # Read metadata print("Read metadata...") boxsize = max(mask.metadata.boxsize.value) ut = hd.read_attribute(snapdir, 'Units', 'Unit time in cgs (U_t)')[0] um = hd.read_attribute(snapdir, 'Units', 'Unit mass in cgs (U_M)')[0] time_int = hd.read_attribute(snapdir, 'Header', 'Time')[0] aexp_factor = hd.read_attribute(snapdir, 'Header', 'Scale-factor')[0] zred = hd.read_attribute(snapdir, 'Header', 'Redshift')[0] num_part = hd.read_attribute(snapdir, 'Header', 'NumPart_Total') time_gyr = time_int * ut / (3600 * 24 * 365.24 * 1e9) mdot_factor = (um / 1.989e33) / (ut / (3600 * 24 * 365.24)) # ----------------------- # Snapshot-specific setup # ----------------------- # Camera position camPos = None if vr_halo >= 0: print("Reading camera position from VR catalogue...") vr_file = args.rootdir + f'vr_{isnap:04d}.hdf5' camPos = hd.read_data(vr_file, 'MinimumPotential/Coordinates') elif args.varpos is not None: print("Find camera position...") if len(args.varpos) != 6: print("Need 6 arguments for moving box") set_trace() camPos = np.array([ args.varpos[0] + args.varpos[3] * time_gyr, args.varpos[1] + args.varpos[4] * time_gyr, args.varpos[2] + args.varpos[5] * time_gyr ]) print(camPos) camPos *= aexp_factor elif args.campos is not None: camPos = np.array(args.campos) * aexp_factor elif args.campos_phys is not None: camPos = np.array(args.campos) elif args.cambhid is not None: all_bh_ids = hd.read_data(snapdir, 'PartType5/ParticleIDs') args.cambh = np.nonzero(all_bh_ids == args.cambhid)[0] if len(args.cambh) == 0: print(f"BH ID {args.cambhid} does not exist, skipping.") return if len(args.cambh) != 1: print(f"Could not unambiguously find BH ID '{args.cambhid}'!") set_trace() args.cambh = args.cambh[0] if args.cambh is not None and camPos is None: camPos = hd.read_data(snapdir, 'PartType5/Coordinates', read_index=args.cambh) * aexp_factor args.hsml = hd.read_data( snapdir, 'PartType5/SmoothingLengths', read_index=args.cambh) * aexp_factor * kernel_gamma elif camPos is None: print("Setting camera position to box centre...") camPos = np.array([0.5, 0.5, 0.5]) * boxsize * aexp_factor # Image size conversion, if necessary if not args.propersize: args.imsize = args.realimsize * aexp_factor args.zsize = args.realzsize * aexp_factor else: args.imsize = args.realimsize args.zsize = args.realzsize max_sel = 1.2 * np.sqrt(3) * max(args.imsize, args.zsize) extent = np.array([-1, 1, -1, 1]) * args.imsize # Set up loading region if max_sel < boxsize * aexp_factor / 2: load_region = np.array( [[camPos[0] - args.imsize * 1.2, camPos[0] + args.imsize * 1.2], [camPos[1] - args.imsize * 1.2, camPos[1] + args.imsize * 1.2], [camPos[2] - args.zsize * 1.2, camPos[2] + args.zsize * 1.2]]) load_region = sw.cosmo_array(load_region / aexp_factor, "Mpc") mask.constrain_spatial(load_region) data = sw.load(snapdir, mask=mask) else: data = sw.load(snapdir) pt_names = ['gas', 'dark_matter', None, None, 'stars', 'black_holes'] datapt = getattr(data, pt_names[args.ptype]) pos = datapt.coordinates.value * aexp_factor # Next bit does periodic wrapping def flip_dim(idim): full_box_phys = boxsize * aexp_factor half_box_phys = boxsize * aexp_factor / 2 if camPos[idim] < min(max_sel, half_box_phys): ind_high = np.nonzero(pos[:, idim] > half_box_phys)[0] pos[ind_high, idim] -= full_box_phys elif camPos[idim] > max(full_box_phys - max_sel, half_box_phys): ind_low = np.nonzero(pos[:, idim] < half_box_phys)[0] pos[ind_low, idim] += full_box_phys for idim in range(3): print(f"Periodic wrapping in dimension {idim}...") flip_dim(idim) rad = np.linalg.norm(pos - camPos[None, :], axis=1) ind_sel = np.nonzero(rad < max_sel)[0] pos = pos[ind_sel, :] # Read BH properties, if they exist if num_part[5] > 0 and not args.nobh: bh_hsml = (hd.read_data(snapdir, 'PartType5/SmoothingLengths') * aexp_factor) bh_pos = hd.read_data(snapdir, 'PartType5/Coordinates') * aexp_factor bh_mass = hd.read_data(snapdir, 'PartType5/SubgridMasses') * 1e10 bh_maccr = (hd.read_data(snapdir, 'PartType5/AccretionRates') * mdot_factor) bh_id = hd.read_data(snapdir, 'PartType5/ParticleIDs') bh_nseed = hd.read_data(snapdir, 'PartType5/CumulativeNumberOfSeeds') bh_ft = hd.read_data(snapdir, 'PartType5/FormationScaleFactors') print(f"Max BH mass: {np.log10(np.max(bh_mass))}") else: bh_mass = None # Dummy value # Read the appropriate 'mass' quantity if args.ptype == 0 and args.imtype == 'sfr': mass = datapt.star_formation_rates[ind_sel] mass.convert_to_units(unyt.Msun / unyt.yr) mass = np.clip(mass.value, 0, None) # Don't care about last SFR aExp else: mass = datapt.masses[ind_sel] mass.convert_to_units(unyt.Msun) mass = mass.value if args.ptype == 0: hsml = (datapt.smoothing_lengths.value[ind_sel] * aexp_factor * kernel_gamma) elif fixedSmoothingLength > 0: hsml = np.zeros(mass.shape[0], dtype=np.float32) + fixedSmoothingLength else: hsml = None if args.imtype == 'temp': quant = datapt.temperatures.value[ind_sel] elif args.imtype == 'diffusion_parameters': quant = datapt.diffusion_parameters.value[ind_sel] else: quant = mass # Read quantities for gri computation if necessary if args.ptype == 4 and args.imtype == 'gri': m_init = datapt.initial_masses.value[ind_sel] * 1e10 # in M_sun z_star = datapt.metal_mass_fractions.value[ind_sel] sft = datapt.birth_scale_factors.value[ind_sel] age_star = (time_gyr - hy.aexp_to_time(sft, time_type='age')) * 1e9 age_star = np.clip(age_star, 0, None) # Avoid rounding issues lum_g = et.imaging.stellar_luminosity(m_init, z_star, age_star, 'g') lum_r = et.imaging.stellar_luminosity(m_init, z_star, age_star, 'r') lum_i = et.imaging.stellar_luminosity(m_init, z_star, age_star, 'i') # --------------------- # Generate actual image # --------------------- xBase = np.zeros(3, dtype=np.float32) yBase = np.copy(xBase) zBase = np.copy(xBase) if args.imtype == 'gri': image_weight_all_g, image_quant, hsml = ir.make_sph_image_new_3d( pos, lum_g, lum_g, hsml, DesNgb=desNGB, imsize=args.numpix, zpix=1, boxsize=args.imsize, CamPos=camPos, CamDir=camDir, ProjectionPlane=projectionPlane, verbose=True, CamAngle=[0, 0, rho], rollMode=0, edge_on=edge_on, treeAllocFac=10, xBase=xBase, yBase=yBase, zBase=zBase, return_hsml=True) image_weight_all_r, image_quant = ir.make_sph_image_new_3d( pos, lum_r, lum_r, hsml, DesNgb=desNGB, imsize=args.numpix, zpix=1, boxsize=args.imsize, CamPos=camPos, CamDir=camDir, ProjectionPlane=projectionPlane, verbose=True, CamAngle=[0, 0, rho], rollMode=0, edge_on=edge_on, treeAllocFac=10, xBase=xBase, yBase=yBase, zBase=zBase, return_hsml=False) image_weight_all_i, image_quant = ir.make_sph_image_new_3d( pos, lum_i, lum_i, hsml, DesNgb=desNGB, imsize=args.numpix, zpix=1, boxsize=args.imsize, CamPos=camPos, CamDir=camDir, ProjectionPlane=projectionPlane, verbose=True, CamAngle=[0, 0, rho], rollMode=0, edge_on=edge_on, treeAllocFac=10, xBase=xBase, yBase=yBase, zBase=zBase, return_hsml=False) map_maas_g = -5 / 2 * np.log10(image_weight_all_g[:, :, 1] + 1e-15) + 5 * np.log10( 180 * 3600 / np.pi) + 25 map_maas_r = -5 / 2 * np.log10(image_weight_all_r[:, :, 1] + 1e-15) + 5 * np.log10( 180 * 3600 / np.pi) + 25 map_maas_i = -5 / 2 * np.log10(image_weight_all_i[:, :, 1] + 1e-15) + 5 * np.log10( 180 * 3600 / np.pi) + 25 else: image_weight_all, image_quant = ir.make_sph_image_new_3d( pos, mass, quant, hsml, DesNgb=desNGB, imsize=args.numpix, zpix=1, boxsize=args.imsize, CamPos=camPos, CamDir=camDir, ProjectionPlane=projectionPlane, verbose=True, CamAngle=[0, 0, rho], rollMode=0, edge_on=edge_on, treeAllocFac=10, xBase=xBase, yBase=yBase, zBase=zBase, zrange=[-args.zsize, args.zsize]) # Extract surface density in M_sun [/yr] / kpc^2 sigma = np.log10(image_weight_all[:, :, 1] + 1e-15) - 6 if args.ptype == 0 and args.imtype in ['temp']: tmap = np.log10(image_quant[:, :, 1]) elif args.ptype == 0 and args.imtype in ['diffusion_parameters']: tmap = image_quant[:, :, 1] # ----------------- # Save image data # ----------------- if save_maps: maploc = plotloc + f'{isnap:04d}.hdf5' if args.imtype == 'gri' and args.ptype == 4: hd.write_data(maploc, 'g_maas', map_maas_g, new=True) hd.write_data(maploc, 'r_maas', map_maas_r) hd.write_data(maploc, 'i_maas', map_maas_i) else: hd.write_data(maploc, 'Sigma', sigma, new=True) if args.ptype == 0 and args.imtype == 'temp': hd.write_data(maploc, 'Temperature', tmap) elif args.ptype == 0 and args.imtype == 'diffusion_parameters': hd.write_data(maploc, 'DiffusionParameters', tmap) hd.write_data(maploc, 'Extent', extent) hd.write_attribute(maploc, 'Header', 'CamPos', camPos) hd.write_attribute(maploc, 'Header', 'ImSize', args.imsize) hd.write_attribute(maploc, 'Header', 'NumPix', args.numpix) hd.write_attribute(maploc, 'Header', 'Redshift', 1 / aexp_factor - 1) hd.write_attribute(maploc, 'Header', 'AExp', aexp_factor) hd.write_attribute(maploc, 'Header', 'Time', time_gyr) if bh_mass is not None: hd.write_data(maploc, 'BH_pos', bh_pos - camPos[None, :], comment='Relative position of BHs') hd.write_data(maploc, 'BH_mass', bh_mass, comment='Subgrid mass of BHs') hd.write_data( maploc, 'BH_maccr', bh_maccr, comment='Instantaneous BH accretion rate in M_sun/yr') hd.write_data(maploc, 'BH_id', bh_id, comment='Particle IDs of BHs') hd.write_data(maploc, 'BH_nseed', bh_nseed, comment='Number of seeds in each BH') hd.write_data(maploc, 'BH_aexp', bh_ft, comment='Formation scale factor of each BH') # ------------- # Plot image... # ------------- if not args.noplot: print("Obtained image, plotting...") fig = plt.figure(figsize=(args.inch, args.inch)) ax = fig.add_axes([0.0, 0.0, 1.0, 1.0]) plt.sca(ax) # Option I: we have really few particles. Plot them individually: if pos.shape[0] < 32: plt.scatter(pos[:, 0] - camPos[0], pos[:, 1] - camPos[1], color='white') else: # Main plotting regime # Case A: gri image -- very different from rest if args.ptype == 4 and args.imtype == 'gri': vmin = -args.scale[0] + np.array([-0.5, -0.25, 0.0]) vmax = -args.scale[1] + np.array([-0.5, -0.25, 0.0]) clmap_rgb = np.zeros((args.numpix, args.numpix, 3)) clmap_rgb[:, :, 2] = np.clip( ((-map_maas_g) - vmin[0]) / ((vmax[0] - vmin[0])), 0, 1) clmap_rgb[:, :, 1] = np.clip( ((-map_maas_r) - vmin[1]) / ((vmax[1] - vmin[1])), 0, 1) clmap_rgb[:, :, 0] = np.clip( ((-map_maas_i) - vmin[2]) / ((vmax[2] - vmin[2])), 0, 1) im = plt.imshow(clmap_rgb, extent=extent, aspect='equal', interpolation='nearest', origin='lower', alpha=1.0) else: # Establish image scaling if not args.absscale: ind_use = np.nonzero(sigma > 1e-15) vrange = np.percentile(sigma[ind_use], args.scale) else: vrange = args.scale print(f'Sigma range: {vrange[0]:.4f} -- {vrange[1]:.4f}') # Case B: temperature/diffusion parameter image if (args.ptype == 0 and args.imtype in ['temp', 'diffusion_parameters'] and not args.no_double_image): if args.imtype == 'temp': cmap = None elif args.imtype == 'diffusion_parameters': cmap = cmocean.cm.haline clmap_rgb = ir.make_double_image( sigma, tmap, percSigma=vrange, absSigma=True, rangeQuant=args.quantrange, cmap=cmap) im = plt.imshow(clmap_rgb, extent=extent, aspect='equal', interpolation='nearest', origin='lower', alpha=1.0) else: # Standard sigma images if args.ptype == 0: if args.imtype == 'hi': cmap = plt.cm.bone elif args.imtype == 'sfr': cmap = plt.cm.magma elif args.imtype == 'diffusion_parameters': cmap = cmocean.cm.haline else: cmap = plt.cm.inferno elif args.ptype == 1: cmap = plt.cm.Greys_r elif args.ptype == 4: cmap = plt.cm.bone if args.no_double_image: plotquant = tmap vmin, vmax = args.quantrange[0], args.quantrange[1] else: plotquant = sigma vmin, vmax = vrange[0], vrange[1] im = plt.imshow(plotquant, cmap=cmap, extent=extent, vmin=vmin, vmax=vmax, origin='lower', interpolation='nearest', aspect='equal') # Plot BHs if desired: if show_bhs and bh_mass is not None: if args.bh_file is not None: bh_inds = np.loadtxt(args.bh_file, dtype=int) else: bh_inds = np.arange(bh_pos.shape[0]) ind_show = np.nonzero( (np.abs(bh_pos[bh_inds, 0] - camPos[0]) < args.imsize) & (np.abs(bh_pos[bh_inds, 1] - camPos[1]) < args.imsize) & (np.abs(bh_pos[bh_inds, 2] - camPos[2]) < args.zsize) & (bh_ft[bh_inds] >= args.bh_ftrange[0]) & (bh_ft[bh_inds] <= args.bh_ftrange[1]) & (bh_mass[bh_inds] >= 10.0**args.bh_mrange[0]) & (bh_mass[bh_inds] <= 10.0**args.bh_mrange[1]))[0] ind_show = bh_inds[ind_show] if args.bh_quant == 'mass': sorter = np.argsort(bh_mass[ind_show]) sc = plt.scatter(bh_pos[ind_show[sorter], 0] - camPos[0], bh_pos[ind_show[sorter], 1] - camPos[1], marker='o', c=np.log10(bh_mass[ind_show[sorter]]), edgecolor='grey', vmin=5.0, vmax=args.bh_mmax, s=5.0, linewidth=0.2) bticks = np.linspace(5.0, args.bh_mmax, num=6, endpoint=True) blabel = r'log$_{10}$ ($m_\mathrm{BH}$ [M$_\odot$])' elif args.bh_quant == 'formation': sorter = np.argsort(bh_ft[ind_show]) sc = plt.scatter(bh_pos[ind_show[sorter], 0] - camPos[0], bh_pos[ind_show[sorter], 1] - camPos[1], marker='o', c=bh_ft[ind_show[sorter]], edgecolor='grey', vmin=0, vmax=1.0, s=5.0, linewidth=0.2) bticks = np.linspace(0.0, 1.0, num=6, endpoint=True) blabel = 'Formation scale factor' if args.bhind: for ibh in ind_show[sorter]: c = plt.cm.viridis( (np.log10(bh_mass[ibh]) - 5.0) / (args.bh_mmax - 5.0)) plt.text(bh_pos[ibh, 0] - camPos[0] + args.imsize / 200, bh_pos[ibh, 1] - camPos[1] + args.imsize / 200, f'{ibh}', color=c, fontsize=4, va='bottom', ha='left') if args.draw_hsml: phi = np.arange(0, 2.01 * np.pi, 0.01) plt.plot(args.hsml * np.cos(phi), args.hsml * np.sin(phi), color='white', linestyle=':', linewidth=0.5) # Add colour bar for BH masses if args.imtype != 'sfr': ax2 = fig.add_axes([0.6, 0.07, 0.35, 0.02]) ax2.set_xticks([]) ax2.set_yticks([]) cbar = plt.colorbar(sc, cax=ax2, orientation='horizontal', ticks=bticks) cbar.ax.tick_params(labelsize=8) fig.text(0.775, 0.1, blabel, rotation=0.0, va='bottom', ha='center', color='white', fontsize=8) # Done with main image, some embellishments... plt.sca(ax) plt.text(-0.045 / 0.05 * args.imsize, 0.045 / 0.05 * args.imsize, 'z = {:.3f}'.format(1 / aexp_factor - 1), va='center', ha='left', color='white') plt.text(-0.045 / 0.05 * args.imsize, 0.041 / 0.05 * args.imsize, 't = {:.3f} Gyr'.format(time_gyr), va='center', ha='left', color='white', fontsize=8) plot_bar() # Plot colorbar for SFR if appropriate if args.ptype == 0 and args.imtype == 'sfr': ax2 = fig.add_axes([0.6, 0.07, 0.35, 0.02]) ax2.set_xticks([]) ax2.set_yticks([]) scc = plt.scatter([-1e10], [-1e10], c=[0], cmap=plt.cm.magma, vmin=vrange[0], vmax=vrange[1]) cbar = plt.colorbar(scc, cax=ax2, orientation='horizontal', ticks=np.linspace(np.floor(vrange[0]), np.ceil(vrange[1]), 5, endpoint=True)) cbar.ax.tick_params(labelsize=8) fig.text( 0.775, 0.1, r'log$_{10}$ ($\Sigma_\mathrm{SFR}$ [M$_\odot$ yr$^{-1}$ kpc$^{-2}$])', rotation=0.0, va='bottom', ha='center', color='white', fontsize=8) ax.set_xlabel(r'$\Delta x$ [pMpc]') ax.set_ylabel(r'$\Delta y$ [pMpc]') ax.set_xlim((-args.imsize, args.imsize)) ax.set_ylim((-args.imsize, args.imsize)) plt.savefig(plotloc + str(isnap).zfill(4) + '.png', dpi=args.numpix / args.inch) plt.close() print(f"Finished snapshot {isnap} in {(time.time() - stime):.2f} sec.") print(f"Image saved in {plotloc}{isnap:04d}.png")
def find_black_ids(args): """Get the IDs of all black holes that ever existed in a simulation.""" # List holding all black hole particle IDs, starts with zero elements. particle_ids_set = set() # We only use the above set for efficient finding of new members. Actual # "membership list" is kept in a separate array, so we can keep it aligned # with the list of first snapshots. particle_ids = np.zeros(0, dtype=int) first_snaps = np.zeros(0, dtype=int) for isnap in range(args.max_snap + 1): snapfile = args.wdir + args.snap_name + f'_{isnap:04d}.hdf5' if not os.path.isfile(snapfile): continue # Load IDs of all black holes existing in current output bpart_ids = hd.read_data(snapfile, 'PartType5/ParticleIDs') if bpart_ids is None: print(f"Did not find any black holes in output {isnap}...") continue else: #print(f"Processing output {isnap}...") bpart_ids = bpart_ids.astype(int) # Update first/last-snap-with-BHs tracker args.last_snap = isnap if args.first_snap is None: args.first_snap = isnap # Check which of these are new to the club if len(particle_ids_set) == 0: ind_new = np.arange(len(bpart_ids)) else: status = [_id in particle_ids_set for _id in bpart_ids] ind_new = [i for i, val in enumerate(status) if not val] #ind_new = np.nonzero(status is False)[0] # bhids, ind_old = hx.find_id_indices(bpart_ids, particle_ids) #ind_new = np.nonzero(bhids < 0)[0] print(f"Found {len(ind_new)} new BHs in output {isnap} (out of " f"{len(bpart_ids)}).") # Subscribe all new members if len(ind_new) > 0: for inew in ind_new: particle_ids_set.add(bpart_ids[inew]) particle_ids = np.concatenate((particle_ids, bpart_ids[ind_new])) first_snaps = np.concatenate( (first_snaps, np.zeros(len(ind_new), dtype=int) + isnap)) # Done looping through outputs, report what we caught particle_ids_from_set = np.array(list(particle_ids_set)) if len(particle_ids_from_set) != len(particle_ids): print("Inconsistent number of caught BHs!") set_trace() if np.max(np.abs(np.sort(particle_ids_from_set) - np.sort(particle_ids))) > 0: print("Inconsistent IDs of caught BHs!") set_trace() args.num_bhs = len(particle_ids) if args.first_snap is not None: args.num_bh_snaps = args.last_snap - args.first_snap + 1 first_snaps -= args.first_snap else: args.num_bh_snaps = 0 print(f"Found a total of {args.num_bhs} black holes in " f"{args.num_bh_snaps} snapshots.") return particle_ids, first_snaps
def process_sim(isim, args): """Process one specific simulation.""" if args.have_full_sim_dir: args.wdir = isim else: args.wdir = xl.get_sim_dir(args.base_dir, isim) print(f"Analysing simulation {args.wdir}...") # Name of the input catalogue, containing all the data to plot args.catloc = f'{args.wdir}{args.bh_file}' # Select BHs in this sim args.plotdata_file = f'{args.wdir}{args.vrplot_prefix}.hdf5' if os.path.isfile(args.plotdata_file): bh_list = hd.read_data(args.plotdata_file, 'BlackHoleBIDs') select_list = None elif args.bh_bid is not None: select_list = None bh_list = args.bh_bid else: # Find BHs we are intereste in, load data select_list = [["Halo_MStar", '>=', args.halo_mstar_range[0]], ["Halo_MStar", '<', args.halo_mstar_range[1]], ["Halo_M200c", '>=', args.halo_m200_range[0]], ["Halo_M200c", '<', args.halo_m200_range[1]]] if not args.include_subdominant_bhs: select_list.append(['Flag_MostMassiveInHalo', '==', 1]) if not args.include_satellites: select_list.append(['HaloTypes', '==', 10]) if args.bh_mass_range is not None: zreds = hd.read_data(args.wdir + args.bh_file, 'Redshifts') best_index = np.argmin(np.abs(zreds - args.bh_selection_redshift)) print(f"Best index for redshift {args.bh_selection_redshift} is " f"{best_index}.") # Subgrid masses are in 10^10 M_sun, so need to adjust selection # range select_list.append([ 'SubgridMasses', '>=', args.bh_mass_range[0] / 1e10, best_index ]) select_list.append([ 'SubgridMasses', '<=', args.bh_mass_range[1] / 1e10, best_index ]) bh_list = None bh_file = args.wdir + args.bh_file bh_data, bh_list = xl.lookup_bh_data(bh_file, bh_props_list, select_list, bh_list) args.nsnap = len(bh_data['Times']) # Extract meta-data from Header bh_data['CodeBranch'] = hd.read_attribute(bh_file, 'Code', 'Git Branch') bh_data['CodeDate'] = hd.read_attribute(bh_file, 'Code', 'Git Date') bh_data['CodeRev'] = hd.read_attribute(bh_file, 'Code', 'Git Revision') bh_data['SimName'] = hd.read_attribute(bh_file, 'Header', 'RunName') # Look up stars data stars = Stars(args) for ibh in bh_list: process_bh(args, stars, bh_data, ibh, isim)