def test_reading_select_region_spatial(filename): """ Tests reading select regions of the volume, comparing the masks attained with spatial_only = True and spatial_only = False. """ full_data = load(filename) # Mask off the lower bottom corner of the volume. mask_region = mask(filename, spatial_only=True) mask_region_nospatial = mask(filename, spatial_only=False) restrict = array([ [0.0, 0.0, 0.0] * full_data.metadata.boxsize.units, full_data.metadata.boxsize * 0.5, ]).T mask_region.constrain_spatial(restrict=restrict) mask_region_nospatial.constrain_spatial(restrict=restrict) selected_data = load(filename, mask=mask_region) selected_data_nospatial = load(filename, mask=mask_region_nospatial) selected_coordinates = selected_data.gas.coordinates selected_coordinates_nospatial = selected_data_nospatial.gas.coordinates assert (selected_coordinates_nospatial == selected_coordinates).all() return
def test_reading_select_region_half_box(filename): """ Tests reading the spatial region and sees if it lies within the region we told it to! Specifically, we test to see if all particles lie within half a boxsize. """ full_data = load(filename) # Mask off the lower bottom corner of the volume. mask_region = mask(filename, spatial_only=True) restrict = array([ [0.0, 0.0, 0.0] * full_data.metadata.boxsize.units, full_data.metadata.boxsize * 0.49, ]).T mask_region.constrain_spatial(restrict=restrict) selected_data = load(filename, mask=mask_region) selected_coordinates = selected_data.gas.coordinates # Some of these particles will be outside because of the periodic BCs assert ( (selected_coordinates / full_data.metadata.boxsize) > 0.5).sum() < 25
def process_single_halo( path_to_snap: str, path_to_catalogue: str ): # Read in halo properties with h5py.File(f'{path_to_catalogue}', 'r') as h5file: M500c = unyt.unyt_quantity(h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass) R500c = unyt.unyt_quantity(h5file['/SO_R_500_rhocrit'][0], unyt.Mpc) XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) # Read in gas particles to compute the core-excised temperature mask = sw.mask(f'{path_to_snap}', spatial_only=False) region = [[XPotMin - 0.5 * R500c, XPotMin + 0.5 * R500c], [YPotMin - 0.5 * R500c, YPotMin + 0.5 * R500c], [ZPotMin - 0.5 * R500c, ZPotMin + 0.5 * R500c]] mask.constrain_spatial(region) mask.constrain_mask( "gas", "temperatures", 1.e5 * mask.units.temperature, 1.e10 * mask.units.temperature ) data = sw.load(f'{path_to_snap}', mask=mask) # Select hot gas within sphere and without core deltaX = data.gas.coordinates[:, 0] - XPotMin deltaY = data.gas.coordinates[:, 1] - YPotMin deltaZ = data.gas.coordinates[:, 2] - ZPotMin deltaR = np.sqrt(deltaX ** 2 + deltaY ** 2 + deltaZ ** 2) # Keep only particles inside 5 R500crit index = np.where((deltaR > 0.15 * R500c) & (deltaR < R500c))[0] data.gas.radial_distances = deltaR[index] data.gas.densities = data.gas.densities[index] data.gas.masses = data.gas.masses[index] data.gas.temperatures = data.gas.temperatures[index] data.gas.element_mass_fractions.hydrogen = data.gas.element_mass_fractions.hydrogen[index] data.gas.element_mass_fractions.helium = data.gas.element_mass_fractions.helium[index] data.gas.element_mass_fractions.carbon = data.gas.element_mass_fractions.carbon[index] data.gas.element_mass_fractions.nitrogen = data.gas.element_mass_fractions.nitrogen[index] data.gas.element_mass_fractions.oxygen = data.gas.element_mass_fractions.oxygen[index] data.gas.element_mass_fractions.neon = data.gas.element_mass_fractions.neon[index] data.gas.element_mass_fractions.magnesium = data.gas.element_mass_fractions.magnesium[index] data.gas.element_mass_fractions.silicon = data.gas.element_mass_fractions.silicon[index] data.gas.element_mass_fractions.iron = data.gas.element_mass_fractions.iron[index] Lx, Sx, Ypix = interpolate_xray(data) print(f"M_500_crit = {M500c:.3E}") print(f'LX = {np.sum(Lx):.3E}') return np.sum(Lx), Sx, Ypix
def process_single_halo( path_to_snap: str, path_to_catalogue: str ) -> Tuple[unyt.unyt_quantity]: # Read in halo properties with h5.File(f'{path_to_catalogue}', 'r') as h5file: XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) R500c = unyt.unyt_quantity(h5file['/SO_R_500_rhocrit'][0], unyt.Mpc) R200c = unyt.unyt_quantity(h5file['/R_200crit'][0], unyt.Mpc) M500c = unyt.unyt_quantity( h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass ) Mbh_aperture50kpc = unyt.unyt_quantity( h5file['Aperture_SubgridMasses_aperture_total_bh_50_kpc'][0] * 1.e10, unyt.Solar_Mass ) Mbh_max = unyt.unyt_quantity( h5file['/SubgridMasses_max_bh'][0] * 1.e10, unyt.Solar_Mass ) Mstar_bcg_50kpc = unyt.unyt_quantity( h5file['/Aperture_mass_star_50_kpc'][0] * 1.e10, unyt.Solar_Mass ) # Read in particles mask = sw.mask(f'{path_to_snap}', spatial_only=True) region = [[XPotMin - R200c, XPotMin + R200c], [YPotMin - R200c, YPotMin + R200c], [ZPotMin - R200c, ZPotMin + R200c]] mask.constrain_spatial(region) data = sw.load(f'{path_to_snap}', mask=mask) # Get positions for all BHs in the bounding region bh_positions = data.black_holes.coordinates bh_coordX = bh_positions[:, 0] - XPotMin bh_coordY = bh_positions[:, 1] - YPotMin bh_coordZ = bh_positions[:, 2] - ZPotMin bh_radial_distance = np.sqrt(bh_coordX ** 2 + bh_coordY ** 2 + bh_coordZ ** 2) # The central SMBH will probably be massive: filter above 1e8 Msun bh_masses = data.black_holes.subgrid_masses.to_physical() bh_top_massive_index = np.where(bh_masses.to('Msun').value > 1.e8)[0] massive_bh_radial_distances = bh_radial_distance[bh_top_massive_index] massive_bh_masses = bh_masses[bh_top_massive_index] # Get the central BH closest to centre of halo at z=0 central_bh_index = np.argmin(bh_radial_distance[bh_top_massive_index]) central_bh_dr = massive_bh_radial_distances[central_bh_index] central_bh_mass = massive_bh_masses[central_bh_index].to('Msun') return M500c, Mbh_aperture50kpc, Mbh_max, central_bh_mass, central_bh_dr, Mstar_bcg_50kpc
def test_subset_writer(filename): """ Test to make sure subset writing works as intended Writes a subset of the cosmological volume to a snapshot file and compares result against masked load of the original file. """ # Specify output filepath outfile = "subset_cosmological_volume.hdf5" # Create a mask mask = sw.mask(filename) boxsize = mask.metadata.boxsize # Decide which region we want to load load_region = [[0.25 * b, 0.75 * b] for b in boxsize] mask.constrain_spatial(load_region) # Write the subset write_subset(outfile, mask) # Compare subset of written subset of snapshot against corresponding region in # full snapshot. This checks that both the metadata and dataset subsets are # written properly. sub_mask = sw.mask(outfile) sub_load_region = [[0.375 * b, 0.625 * b] for b in boxsize] sub_mask.constrain_spatial(sub_load_region) mask.constrain_spatial(sub_load_region) snapshot = sw.load(filename, mask) sub_snapshot = sw.load(outfile, sub_mask) compare_data_contents(snapshot, sub_snapshot) # Clean up os.remove(outfile) return
def profile_3d_particles( path_to_snap: str, path_to_catalogue: str, ) -> tuple: # Read in halo properties vr_catalogue_handle = vr.load(path_to_catalogue) M500 = vr_catalogue_handle.spherical_overdensities.mass_500_rhocrit[0].to( 'Msun') R500 = vr_catalogue_handle.spherical_overdensities.r_500_rhocrit[0].to( 'Mpc') XPotMin = vr_catalogue_handle.positions.xcminpot[0].to('Mpc') YPotMin = vr_catalogue_handle.positions.ycminpot[0].to('Mpc') ZPotMin = vr_catalogue_handle.positions.zcminpot[0].to('Mpc') # Apply spatial mask to particles. SWIFTsimIO needs comoving coordinates # to filter particle coordinates, while VR outputs are in physical units. # Convert the region bounds to comoving, but keep the CoP and Rcrit in # physical units for later use. mask = sw.mask(path_to_snap, spatial_only=True) region = [[(XPotMin - R500), (XPotMin + R500)], [(YPotMin - R500), (YPotMin + R500)], [(ZPotMin - R500), (ZPotMin + R500)]] mask.constrain_spatial(region) data = sw.load(path_to_snap, mask=mask) # Select hot gas within sphere tempGas = data.gas.temperatures deltaX = data.gas.coordinates[:, 0] - XPotMin deltaY = data.gas.coordinates[:, 1] - YPotMin deltaZ = data.gas.coordinates[:, 2] - ZPotMin radial_distance = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) / R500 index = np.where((radial_distance < 2) & (tempGas > 1e5))[0] del tempGas, deltaX, deltaY, deltaZ mass_weighted_temperatures = (data.gas.temperatures * unyt.boltzmann_constant).to('keV') number_densities = (data.gas.densities.to('g/cm**3') / (unyt.mp * mean_molecular_weight)).to('cm**-3') field_value = mass_weighted_temperatures / number_densities**(2 / 3) radial_distance = radial_distance[index] entropies = field_value[index] temperatures = mass_weighted_temperatures[index] rho_crit = unyt.unyt_quantity( data.metadata.cosmology.critical_density(data.metadata.z).value, 'g/cm**3').to('Msun/Mpc**3') densities = data.gas.densities[index] / rho_crit return radial_distance, densities, temperatures, entropies, M500, R500
def process_single_halo(path_to_snap: str, path_to_catalogue: str) -> Tuple[unyt.unyt_quantity]: # Read in halo properties with h5.File(f'{path_to_catalogue}', 'r') as h5file: M500c = unyt.unyt_quantity(h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass) R500c = unyt.unyt_quantity(h5file['/SO_R_500_rhocrit'][0], unyt.Mpc) Thot500c = unyt.unyt_quantity( h5file['/SO_T_gas_highT_1.000000_times_500.000000_rhocrit'][0], unyt.K) Zhot500c = unyt.unyt_quantity( h5file['/SO_Zmet_gas_highT_1.000000_times_500.000000_rhocrit'][0], unyt.solar_metallicity) XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) # Read in gas particles to compute the core-excised temperature mask = sw.mask(f'{path_to_snap}', spatial_only=False) region = [[XPotMin - R500c, XPotMin + R500c], [YPotMin - R500c, YPotMin + R500c], [ZPotMin - R500c, ZPotMin + R500c]] mask.constrain_spatial(region) mask.constrain_mask("gas", "temperatures", Tcut_halogas * mask.units.temperature, 1.e12 * mask.units.temperature) data = sw.load(f'{path_to_snap}', mask=mask) posGas = data.gas.coordinates massGas = data.gas.masses mass_weighted_temperatures = data.gas.temperatures * data.gas.masses iron_fraction = data.gas.element_mass_fractions.iron * data.gas.masses # Select hot gas within sphere and without core deltaX = posGas[:, 0] - XPotMin deltaY = posGas[:, 1] - YPotMin deltaZ = posGas[:, 2] - ZPotMin deltaR = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) index = np.where(deltaR < R500c)[0] iron_fraction_500c = np.sum(iron_fraction[index]) / np.sum(massGas[index]) index = np.where((deltaR > 0.15 * R500c) & (deltaR < R500c))[0] Thot500c_nocore = np.sum(mass_weighted_temperatures[index]) / np.sum( massGas[index]) # Convert temperatures to keV Thot500c = (Thot500c * unyt.boltzmann_constant).to('keV') Thot500c_nocore = (Thot500c_nocore * unyt.boltzmann_constant).to('keV') return M500c, Thot500c, Thot500c_nocore, Zhot500c, iron_fraction_500c
def test_region_mask_not_modified(filename): """ Tests if a mask region is modified during the course of its use. Checks if https://github.com/SWIFTSIM/swiftsimio/issues/22 is broken. """ this_mask = mask(filename, spatial_only=True) bs = this_mask.metadata.boxsize read = [[0 * b, 0.5 * b] for b in bs] read_constant = [[0 * b, 0.5 * b] for b in bs] this_mask._generate_cell_mask(read) assert read == read_constant
def process_single_halo(path_to_snap: str, path_to_catalogue: str) -> Tuple[unyt.unyt_quantity]: # Read in halo properties with h5.File(f'{path_to_catalogue}', 'r') as h5file: M500c = unyt.unyt_quantity(h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass) R500c = unyt.unyt_quantity(h5file['/SO_R_500_rhocrit'][0], unyt.Mpc) Thot500c = unyt.unyt_quantity( h5file['/SO_T_gas_highT_1.000000_times_500.000000_rhocrit'][0], unyt.K) XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) # Read in gas particles to compute the core-excised temperature mask = sw.mask(f'{path_to_snap}', spatial_only=False) region = [[XPotMin - 5 * R500c, XPotMin + 5 * R500c], [YPotMin - 5 * R500c, YPotMin + 5 * R500c], [ZPotMin - 5 * R500c, ZPotMin + 5 * R500c]] mask.constrain_spatial(region) mask.constrain_mask("gas", "temperatures", Tcut_halogas * mask.units.temperature, 1.e12 * mask.units.temperature) data = sw.load(f'{path_to_snap}', mask=mask) posGas = data.gas.coordinates massGas = data.gas.masses mass_weighted_temperatures = data.gas.temperatures * data.gas.masses # Select hot gas within sphere and without core deltaX = posGas[:, 0] - XPotMin deltaY = posGas[:, 1] - YPotMin deltaZ = posGas[:, 2] - ZPotMin deltaR = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) index = np.where(deltaR < 5 * R500c)[0] compton_y = tsz_const * np.sum( mass_weighted_temperatures[index]) # / (np.pi * 25 * r500c ** 2) compton_y = compton_y.to('Mpc**2') index = np.where((deltaR > 0.15 * R500c) & (deltaR < R500c))[0] Thot500c_nocore = np.sum(mass_weighted_temperatures[index]) / np.sum( massGas[index]) return M500c, Thot500c, Thot500c_nocore, compton_y
def test_dithered_cell_metadata_is_valid(filename): """ Test that the metadata does what we think it does, in the dithered case. I.e. that it sets the particles contained in a top-level cell. """ mask_region = mask(filename) # Because we sort by offset if we are using the metadata we # must re-order the data to be in the correct order spatial_constraint = mask_region.constrain_spatial( [[0 * b, b] for b in mask_region.metadata.boxsize]) data = load(filename, mask=mask_region) cell_size = mask_region.cell_size.to(data.dark_matter.coordinates.units) boxsize = mask_region.metadata.boxsize[0].to( data.dark_matter.coordinates.units) offsets = mask_region.offsets["dark_matter"] counts = mask_region.counts["dark_matter"] start_offset = offsets stop_offset = offsets + counts for center, start, stop in zip( mask_region.centers.to(data.dark_matter.coordinates.units), start_offset, stop_offset, ): for dimension in range(0, 3): lower = (center - 0.5 * cell_size)[dimension] upper = (center + 0.5 * cell_size)[dimension] max = data.dark_matter.coordinates[start:stop, dimension].max() min = data.dark_matter.coordinates[start:stop, dimension].min() # Ignore things close to the boxsize if min < 0.05 * boxsize or max > 0.95 * boxsize: continue # Give it a little wiggle room assert max <= upper * 1.05 assert min > lower * 0.95
def test_reading_select_region_metadata_not_spatial_only(filename): """ The same as test_reading_select_region_metadata but for spatial_only=False. """ full_data = load(filename) # Mask off the centre of the volume. mask_region = mask(filename, spatial_only=False) restrict = array( [full_data.metadata.boxsize * 0.26, full_data.metadata.boxsize * 0.74]).T mask_region.constrain_spatial(restrict=restrict) selected_data = load(filename, mask=mask_region) selected_coordinates = selected_data.gas.coordinates # Now need to repeat the selection by hand: subset_mask = logical_and.reduce([ logical_and(x > y_lower, x < y_upper) for x, (y_lower, y_upper) in zip(full_data.gas.coordinates.T, restrict) ]) # We also need to repeat for the thing we just selected; the cells only give # us an _approximate_ selection! selected_subset_mask = logical_and.reduce([ logical_and(x > y_lower, x < y_upper) for x, (y_lower, y_upper) in zip(selected_data.gas.coordinates.T, restrict) ]) hand_selected_coordinates = full_data.gas.coordinates[subset_mask] assert (hand_selected_coordinates == selected_coordinates[selected_subset_mask]).all() return
def load_snapshot(snapshot_time, ax_lim): """ Select and load the particles to plot. """ # Snapshot to load snapshot = "earth_impact_%06d.hdf5" % snapshot_time # Only load data with the axis limits and below z=0 ax_lim = 0.1 mask = sw.mask(snapshot) box_mid = 0.5 * mask.metadata.boxsize[0].to(unyt.Rearth) x_min = box_mid - ax_lim * unyt.Rearth x_max = box_mid + ax_lim * unyt.Rearth load_region = [[x_min, x_max], [x_min, x_max], [x_min, box_mid]] mask.constrain_spatial(load_region) # Load data = sw.load(snapshot, mask=mask) pos = data.gas.coordinates.to(unyt.Rearth) - box_mid id = data.gas.particle_ids mat_id = data.gas.material_ids.value # Restrict to z < 0 sel = np.where(pos[:, 2] < 0)[0] pos = pos[sel] id = id[sel] mat_id = mat_id[sel] # Sort in z order so higher particles are plotted on top sort = np.argsort(pos[:, 2]) pos = pos[sort] id = id[sort] mat_id = mat_id[sort] # Edit material IDs for particles in the impactor num_in_target = 99740 mat_id[num_in_target <= id] += id_body return pos, mat_id
def dm_map_parent( run_name: str, velociraptor_properties_parent: str, snap_filepath_parent: str, velociraptor_properties_zoom: str, out_to_radius: Tuple[int, str] = (5, 'r200c'), highres_radius: Tuple[int, str] = (6, 'r500c'), output_directory: str = '.' ) -> None: print(f"Rendering {snap_filepath_parent}...") # Rendezvous over parent VR catalogue using zoom information with h5py.File(velociraptor_properties_zoom, 'r') as vr_file: idx, M200c, R200c, Xcminpot, Ycminpot, Zcminpot = find_object( vr_properties_catalog=velociraptor_properties_parent, sample_structType=10, sample_mass_lower_lim=vr_file['/Mass_200crit'][0] * 1e10 * 0.8, sample_x=vr_file['/Xcminpot'][0], sample_y=vr_file['/Ycminpot'][0], sample_z=vr_file['/Zcminpot'][0], ) with h5py.File(velociraptor_properties_parent, 'r') as vr_file: R500c = unyt.unyt_quantity(vr_file['/SO_R_500_rhocrit'][idx], unyt.Mpc) M200c = unyt.unyt_quantity(M200c, unyt.Solar_Mass) R200c = unyt.unyt_quantity(R200c, unyt.Mpc) xCen = unyt.unyt_quantity(Xcminpot, unyt.Mpc) yCen = unyt.unyt_quantity(Ycminpot, unyt.Mpc) zCen = unyt.unyt_quantity(Zcminpot, unyt.Mpc) if out_to_radius[1] == 'r200c': size = out_to_radius[0] * R200c elif out_to_radius[1] == 'r500c': size = out_to_radius[0] * R500c elif out_to_radius[1] == 'Mpc' or out_to_radius[1] is None: size = unyt.unyt_quantity(out_to_radius[0], unyt.Mpc) if highres_radius[1] == 'r200c': _highres_radius = highres_radius[0] * R200c elif highres_radius[1] == 'r500c': _highres_radius = highres_radius[0] * R500c elif highres_radius[1] == 'Mpc' or highres_radius[1] is None: _highres_radius = unyt.unyt_quantity(highres_radius[0], unyt.Mpc) # Construct spatial mask to feed into swiftsimio mask = sw.mask(snap_filepath_parent) region = [ [xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size] ] mask.constrain_spatial(region) data = sw.load(snap_filepath_parent, mask=mask) # Generate smoothing lengths for the dark matter data.dark_matter.smoothing_lengths = generate_smoothing_lengths( data.dark_matter.coordinates, data.metadata.boxsize, kernel_gamma=1.8, neighbours=57, speedup_fac=2, dimension=3, ) # data.dark_matter.coordinates[:, 0] = wrap( # data.dark_matter.coordinates[:, 0] - xCen, # data.metadata.boxsize[0] # ) # data.dark_matter.coordinates[:, 1] = wrap( # data.dark_matter.coordinates[:, 1] - yCen, # data.metadata.boxsize[1] # ) # data.dark_matter.coordinates[:, 2] = wrap( # data.dark_matter.coordinates[:, 2] - zCen, # data.metadata.boxsize[2] # ) dm_mass = dm_render(data, region=( [ xCen - size, xCen + size, yCen - size, yCen + size ] ), resolution=resolution) # Make figure fig, ax = plt.subplots(figsize=(8, 8), dpi=resolution // 8) fig.subplots_adjust(0, 0, 1, 1) ax.axis("off") ax.imshow(dm_mass.T, norm=LogNorm(), cmap="inferno", origin="lower", extent=(region[0] + region[1])) info = ax.text( 0.025, 0.025, ( f"Halo {run_name:s} DMO - parent\n" f"$z={data.metadata.z:3.3f}$\n" f"$M_{{200c}}={latex_float(M200c.value)}\\ {M200c.units.latex_repr}$\n" f"$R_{{200c}}={latex_float(R200c.value)}\\ {R200c.units.latex_repr}$\n" f"$R_\\mathrm{{clean}}={highres_radius[0]}\\ {highres_radius[1]}$" ), color="white", ha="left", va="bottom", alpha=0.8, transform=ax.transAxes, ) info.set_bbox(dict(facecolor='black', alpha=0.2, edgecolor='grey')) ax.text( xCen, yCen + 1.05 * R200c, r"$R_{200c}$", color="black", ha="center", va="bottom" ) ax.text( xCen, yCen + 1.02 * _highres_radius, r"$R_\mathrm{clean}$", color="white", ha="center", va="bottom" ) circle_r200 = plt.Circle((xCen, yCen), R200c, color="black", fill=False, linestyle='-') circle_clean = plt.Circle((xCen, yCen), _highres_radius.value, color="white", fill=False, linestyle=':') ax.add_artist(circle_r200) ax.add_artist(circle_clean) ax.set_xlim([xCen.value - size.value, xCen.value + size.value]) ax.set_ylim([yCen.value - size.value, yCen.value + size.value]) fig.savefig(f"{output_directory}/{run_name}_dark_matter_map_parent.png") plt.close(fig) print(f"Saved: {output_directory}/{run_name}_dark_matter_map_parent.png") del data, dm_mass plt.close('all') return
def contamination_map(run_name: str, velociraptor_properties_zoom: str, snap_filepath_zoom: str, out_to_radius: Tuple[int, str] = (5, 'r200c'), highres_radius: Tuple[int, str] = (6, 'r500c'), output_directory: str = '.') -> None: # Rendezvous over parent VR catalogue using zoom information with h5py.File(velociraptor_properties_zoom, 'r') as vr_file: M200c = unyt.unyt_quantity(vr_file['/Mass_200crit'][0] * 1e10, unyt.Solar_Mass) R200c = unyt.unyt_quantity(vr_file['/R_200crit'][0], unyt.Mpc) R500c = unyt.unyt_quantity(vr_file['/SO_R_500_rhocrit'][0], unyt.Mpc) xCen = unyt.unyt_quantity(vr_file['/Xcminpot'][0], unyt.Mpc) yCen = unyt.unyt_quantity(vr_file['/Ycminpot'][0], unyt.Mpc) zCen = unyt.unyt_quantity(vr_file['/Zcminpot'][0], unyt.Mpc) # EAGLE-XL data path print(f"Rendering {snap_filepath_zoom}...") if out_to_radius[1] == 'r200c': size = out_to_radius[0] * R200c elif out_to_radius[1] == 'r500c': size = out_to_radius[0] * R500c elif out_to_radius[1] == 'Mpc' or out_to_radius[1] is None: size = unyt.unyt_quantity(out_to_radius[0], unyt.Mpc) else: raise ValueError( "The `out_to_radius` input is not in the correct format or not recognised." ) if highres_radius[1] == 'r200c': _highres_radius = highres_radius[0] * R200c elif highres_radius[1] == 'r500c': _highres_radius = highres_radius[0] * R500c elif highres_radius[1] == 'Mpc' or highres_radius[1] is None: _highres_radius = unyt.unyt_quantity(highres_radius[0], unyt.Mpc) else: raise ValueError( "The `highres_radius` input is not in the correct format or not recognised." ) mask = sw.mask(snap_filepath_zoom) region = [[xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size]] mask.constrain_spatial(region) # Load data using mask data = sw.load(snap_filepath_zoom, mask=mask) posDM = data.dark_matter.coordinates / data.metadata.a highres_coordinates = { 'x': wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0]), 'y': wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1]), 'z': wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2]), 'r': np.sqrt( wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0])**2 + wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1])**2 + wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2])**2) } del posDM posDM = data.boundary.coordinates / data.metadata.a lowres_coordinates = { 'x': wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0]), 'y': wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1]), 'z': wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2]), 'r': np.sqrt( wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0])**2 + wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1])**2 + wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2])**2) } del posDM # Flag contamination particles within 5 R200 contaminated_idx = np.where(lowres_coordinates['r'] < _highres_radius)[0] contaminated_r200_idx = np.where(lowres_coordinates['r'] < 1. * R200c)[0] print( f"Total low-res DM: {len(lowres_coordinates['r'])} particles detected") print( f"Contaminating low-res DM (< R_clean): {len(contaminated_idx)} particles detected" ) print( f"Contaminating low-res DM (< r200c): {len(contaminated_r200_idx)} particles detected" ) # Make particle maps fig, ax = plt.subplots(figsize=(7, 7), dpi=1024 // 7) ax.set_aspect('equal') ax.set_ylabel(r"$y$ [Mpc]") ax.set_xlabel(r"$x$ [Mpc]") ax.plot(highres_coordinates['x'][::4], highres_coordinates['y'][::4], ',', c="C0", alpha=0.1, label='Highres') ax.plot(lowres_coordinates['x'][contaminated_idx], lowres_coordinates['y'][contaminated_idx], 'x', c="red", alpha=1, label='Lowres contaminating') ax.plot(lowres_coordinates['x'][~contaminated_idx], lowres_coordinates['y'][~contaminated_idx], '.', c="green", alpha=0.2, label='Lowres clean') ax.text( 0.025, 0.025, (f"Halo {run_name:s} DMO\n" f"$z={data.metadata.z:3.3f}$\n" f"$M_{{200c}}={latex_float(M200c.value)}$ M$_\odot$\n" f"$R_{{200c}}={latex_float(R200c.value)}$ Mpc\n" f"$R_{{\\rm clean}}={latex_float(_highres_radius.value)}$ Mpc"), color="black", ha="left", va="bottom", transform=ax.transAxes, ) ax.text(0, 0 + 1.05 * R200c, r"$R_{200c}$", color="black", ha="center", va="bottom") ax.text(0, 0 + 1.002 * 5 * R200c, r"$5 \times R_{200c}$", color="grey", ha="center", va="bottom") ax.text(0, 0 + 1.02 * _highres_radius, r"$R_\mathrm{clean}$", color="red", ha="center", va="bottom") circle_r200 = plt.Circle((0, 0), R200c, color="black", fill=False, linestyle='-') circle_5r200 = plt.Circle((0, 0), 5 * R200c, color="grey", fill=False, linestyle='--') circle_clean = plt.Circle((0, 0), _highres_radius.value, color="red", fill=False, linestyle=':') ax.add_artist(circle_r200) ax.add_artist(circle_5r200) ax.add_artist(circle_clean) ax.set_xlim([-size.value, size.value]) ax.set_ylim([-size.value, size.value]) plt.legend() fig.savefig( f"{output_directory}/{run_name}_contamination_map{out_to_radius[0]}{out_to_radius[1]}.png" ) print( f"Saved: {output_directory}/{run_name}_contamination_map{out_to_radius[0]}{out_to_radius[1]}.png" ) plt.close(fig)
def contamination_radial_histogram(run_name: str, velociraptor_properties_zoom: str, snap_filepath_zoom: str, out_to_radius: Tuple[int, str] = (5, 'r200c'), highres_radius: Tuple[int, str] = (6, 'r500c'), output_directory: str = '.') -> None: # Rendezvous over parent VR catalogue using zoom information with h5py.File(velociraptor_properties_zoom, 'r') as vr_file: M200c = unyt.unyt_quantity(vr_file['/Mass_200crit'][0] * 1e10, unyt.Solar_Mass) R200c = unyt.unyt_quantity(vr_file['/R_200crit'][0], unyt.Mpc) R500c = unyt.unyt_quantity(vr_file['/SO_R_500_rhocrit'][0], unyt.Mpc) xCen = unyt.unyt_quantity(vr_file['/Xcminpot'][0], unyt.Mpc) yCen = unyt.unyt_quantity(vr_file['/Ycminpot'][0], unyt.Mpc) zCen = unyt.unyt_quantity(vr_file['/Zcminpot'][0], unyt.Mpc) # EAGLE-XL data path print(f"Rendering {snap_filepath_zoom}...") if out_to_radius[1] == 'r200c': size = out_to_radius[0] * R200c elif out_to_radius[1] == 'r500c': size = out_to_radius[0] * R500c elif out_to_radius[1] == 'Mpc' or out_to_radius[1] is None: size = unyt.unyt_quantity(out_to_radius[0], unyt.Mpc) if highres_radius[1] == 'r200c': _highres_radius = highres_radius[0] * R200c elif highres_radius[1] == 'r500c': _highres_radius = highres_radius[0] * R500c elif highres_radius[1] == 'Mpc' or highres_radius[1] is None: _highres_radius = unyt.unyt_quantity(highres_radius[0], unyt.Mpc) mask = sw.mask(snap_filepath_zoom) region = [[xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size]] mask.constrain_spatial(region) # Load data using mask data = sw.load(snap_filepath_zoom, mask=mask) posDM = data.dark_matter.coordinates / data.metadata.a highres_coordinates = { 'x': wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0]), 'y': wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1]), 'z': wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2]), 'r': np.sqrt( wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0])**2 + wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1])**2 + wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2])**2) } del posDM posDM = data.boundary.coordinates / data.metadata.a lowres_coordinates = { 'x': wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0]), 'y': wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1]), 'z': wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2]), 'r': np.sqrt( wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0])**2 + wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1])**2 + wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2])**2) } del posDM # Histograms contaminated_idx = np.where(lowres_coordinates['r'] < _highres_radius)[0] bins = np.linspace(0, size, 40) hist, bin_edges = np.histogram(lowres_coordinates['r'][contaminated_idx], bins=bins) lowres_coordinates['r_bins'] = (bin_edges[1:] + bin_edges[:-1]) / 2 / R200c lowres_coordinates['hist_contaminating'] = hist del hist, bin_edges hist, _ = np.histogram(lowres_coordinates['r'], bins=bins) lowres_coordinates['hist_all'] = hist del hist hist, _ = np.histogram(highres_coordinates['r'], bins=bins) highres_coordinates['r_bins'] = lowres_coordinates['r_bins'] highres_coordinates['hist_all'] = hist del bins, hist # Make radial distribution plot fig, ax = plt.subplots() ax.set_yscale('log') ax.set_ylabel("Number of particles") ax.set_xlabel(r"$R\ /\ R_{200c}$") ax.step(highres_coordinates['r_bins'], highres_coordinates['hist_all'], where='mid', color='grey', label='Highres all') ax.step(lowres_coordinates['r_bins'], lowres_coordinates['hist_all'], where='mid', color='green', label='Lowres all') ax.step(lowres_coordinates['r_bins'], lowres_coordinates['hist_contaminating'], where='mid', color='red', label='Lowres contaminating') ax.text( 0.025, 0.025, (f"Halo {run_name:s} DMO\n" f"$z={data.metadata.z:3.3f}$\n" f"$M_{{200c}}={latex_float(M200c.value)}$ M$_\odot$\n" f"$R_{{200c}}={latex_float(R200c.value)}$ Mpc\n" f"$R_{{\\rm clean}}={latex_float(_highres_radius.value)}$ Mpc"), color="black", ha="left", va="bottom", transform=ax.transAxes, ) ax.axvline(1, color="grey", linestyle='--') ax.axvline(_highres_radius / R200c, color="red", linestyle='--') ax.set_xlim([0, size.value]) plt.legend() fig.tight_layout() fig.savefig( f"{output_directory}/{run_name}_contamination_hist{out_to_radius[0]}{out_to_radius[1]}.png" ) print( f"Saved: {output_directory}/{run_name}_contamination_hist{out_to_radius[0]}{out_to_radius[1]}.png" ) plt.close(fig)
def cumulative_mass_compare_plot( run_name: str, snap_filepath_parent: str = None, snap_filepath_zoom: List[str] = None, velociraptor_properties_zoom: List[str] = None, output_directory: str = None) -> None: """ This function compares the cumulative mass of DMO zooms to their corresponding parent halo. It also allows to assess numerical convergence by overlapping multiple profiles from zooms with different resolutions. The cumulative mass profiles are then listed in the legend, where the DM particle mass is quoted. The zoom inputs are in the form of arrays of strings, to allow for multiple entries. Each entry is a zoom snap/VR output resolution. The function allows not to plot either the parent mass profile or the zooms. At least one of them must be entered. :param run_name: str A custom and identifiable name for the run. Currently the standard name follows the scheme {AUTHOR_INITIALS}{HALO_ID}, e.g. SK0 or EA1. This argument must be defined. :param snap_filepath_parent: str The complete path to the snapshot of the parent box. If parameter is None, the mass profile of the parent box is not displayed. :param snap_filepath_zoom: list(str) The list of complete paths to the snapshots of the zooms at different resolution. Note: the order must match that of the `velociraptor_properties_zoom` parameter. If parameter is None, the mass profile of the zoom is not displayed and the `velociraptor_properties_zoom` parameter is ignored. :param velociraptor_properties_zoom: list(str) The list of complete paths to the VR outputs (properties) of the zooms at different resolution. Note: the order must match that of the `snap_filepath_zoom` parameter. If `snap_filepath_zoom` is None, then this parameter is ignored. If this parameter is None and `snap_filepath_zoom` is defined, raises an error. :param output_directory: str The output directory where to save the plot. This code assumes that the output directory exists. If it does not exist, matplotlib will return an error. This argument must be defined. :return: None """ # ARGS CHECK # assert run_name assert snap_filepath_parent or snap_filepath_zoom if snap_filepath_zoom and velociraptor_properties_zoom: assert len(snap_filepath_zoom) == len(velociraptor_properties_zoom) elif not snap_filepath_zoom: velociraptor_properties_zoom = None elif snap_filepath_zoom and not velociraptor_properties_zoom: raise ValueError assert output_directory # TEMPORARY # # Split the run_name into author and halo_id to make everything work fine for now import re match = re.match(r"([a-z]+)([0-9]+)", run_name, re.I) author = None halo_id = None if match: author, halo_id = match.groups() halo_id = int(halo_id) fig, (ax, ax_residual) = plt.subplots(nrows=2, ncols=1, figsize=(3.5, 4.1), sharex=True, gridspec_kw={'height_ratios': [3, 1]}) # PARENT # if snap_filepath_parent: # Load VR output gathered from the halo selection process lines = np.loadtxt(f"{output_directory}/halo_selected_{author}.txt", comments="#", delimiter=",", unpack=False).T M200c = lines[1] * 1e13 R200c = lines[2] Xcminpot = lines[3] Ycminpot = lines[4] Zcminpot = lines[5] M200c = unyt.unyt_quantity(M200c[halo_id], unyt.Solar_Mass) R200c = unyt.unyt_quantity(R200c[halo_id], unyt.Mpc) xCen = unyt.unyt_quantity(Xcminpot[halo_id], unyt.Mpc) yCen = unyt.unyt_quantity(Ycminpot[halo_id], unyt.Mpc) zCen = unyt.unyt_quantity(Zcminpot[halo_id], unyt.Mpc) # Construct spatial mask to feed into swiftsimio size = radius_bounds[1] * R200c mask = sw.mask(snap_filepath_parent) region = [[xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size]] mask.constrain_spatial(region) data = sw.load(snap_filepath_parent, mask=mask) # Get DM particle coordinates and compute radial distance from CoP in R200 units posDM = data.dark_matter.coordinates / data.metadata.a r = np.sqrt((posDM[:, 0] - xCen)**2 + (posDM[:, 1] - yCen)**2 + (posDM[:, 2] - zCen)**2) # Calculate particle mass and rho_crit unitLength = data.metadata.units.length unitMass = data.metadata.units.mass rho_crit = unyt.unyt_quantity( data.metadata.cosmology['Critical density [internal units]'], unitMass / unitLength**3)[0].to('Msun/Mpc**3') rhoMean = rho_crit * data.metadata.cosmology['Omega_m'] vol = data.metadata.boxsize[0]**3 numPart = data.metadata.n_dark_matter particleMass = rhoMean * vol / numPart parent_mass_resolution = particleMass particleMasses = np.ones_like(r.value) * particleMass # Construct bins and compute density profile lbins = np.logspace(np.log10(radius_bounds[0]), np.log10(radius_bounds[1]), bins) r_scaled = r / R200c hist, bin_edges = np.histogram(r_scaled, bins=lbins, weights=particleMasses) bin_centre = np.sqrt(bin_edges[1:] * bin_edges[:-1]) cumulative_mass_parent = np.cumsum(hist) # Plot density profile for each selected halo in volume cumulative_mass_parent[cumulative_mass_parent == 0] = np.nan parent_label = f'Parent: $m_\\mathrm{{DM}} = {latex_float(parent_mass_resolution.value[0])}\\ {parent_mass_resolution.units.latex_repr}$' ax.plot(bin_centre, cumulative_mass_parent, c="grey", linestyle="-", label=parent_label) # Compute convergence radius conv_radius = convergence_radius(r, particleMasses, rho_crit) / R200c ax.axvline(conv_radius, color='grey', linestyle='--') ax_residual.axvline(conv_radius, color='grey', linestyle='--') t = ax.text(conv_radius, ax.get_ylim()[1], 'Convergence radius', ha='center', va='top', rotation='vertical', alpha=0.6) t.set_bbox(dict(facecolor='white', alpha=0.6, edgecolor='none')) # ZOOMS # if snap_filepath_zoom: # Set-up colors cmap_discrete = plt.cm.get_cmap(cmap_name, len(velociraptor_properties_zoom) + 3) cmaplist = [cmap_discrete(i) for i in range(cmap_discrete.N)] for snap_path, vrprop_path, color in zip(snap_filepath_zoom, velociraptor_properties_zoom, cmaplist): # Load velociraptor data with h5py.File(vrprop_path, 'r') as vr_file: M200c = vr_file['/Mass_200crit'][0] * 1e10 R200c = vr_file['/R_200crit'][0] Xcminpot = vr_file['/Xcminpot'][0] Ycminpot = vr_file['/Ycminpot'][0] Zcminpot = vr_file['/Zcminpot'][0] M200c = unyt.unyt_quantity(M200c, unyt.Solar_Mass) R200c = unyt.unyt_quantity(R200c, unyt.Mpc) xCen = unyt.unyt_quantity(Xcminpot, unyt.Mpc) yCen = unyt.unyt_quantity(Ycminpot, unyt.Mpc) zCen = unyt.unyt_quantity(Zcminpot, unyt.Mpc) # Construct spatial mask to feed into swiftsimio size = radius_bounds[1] * R200c mask = sw.mask(snap_path) region = [[xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size]] mask.constrain_spatial(region) data = sw.load(snap_path, mask=mask) # Get DM particle coordinates and compute radial distance from CoP in R200 units posDM = data.dark_matter.coordinates / data.metadata.a r = np.sqrt((posDM[:, 0] - xCen)**2 + (posDM[:, 1] - yCen)**2 + (posDM[:, 2] - zCen)**2) # Calculate particle mass and rho_crit unitLength = data.metadata.units.length unitMass = data.metadata.units.mass rho_crit = unyt.unyt_quantity( data.metadata.cosmology['Critical density [internal units]'], unitMass / unitLength**3)[0].to('Msun/Mpc**3') particleMasses = data.dark_matter.masses.to('Msun') zoom_mass_resolution = particleMasses # Construct bins and compute density profile lbins = np.logspace(np.log10(radius_bounds[0]), np.log10(radius_bounds[1]), bins) r_scaled = r / R200c hist, bin_edges = np.histogram(r_scaled, bins=lbins, weights=particleMasses) bin_centre = np.sqrt(bin_edges[1:] * bin_edges[:-1]) cumulative_mass_zoom = np.cumsum(hist) # Plot density profile for each selected halo in volume cumulative_mass_zoom[cumulative_mass_zoom == 0] = np.nan zoom_label = f'Zoom: $m_\\mathrm{{DM}} = {latex_float(zoom_mass_resolution.value[0])}\\ {zoom_mass_resolution.units.latex_repr}$' ax.plot(bin_centre, cumulative_mass_zoom, c=color, linestyle="-", label=zoom_label) # Compute convergence radius conv_radius = convergence_radius(r, particleMasses, rho_crit) / R200c ax.axvline(conv_radius, color=color, linestyle='--') t = ax.text(conv_radius, ax.get_ylim()[1], 'Convergence radius', ha='center', va='top', rotation='vertical', alpha=0.6) t.set_bbox(dict(facecolor='white', alpha=0.6, edgecolor='none')) # RESIDUALS # if snap_filepath_parent and snap_filepath_zoom: residual = (cumulative_mass_zoom - cumulative_mass_parent) / cumulative_mass_parent ax_residual.axhline(0, color='grey', linestyle='-') ax_residual.plot(bin_centre, residual, c=color, linestyle="-") ax_residual.axvline(conv_radius, color=color, linestyle='--') ax.text( 0.975, 0.025, (f"Halo {run_name:s} DMO\n" f"$z={data.metadata.z:3.3f}$\n" "Zoom VR output:\n" f"$M_{{200c}}={latex_float(M200c.value)}\\ {M200c.units.latex_repr}$\n" f"$R_{{200c}}={latex_float(R200c.value)}\\ {R200c.units.latex_repr}$" ), color="black", ha="right", va="bottom", backgroundcolor='white', transform=ax.transAxes, ) ax.axvline(1, color="grey", linestyle='--') ax_residual.axvline(1, color="grey", linestyle='--') ax_residual.set_xlim(radius_bounds[0], radius_bounds[1]) ax_residual.set_ylim(residual_bounds[0], residual_bounds[1]) ax.set_xscale('log') ax.set_yscale('log') ax.set_ylabel(f"$M_{{\\rm DM}} (< R)\\ [{M200c.units.latex_repr}]$") ax_residual.set_ylabel(f"$\\Delta M\\ /\\ M_{{\\rm parent}}\\ (< R)$") ax_residual.set_xlabel(r"$R\ /\ R_{200c}$") ax.legend(loc="upper right") fig.tight_layout() fig.savefig(f"{output_directory}/{run_name}_cumulative_mass_compare.png") plt.close(fig) return
def profile_3d_shells( path_to_snap: str, path_to_catalogue: str, ) -> tuple: # Read in halo properties vr_catalogue_handle = vr.load(path_to_catalogue) M500 = vr_catalogue_handle.spherical_overdensities.mass_500_rhocrit[0].to( 'Msun') R500 = vr_catalogue_handle.spherical_overdensities.r_500_rhocrit[0].to( 'Mpc') XPotMin = vr_catalogue_handle.positions.xcminpot[0].to('Mpc') YPotMin = vr_catalogue_handle.positions.ycminpot[0].to('Mpc') ZPotMin = vr_catalogue_handle.positions.zcminpot[0].to('Mpc') # Apply spatial mask to particles. SWIFTsimIO needs comoving coordinates # to filter particle coordinates, while VR outputs are in physical units. # Convert the region bounds to comoving, but keep the CoP and Rcrit in # physical units for later use. mask = sw.mask(path_to_snap, spatial_only=True) region = [[(XPotMin - R500), (XPotMin + R500)], [(YPotMin - R500), (YPotMin + R500)], [(ZPotMin - R500), (ZPotMin + R500)]] mask.constrain_spatial(region) data = sw.load(path_to_snap, mask=mask) # Select gas within sphere and main FOF halo fof_id = data.gas.fofgroup_ids tempGas = data.gas.temperatures deltaX = data.gas.coordinates[:, 0] - XPotMin deltaY = data.gas.coordinates[:, 1] - YPotMin deltaZ = data.gas.coordinates[:, 2] - ZPotMin radial_distance = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) / R500 index = np.where((radial_distance < 2) & (fof_id == 1) & (tempGas > 1e5))[0] del deltaX, deltaY, deltaZ, fof_id, tempGas radial_distance = radial_distance[index] data.gas.masses = data.gas.masses[index] data.gas.temperatures = data.gas.temperatures[index] # Define radial bins and shell volumes lbins = np.logspace(-3, 2, 40) * radial_distance.units radial_bin_centres = 10.0**( 0.5 * np.log10(lbins[1:] * lbins[:-1])) * radial_distance.units volume_shell = (4. * np.pi / 3.) * (R500**3) * ((lbins[1:])**3 - (lbins[:-1])**3) mass_weights, _ = histogram_unyt(radial_distance, bins=lbins, weights=data.gas.masses) mass_weights[mass_weights == 0] = np.nan # Replace zeros with Nans density_profile = mass_weights / volume_shell number_density_profile = (density_profile.to('g/cm**3') / (unyt.mp * mean_molecular_weight)).to('cm**-3') mass_weighted_temperatures = ( data.gas.temperatures * unyt.boltzmann_constant).to('keV') * data.gas.masses temperature_weights, _ = histogram_unyt(radial_distance, bins=lbins, weights=mass_weighted_temperatures) temperature_weights[temperature_weights == 0] = np.nan # Replace zeros with Nans temperature_profile = temperature_weights / mass_weights # kBT in units of [keV] entropy_profile = temperature_profile / number_density_profile**(2 / 3) rho_crit = unyt.unyt_quantity( data.metadata.cosmology.critical_density(data.metadata.z).value, 'g/cm**3').to('Msun/Mpc**3') density_profile /= rho_crit return radial_bin_centres, density_profile, temperature_profile, entropy_profile, M500, R500
def to_swiftsimio_dataset( particles: VelociraptorParticles, snapshot_filename, generate_extra_mask: bool = False, ) -> Union[ swiftsimio.reader.SWIFTDataset, Tuple[swiftsimio.reader.SWIFTDataset, namedtuple] ]: """ Loads a VelociraptorParticles instance for one halo into a `swiftsimio` masked dataset. Initially, this uses `r_max` to perform a spatial mask, and then returns the `swiftsimio` dataset and a secondary mask that may be used to extract only the particles that are part of the FoF group. You will need to instantiate the VelociraptorParticles instance with an associated catalogue to use this feature, as it requires the knowledge of `r_max`. Takes three arguments: + particles, the VelociraptorParticles instance, + snapshot_filename, the path to the associated SWIFT snapshot. + generate_extra_mask, whether or not to generate the secondary mask object that allows for the extraction of particles that are present only in the FoF group. It returns: + data, the swiftsimio dataset + mask, an object containing for all available datasets in the swift dataset. The initial masking is performed on a spatial only basis, and this is required to only extract the particles in the FoF group as identified by velociraptor. This is only provided if generate_extra_mask has a truthy value. """ # First use the swiftsimio spatial masking to constrain our dataset # to only contain particles within the cube that contains the halo # (this is only approximate down to the swift cell size) swift_mask = swiftsimio.mask(snapshot_filename, spatial_only=True) # SWIFT data is stored in comoving units, so we need to un-correct # the velociraptor data if it is stored in physical. try: if not particles.groups_instance.catalogue.units.comoving: length_factor = particles.groups_instance.catalogue.units.a else: length_factor = 1.0 except AttributeError: raise RuntimeError( "Please use a particles instance with an associated halo catalogue." ) spatial_mask = [ [ particles.x / length_factor - particles.r_size / length_factor, particles.x / length_factor + particles.r_size / length_factor, ], [ particles.y / length_factor - particles.r_size / length_factor, particles.y / length_factor + particles.r_size / length_factor, ], [ particles.z / length_factor - particles.r_size / length_factor, particles.z / length_factor + particles.r_size / length_factor, ], ] swift_mask.constrain_spatial(spatial_mask) # TODO: Make spatial masking work # swift_mask = None data = swiftsimio.load(snapshot_filename, mask=swift_mask) if not generate_extra_mask: return data # Now we must generate the secondary mask, for all available # particle types. particle_name_masks = {} for particle_name in data.metadata.present_particle_names: # This will change if we ever take advantage of the # parttypes available through velociraptor. particle_name_masks[particle_name] = np.in1d( getattr(data, particle_name).particle_ids, particles.particle_ids ) # Finally we generate a named tuple with the correct fields and # fill it with the contents of our dictionary MaskTuple = namedtuple("MaskCollection", data.metadata.present_particle_names) mask = MaskTuple(**particle_name_masks) return data, mask
R200c.append(vr_file['/R_200crit'][0]) x.append(vr_file['/Xcminpot'][0]) y.append(vr_file['/Ycminpot'][0]) z.append(vr_file['/Zcminpot'][0]) for i in range(len(snap_relative_filepaths)): # EAGLE-XL data path snapFile = simdata_dirpath + snap_relative_filepaths[i] print(f"Rendering {snap_relative_filepaths[i]}...") # Load data using mask xCen = unyt.unyt_quantity(x[i], unyt.Mpc) yCen = unyt.unyt_quantity(y[i], unyt.Mpc) zCen = unyt.unyt_quantity(z[i], unyt.Mpc) size = unyt.unyt_quantity(out_to_radius * R200c[i], unyt.Mpc) mask = sw.mask(snapFile) region = [ [xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size] ] mask.constrain_spatial(region) # Load data using mask data = sw.load(snapFile, mask=mask) posDM = data.dark_matter.coordinates / data.metadata.a coord_x = posDM[:, 0] - xCen coord_y = posDM[:, 1] - yCen coord_z = posDM[:, 2] - zCen del posDM
def profile_3d_single_halo( path_to_snap: str, path_to_catalogue: str, hse_dataset: pd.Series = None, ) -> tuple: # Read in halo properties vr_catalogue_handle = vr.load(path_to_catalogue) a = vr_catalogue_handle.a M500 = vr_catalogue_handle.spherical_overdensities.mass_500_rhocrit[0].to( 'Msun') R500 = vr_catalogue_handle.spherical_overdensities.r_500_rhocrit[0].to( 'Mpc') XPotMin = vr_catalogue_handle.positions.xcminpot[0].to('Mpc') YPotMin = vr_catalogue_handle.positions.ycminpot[0].to('Mpc') ZPotMin = vr_catalogue_handle.positions.zcminpot[0].to('Mpc') # If no custom aperture, select r500c as default if hse_dataset is not None: assert R500.units == hse_dataset["R500hse"].units assert M500.units == hse_dataset["M500hse"].units R500 = hse_dataset["R500hse"] M500 = hse_dataset["M500hse"] # Apply spatial mask to particles. SWIFTsimIO needs comoving coordinates # to filter particle coordinates, while VR outputs are in physical units. # Convert the region bounds to comoving, but keep the CoP and Rcrit in # physical units for later use. mask = sw.mask(path_to_snap, spatial_only=True) region = [[(XPotMin - R500) * a, (XPotMin + R500) * a], [(YPotMin - R500) * a, (YPotMin + R500) * a], [(ZPotMin - R500) * a, (ZPotMin + R500) * a]] mask.constrain_spatial(region) data = sw.load(path_to_snap, mask=mask) # Convert datasets to physical quantities # r500c is already in physical units data.gas.coordinates.convert_to_physical() data.gas.masses.convert_to_physical() data.gas.temperatures.convert_to_physical() data.gas.densities.convert_to_physical() # Select hot gas within sphere tempGas = data.gas.temperatures deltaX = data.gas.coordinates[:, 0] - XPotMin deltaY = data.gas.coordinates[:, 1] - YPotMin deltaZ = data.gas.coordinates[:, 2] - ZPotMin radial_distance = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) / R500 index = np.where((radial_distance < 2) & (tempGas > 1e5))[0] del tempGas, deltaX, deltaY, deltaZ # Calculate particle mass and rho_crit rho_crit = unyt.unyt_quantity( data.metadata.cosmology.critical_density(data.metadata.z).value, 'g/cm**3') mass_weighted_temperatures = (data.gas.temperatures * unyt.boltzmann_constant).to('keV') number_densities = (data.gas.densities.to('g/cm**3') / (unyt.mp * mean_molecular_weight)).to('cm**-3') field_value = mass_weighted_temperatures / number_densities**(2 / 3) field_label = r'$K$ [keV cm$^2$]' radial_distance = radial_distance[index] field_value = field_value[index] field_masses = data.gas.temperatures[index] return radial_distance, field_value, field_masses, field_label, M500, R500
def process_single_halo(path_to_snap: str, path_to_catalogue: str): # Read in halo properties with h5py.File(f'{path_to_catalogue}', 'r') as h5file: M500c = unyt.unyt_quantity(h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass) R500c = unyt.unyt_quantity(h5file['/SO_R_500_rhocrit'][0], unyt.Mpc) XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) # Read in gas particles to compute the core-excised temperature mask = sw.mask(f'{path_to_snap}', spatial_only=False) region = [[XPotMin - 5 * R500c, XPotMin + 5 * R500c], [YPotMin - 5 * R500c, YPotMin + 5 * R500c], [ZPotMin - 5 * R500c, ZPotMin + 5 * R500c]] mask.constrain_spatial(region) mask.constrain_mask("gas", "temperatures", 1.e5 * mask.units.temperature, 1.e10 * mask.units.temperature) data = sw.load(f'{path_to_snap}', mask=mask) # Select hot gas within sphere and without core deltaX = data.gas.coordinates[:, 0] - XPotMin deltaY = data.gas.coordinates[:, 1] - YPotMin deltaZ = data.gas.coordinates[:, 2] - ZPotMin deltaR = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) # Keep only particles inside 5 R500crit index = np.where((deltaR > 0.15 * R500c) & (deltaR < 5 * R500c))[0] data.gas.radial_distances = deltaR[index] data.gas.densities = data.gas.densities[index] data.gas.masses = data.gas.masses[index] data.gas.temperatures = data.gas.temperatures[index] data.gas.element_mass_fractions.hydrogen = data.gas.element_mass_fractions.hydrogen[ index] data.gas.element_mass_fractions.helium = data.gas.element_mass_fractions.helium[ index] data.gas.element_mass_fractions.carbon = data.gas.element_mass_fractions.carbon[ index] data.gas.element_mass_fractions.nitrogen = data.gas.element_mass_fractions.nitrogen[ index] data.gas.element_mass_fractions.oxygen = data.gas.element_mass_fractions.oxygen[ index] data.gas.element_mass_fractions.neon = data.gas.element_mass_fractions.neon[ index] data.gas.element_mass_fractions.magnesium = data.gas.element_mass_fractions.magnesium[ index] data.gas.element_mass_fractions.silicon = data.gas.element_mass_fractions.silicon[ index] data.gas.element_mass_fractions.iron = data.gas.element_mass_fractions.iron[ index] spec_fit_data = calc_spectrum(data, R500c) fit_spectrum(spec_fit_data) # print('Rspec', spec_fit_data['Rspec']) # print('Tspec', spec_fit_data['Tspec']) # print('RHOspec', spec_fit_data['RHOspec']) # print('Zspec', spec_fit_data['Zspec']) # print('XIspec', spec_fit_data['XIspec']) hse_true = HydrostaticEstimator.from_data_paths( catalog_file=path_to_catalogue, snapshot_file=path_to_snap, profile_type='true') hse_spec = HydrostaticEstimator.from_data_paths( catalog_file=path_to_catalogue, snapshot_file=path_to_snap, profile_type='spec', spec_fit_data=spec_fit_data) print(f'r500c = {hse_spec.r500c:.3E}') print(f'M500c = {hse_spec.m500c:.3E}') print() print(f'R500,true,hse = {hse_true.R500hse:.3E}') print(f'M500,true,hse = {hse_true.M500hse:.3E}') print(f'P500,true,hse = {hse_true.P500hse:.3E}') print(f'kBT500,true,hse = {hse_true.kBT500hse:.3E}') print(f'K500,true,hse = {hse_true.K500hse:.3E}') print() print(f'R500,spec,hse = {hse_spec.R500hse:.3E}') print(f'M500,spec,hse = {hse_spec.M500hse:.3E}') print(f'P500,spec,hse = {hse_spec.P500hse:.3E}') print(f'kBT500,spec,hse = {hse_spec.kBT500hse:.3E}') print(f'K500,spec,hse = {hse_spec.K500hse:.3E}') print() print(f"[Bias] M500 (hse/true) = {hse_true.M500hse / hse_spec.m500c:.3f}") print(f"[Bias] M500 (spec/true) = {hse_spec.M500hse / hse_spec.m500c:.3f}") return hse_spec
def total_mass_profiles(self): # Read in halo properties from catalog vr_catalogue_handle = vr.load(self.zoom.catalogue_properties_path) a = vr_catalogue_handle.a self.r500c = vr_catalogue_handle.spherical_overdensities.r_500_rhocrit[ 0].to('Mpc') self.r2500c = vr_catalogue_handle.spherical_overdensities.r_2500_rhocrit[ 0].to('Mpc') XPotMin = vr_catalogue_handle.positions.xcminpot[0].to('Mpc') YPotMin = vr_catalogue_handle.positions.ycminpot[0].to('Mpc') ZPotMin = vr_catalogue_handle.positions.zcminpot[0].to('Mpc') # Read in gas particles and parse densities and temperatures mask = sw.mask(self.zoom.snapshot_path, spatial_only=False) region = [[(XPotMin - 1.5 * self.r500c) / a, (XPotMin + 1.5 * self.r500c) / a], [(YPotMin - 1.5 * self.r500c) / a, (YPotMin + 1.5 * self.r500c) / a], [(ZPotMin - 1.5 * self.r500c) / a, (ZPotMin + 1.5 * self.r500c) / a]] mask.constrain_spatial(region) mask.constrain_mask("gas", "temperatures", Tcut_halogas * mask.units.temperature, 1.e12 * mask.units.temperature) data = sw.load(self.zoom.snapshot_path, mask=mask) self.fbary = Cosmology().get_baryon_fraction(data.metadata.z) # Convert datasets to physical quantities # r500c is already in physical units data.gas.coordinates.convert_to_physical() data.gas.masses.convert_to_physical() data.gas.temperatures.convert_to_physical() data.gas.densities.convert_to_physical() data.dark_matter.coordinates.convert_to_physical() data.dark_matter.masses.convert_to_physical() data.stars.coordinates.convert_to_physical() data.stars.masses.convert_to_physical() # Set bounds for the radial profiles radius_bounds = [0.15, 1.5] lbins = np.logspace(np.log10(radius_bounds[0]), np.log10(radius_bounds[1]), true_data_nbins) * dimensionless shell_volume = (4 / 3 * np.pi) * self.r500c**3 * (lbins[1:]**3 - lbins[:-1]**3) critical_density = unyt_quantity( data.metadata.cosmology.critical_density(data.metadata.z).value, 'g/cm**3').to('Msun/Mpc**3') # Select hot gas within sphere and without core deltaX = data.gas.coordinates[:, 0] - XPotMin deltaY = data.gas.coordinates[:, 1] - YPotMin deltaZ = data.gas.coordinates[:, 2] - ZPotMin deltaR = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) / self.r500c # Keep only particles inside 1.5 R500crit index = np.where(deltaR < radius_bounds[1])[0] central_mass = sum( data.gas.masses[np.where(deltaR < radius_bounds[0])[0]]) mass_weights, _ = histogram_unyt(deltaR[index], bins=lbins, weights=data.gas.masses[index]) self.density_profile_input = mass_weights / shell_volume / critical_density # Select DM within sphere and without core deltaX = data.dark_matter.coordinates[:, 0] - XPotMin deltaY = data.dark_matter.coordinates[:, 1] - YPotMin deltaZ = data.dark_matter.coordinates[:, 2] - ZPotMin deltaR = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) / self.r500c # Keep only particles inside 1.5 R500crit index = np.where(deltaR < radius_bounds[1])[0] central_mass += sum( data.dark_matter.masses[np.where(deltaR < radius_bounds[0])[0]]) _mass_weights, _ = histogram_unyt( deltaR[index], bins=lbins, weights=data.dark_matter.masses[index]) mass_weights += _mass_weights # Select stars within sphere and without core deltaX = data.stars.coordinates[:, 0] - XPotMin deltaY = data.stars.coordinates[:, 1] - YPotMin deltaZ = data.stars.coordinates[:, 2] - ZPotMin deltaR = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**2) / self.r500c # Keep only particles inside 1.5 R500crit index = np.where(deltaR < radius_bounds[1])[0] central_mass += sum( data.stars.masses[np.where(deltaR < radius_bounds[0])[0]]) _mass_weights, _ = histogram_unyt(deltaR[index], bins=lbins, weights=data.stars.masses[index]) mass_weights += _mass_weights # Replace zeros with Nans mass_weights[mass_weights == 0] = np.nan cumulative_mass = central_mass + cumsum_unyt(mass_weights) self.radial_bin_centres_input = 10.0**( 0.5 * np.log10(lbins[1:] * lbins[:-1])) * dimensionless self.cumulative_mass_input = cumulative_mass.to('Msun') self.total_density_profile_input = mass_weights / shell_volume / critical_density
def density_profile_parent_plot(halo_id: int, author: str, snap_filepath_parent: str = None, output_directory: str = None) -> None: # Load VR output gathered from the halo selection process lines = np.loadtxt(f"{output_directory}/halo_selected_{author}.txt", comments="#", delimiter=",", unpack=False).T M200c = lines[1] * 1e13 R200c = lines[2] Xcminpot = lines[3] Ycminpot = lines[4] Zcminpot = lines[5] M200c = unyt.unyt_quantity(M200c[halo_id], unyt.Solar_Mass) R200c = unyt.unyt_quantity(R200c[halo_id], unyt.Mpc) xCen = unyt.unyt_quantity(Xcminpot[halo_id], unyt.Mpc) yCen = unyt.unyt_quantity(Ycminpot[halo_id], unyt.Mpc) zCen = unyt.unyt_quantity(Zcminpot[halo_id], unyt.Mpc) # Construct spatial mask to feed into swiftsimio size = radius_bounds[1] * R200c mask = sw.mask(snap_filepath_parent) region = [[xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size]] mask.constrain_spatial(region) data = sw.load(snap_filepath_parent, mask=mask) # Get DM particle coordinates and compute radial distance from CoP in R200 units posDM = data.dark_matter.coordinates / data.metadata.a r = np.sqrt((posDM[:, 0] - xCen)**2 + (posDM[:, 1] - yCen)**2 + (posDM[:, 2] - zCen)**2) / R200c # Calculate particle mass and rho_crit unitLength = data.metadata.units.length unitMass = data.metadata.units.mass rho_crit = unyt.unyt_quantity( data.metadata.cosmology['Critical density [internal units]'], unitMass / unitLength**3) rhoMean = rho_crit * data.metadata.cosmology['Omega_m'] vol = data.metadata.boxsize[0]**3 numPart = data.metadata.n_dark_matter particleMass = rhoMean * vol / numPart parent_mass_resolution = particleMass # Construct bins and compute density profile lbins = np.logspace(np.log10(radius_bounds[0]), np.log10(radius_bounds[1]), bins) hist, bin_edges = np.histogram(r, bins=lbins) bin_centre = np.sqrt(bin_edges[1:] * bin_edges[:-1]) volume_shell = (4. * np.pi / 3.) * (R200c**3) * ((bin_edges[1:])**3 - (bin_edges[:-1])**3) densities = hist * particleMass / volume_shell / rho_crit # Plot density profile for each selected halo in volume fig, ax = plt.subplots() parent_label = f'Parent: $m_\\mathrm{{DM}} = {latex_float(parent_mass_resolution.value[0])}\\ {parent_mass_resolution.units.latex_repr}$' ax.plot(bin_centre, densities, c="lime", linestyle="-", label=parent_label) ax.text( 0.025, 0.025, (f"Halo {halo_id:d} DMO\n" f"$z={data.metadata.z:3.3f}$\n" "Parent VR output:\n" f"$M_{{200c}}={latex_float(M200c.value)}\\ {M200c.units.latex_repr}$\n" f"$R_{{200c}}={latex_float(R200c.value)}\\ {R200c.units.latex_repr}$" ), color="black", ha="left", va="bottom", transform=ax.transAxes, ) ax.set_xlim(radius_bounds[0], radius_bounds[1]) ax.set_xscale('log') ax.set_yscale('log') ax.set_ylabel(r"$\rho_{DM}\ /\ \rho_c$") ax.set_xlabel(r"$R\ /\ R_{200c}$") plt.legend() fig.tight_layout() fig.savefig( f"{output_directory}/halo{halo_id}{author}_density_profile_parent.png") plt.close(fig) return
def visualise_halo( output_path: Path, snapshot_path: Path, config: ImageConfig, halo: Halo, ): """ Creates all of the visualisations in the config for the specified halo, and saves them to disk. Parameters ---------- output_path: Path, str Output path to save images to. Inside this path, there will be a number of directories created (one per halo). This path must already exist. snapshot_path: Path, Path to the snapshot. For a sufficiently large volume, and a sufficiently small number of haloes, there will be little-to -no overlap in the read regions. config: ImageConfig Opened configuration file. halo: Halo The halo to read the data for and visualise. """ # First need to find the maximum radius, amongst any # of the images. radii = [ image.get_radius(stellar_half_mass=halo.radius_100_kpc_star, r_200_crit=halo.radius_200_crit) for image in config.images ] max_radius = max(radii) extent_given_r = lambda r: [[halo.position[x] - r, halo.position[x] + r] for x in range(3)] halo_mask = mask(filename=snapshot_path, spatial_only=True) halo_mask.constrain_spatial(restrict=extent_given_r(max_radius)) data = load(filename=snapshot_path, mask=halo_mask) # Generate the smoothing lengths if required. if config.calculate_dark_matter_smoothing_lengths: data.dark_matter.smoothing_lengths = generate_smoothing_lengths( coordinates=data.dark_matter.coordinates, boxsize=data.metadata.boxsize, kernel_gamma=kernel_gamma, ) if config.recalculate_stellar_smoothing_lengths and hasattr(data, "stars"): if len(data.stars.coordinates) > 0: data.stars.smoothing_lengths = generate_smoothing_lengths( coordinates=data.stars.coordinates, boxsize=data.metadata.boxsize, kernel_gamma=kernel_gamma, ) halo_directory = output_path / f"halo_{halo.unique_id}" halo_directory.mkdir(exist_ok=True) for image in config.images: # Which projections should we make? projections = [Projection.DEFAULT] if image.face_on: projections.append(Projection.FACE_ON) if image.edge_on: projections.append(Projection.EDGE_ON) for projection in projections: scatter = create_scatter( snapshot=data, halo=halo, image=image, projection=projection, resolution=config.resolution, ) save_figure_from_scatter( scatter=scatter, config=config, halo=halo, image=image, projection=projection, output_path=halo_directory, ) if (projection == Projection.DEFAULT and image.base_name == config.thumbnail_image): save_thumbnail_from_scatter( scatter=scatter, config=config, halo=halo, image=image, projection=projection, output_path=halo_directory, )
def density_profile_compare_plot( run_name: str, snap_filepath_parent: str = None, velociraptor_properties_parent: str = None, snap_filepath_zoom: List[str] = None, velociraptor_properties_zoom: List[str] = None, output_directory: str = None) -> None: """ This function compares the density profiles of DMO zooms to their corresponding parent halo. It also allows to assess numerical convergence by overlapping multiple profiles from zooms with different resolutions. The density profiles are then listed in the legend, where the DM particle mass is quoted. The zoom inputs are in the form of arrays of strings, to allow for multiple entries. Each entry is a zoom snap/VR output resolution. The function allows not to plot either the parent density profile or the zooms. At least one of them must be entered. :param run_name: str A custom and identifiable name for the run. Currently the standard name follows the scheme {AUTHOR_INITIALS}{HALO_ID}, e.g. SK0 or EA1. This argument must be defined. :param snap_filepath_parent: str The complete path to the snapshot of the parent box. If parameter is None, the density ptofile of the parent box is not displayed. :param snap_filepath_zoom: list(str) The list of complete paths to the snapshots of the zooms at different resolution. Note: the order must match that of the `velociraptor_properties_zoom` parameter. If parameter is None, the density ptofile of the zoom is not displayed and the `velociraptor_properties_zoom` parameter is ignored. :param velociraptor_properties_zoom: list(str) The list of complete paths to the VR outputs (properties) of the zooms at different resolution. Note: the order must match that of the `snap_filepath_zoom` parameter. If `snap_filepath_zoom` is None, then this parameter is ignored. If this parameter is None and `snap_filepath_zoom` is defined, raises an error. :param output_directory: str The output directory where to save the plot. This code assumes that the output directory exists. If it does not exist, matplotlib will return an error. This argument must be defined. :return: None """ # ARGS CHECK # assert snap_filepath_parent or snap_filepath_zoom if snap_filepath_zoom and velociraptor_properties_zoom: assert len(snap_filepath_zoom) == len(velociraptor_properties_zoom) elif not snap_filepath_zoom: velociraptor_properties_zoom = None elif snap_filepath_zoom and not velociraptor_properties_zoom: raise ValueError assert output_directory fig, (ax, ax_residual) = plt.subplots(nrows=2, ncols=1, figsize=(7, 8), dpi=resolution // 8, sharex=True, gridspec_kw={'height_ratios': [3, 1]}) # PARENT # if snap_filepath_parent: # Rendezvous over parent VR catalogue using zoom information with h5py.File(velociraptor_properties_zoom[0], 'r') as vr_file: idx, M200c, R200c, Xcminpot, Ycminpot, Zcminpot = find_object( vr_properties_catalog=velociraptor_properties_parent, sample_structType=10, sample_mass_lower_lim=vr_file['/Mass_200crit'][0] * 1e10 * 0.8, sample_x=vr_file['/Xcminpot'][0], sample_y=vr_file['/Ycminpot'][0], sample_z=vr_file['/Zcminpot'][0], ) M200c = unyt.unyt_quantity(M200c, unyt.Solar_Mass) R200c = unyt.unyt_quantity(R200c, unyt.Mpc) xCen = unyt.unyt_quantity(Xcminpot, unyt.Mpc) yCen = unyt.unyt_quantity(Ycminpot, unyt.Mpc) zCen = unyt.unyt_quantity(Zcminpot, unyt.Mpc) # Construct spatial mask to feed into swiftsimio size = radius_bounds[1] * R200c mask = sw.mask(snap_filepath_parent) region = [[xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size]] mask.constrain_spatial(region) data = sw.load(snap_filepath_parent, mask=mask) # Get DM particle coordinates and compute radial distance from CoP in R200 units posDM = data.dark_matter.coordinates / data.metadata.a r = np.sqrt( wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0])**2 + wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1])**2 + wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2])**2) # Calculate particle mass and rho_crit unitLength = data.metadata.units.length unitMass = data.metadata.units.mass rho_crit = unyt.unyt_quantity( data.metadata.cosmology_raw['Critical density [internal units]'], unitMass / unitLength**3).to('Msun/Mpc**3') rhoMean = rho_crit * data.metadata.cosmology.Om0 vol = data.metadata.boxsize[0]**3 numPart = data.metadata.n_dark_matter particleMass = rhoMean * vol / numPart parent_mass_resolution = particleMass particleMasses = np.ones_like(r.value) * particleMass # Construct bins and compute density profile # NOTE: numpy.histogram does not preserve units, so restore them after. lbins = np.logspace(np.log10(radius_bounds[0]), np.log10(radius_bounds[1]), bins) r_scaled = r / R200c hist, bin_edges = np.histogram(r_scaled, bins=lbins, weights=particleMasses) hist *= unyt.Solar_Mass bin_centre = np.sqrt(bin_edges[1:] * bin_edges[:-1]) volume_shell = (4. * np.pi / 3.) * (R200c**3) * ((bin_edges[1:])**3 - (bin_edges[:-1])**3) densities_parent = hist / volume_shell / rho_crit # Plot density profile for each selected halo in volume densities_parent[densities_parent == 0] = np.nan parent_label = ( f'Parent: $m_\\mathrm{{DM}} = {latex_float(parent_mass_resolution.value[0])}\\ ' f'{parent_mass_resolution.units.latex_repr}$') ax.plot(bin_centre, densities_parent, c="grey", linestyle="-", label=parent_label) # Compute convergence radius conv_radius = convergence_radius(r, particleMasses, rho_crit) / R200c ax.axvline(conv_radius, color='grey', linestyle='--') ax_residual.axvline(conv_radius, color='grey', linestyle='--') t = ax.text(conv_radius, ax.get_ylim()[1], 'Convergence radius', ha='center', va='top', rotation='vertical', alpha=0.6) t.set_bbox(dict(facecolor='white', alpha=0.6, edgecolor='none')) # ZOOMS # if snap_filepath_zoom: # Set-up colors cmap_discrete = plt.cm.get_cmap(cmap_name, len(velociraptor_properties_zoom) + 3) cmaplist = [cmap_discrete(i) for i in range(cmap_discrete.N)] for snap_path, vrprop_path, color in zip(snap_filepath_zoom, velociraptor_properties_zoom, cmaplist): # Load velociraptor data with h5py.File(vrprop_path, 'r') as vr_file: M200c = vr_file['/Mass_200crit'][0] * 1e10 R200c = vr_file['/R_200crit'][0] Xcminpot = vr_file['/Xcminpot'][0] Ycminpot = vr_file['/Ycminpot'][0] Zcminpot = vr_file['/Zcminpot'][0] M200c = unyt.unyt_quantity(M200c, unyt.Solar_Mass) R200c = unyt.unyt_quantity(R200c, unyt.Mpc) xCen = unyt.unyt_quantity(Xcminpot, unyt.Mpc) yCen = unyt.unyt_quantity(Ycminpot, unyt.Mpc) zCen = unyt.unyt_quantity(Zcminpot, unyt.Mpc) # Construct spatial mask to feed into swiftsimio size = radius_bounds[1] * R200c mask = sw.mask(snap_path) region = [[xCen - size, xCen + size], [yCen - size, yCen + size], [zCen - size, zCen + size]] mask.constrain_spatial(region) data = sw.load(snap_path, mask=mask) # Get DM particle coordinates and compute radial distance from CoP in R200 units posDM = data.dark_matter.coordinates / data.metadata.a r = np.sqrt( wrap(posDM[:, 0] - xCen, data.metadata.boxsize[0])**2 + wrap(posDM[:, 1] - yCen, data.metadata.boxsize[1])**2 + wrap(posDM[:, 2] - zCen, data.metadata.boxsize[2])**2) # Calculate particle mass and rho_crit unitLength = data.metadata.units.length unitMass = data.metadata.units.mass rho_crit = unyt.unyt_quantity( data.metadata. cosmology_raw['Critical density [internal units]'], unitMass / unitLength**3).to('Msun/Mpc**3') particleMasses = data.dark_matter.masses.to('Msun') zoom_mass_resolution = particleMasses # Construct bins and compute density profile # NOTE: numpy.histogram does not preserve units, so restore them after. lbins = np.logspace(np.log10(radius_bounds[0]), np.log10(radius_bounds[1]), bins) r_scaled = r / R200c hist, bin_edges = np.histogram(r_scaled, bins=lbins, weights=particleMasses) hist *= unyt.Solar_Mass bin_centre = np.sqrt(bin_edges[1:] * bin_edges[:-1]) volume_shell = (4. * np.pi / 3.) * (R200c**3) * ( (bin_edges[1:])**3 - (bin_edges[:-1])**3) densities_zoom = hist / volume_shell / rho_crit # Plot density profile for each selected halo in volume densities_zoom[densities_zoom == 0] = np.nan zoom_label = ( f'Zoom: $m_\\mathrm{{DM}} = {latex_float(zoom_mass_resolution.value[0])}\\ ' f'{zoom_mass_resolution.units.latex_repr}$') ax.plot(bin_centre, densities_zoom, c=color, linestyle="-", label=zoom_label) # Compute convergence radius conv_radius = convergence_radius(r, particleMasses, rho_crit) / R200c ax.axvline(conv_radius, color=color, linestyle='--') t = ax.text(conv_radius, ax.get_ylim()[1], 'Convergence radius', ha='center', va='top', rotation='vertical', alpha=0.5) t.set_bbox(dict(facecolor='white', alpha=0.6, edgecolor='none')) # RESIDUALS # if snap_filepath_parent and snap_filepath_zoom: residual = (densities_zoom - densities_parent) / densities_parent ax_residual.axhline(0, color='grey', linestyle='-') ax_residual.plot(bin_centre, residual, c=color, linestyle="-") ax_residual.axvline(conv_radius, color=color, linestyle='--') ax.text( 0.025, 0.025, (f"Halo {run_name:s} DMO\n" f"$z={data.metadata.z:3.3f}$\n" "Zoom VR output:\n" f"$M_{{200c}}={latex_float(M200c.value)}\\ {M200c.units.latex_repr}$\n" f"$R_{{200c}}={latex_float(R200c.value)}\\ {R200c.units.latex_repr}$" ), color="black", ha="left", va="bottom", backgroundcolor='white', alpha=0.5, transform=ax.transAxes, ) ax.axvline(1, color="grey", linestyle='--') ax_residual.axvline(1, color="grey", linestyle='--') ax_residual.set_xlim(radius_bounds[0], radius_bounds[1]) ax_residual.set_ylim(residual_bounds[0], residual_bounds[1]) ax.set_xscale('log') ax.set_yscale('log') ax.set_ylabel(r"$\rho_{DM}\ /\ \rho_c$") ax_residual.set_ylabel(f"$\\Delta \\rho\\ /\\ \\rho_{{\\rm parent}}$") ax_residual.set_xlabel(r"$R\ /\ R_{200c}$") ax.legend(loc="upper right") fig.tight_layout() fig.savefig(f"{output_directory}/{run_name}_density_profile_compare.png") plt.close(fig) print(f"Saved: {output_directory}/{run_name}_density_profile_compare.png") plt.close('all') return
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 feedback_stats_dT(path_to_snap: str, path_to_catalogue: str) -> dict: # Read in halo properties with h5py.File(f'{path_to_catalogue}', 'r') as h5file: XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) R500c = unyt.unyt_quantity(h5file['/SO_R_500_rhocrit'][0], unyt.Mpc) # Read in particles mask = sw.mask(f'{path_to_snap}', spatial_only=True) region = [[ XPotMin - radius_bounds[1] * R500c, XPotMin + radius_bounds[1] * R500c ], [ YPotMin - radius_bounds[1] * R500c, YPotMin + radius_bounds[1] * R500c ], [ ZPotMin - radius_bounds[1] * R500c, ZPotMin + radius_bounds[1] * R500c ]] mask.constrain_spatial(region) data = sw.load(f'{path_to_snap}', mask=mask) # Get positions for all BHs in the bounding region bh_positions = data.black_holes.coordinates bh_coordX = bh_positions[:, 0] - XPotMin bh_coordY = bh_positions[:, 1] - YPotMin bh_coordZ = bh_positions[:, 2] - ZPotMin bh_radial_distance = np.sqrt(bh_coordX**2 + bh_coordY**2 + bh_coordZ**2) # The central SMBH will probably be massive. # Narrow down the search to the BH with top 5% in mass bh_masses = data.black_holes.subgrid_masses.to_physical() bh_top_massive_index = np.where( bh_masses > np.percentile(bh_masses.value, 95))[0] # Get the central BH closest to centre of halo at z=0 central_bh_index = np.argmin(bh_radial_distance[bh_top_massive_index]) central_bh_id_target = data.black_holes.particle_ids[bh_top_massive_index][ central_bh_index] # Initialise typed dictionary for the central BH central_bh = defaultdict(list) central_bh['x'] = [] central_bh['y'] = [] central_bh['z'] = [] central_bh['dx'] = [] central_bh['dy'] = [] central_bh['dz'] = [] central_bh['dr'] = [] central_bh['mass'] = [] central_bh['m500c'] = [] central_bh['id'] = [] central_bh['redshift'] = [] central_bh['time'] = [] # Retrieve BH data from other snaps # Clip redshift data (get snaps below that redshifts) z_clip = 5. all_snaps = get_allpaths_from_last(path_to_snap, z_max=z_clip) all_catalogues = get_allpaths_from_last(path_to_catalogue, z_max=z_clip) assert len(all_snaps) == len(all_catalogues), ( f"Detected different number of high-z snaps and high-z catalogues. " f"Number of snaps: {len(all_snaps)}. Number of catalogues: {len(all_catalogues)}." ) for i, (highz_snap, highz_catalogue) in enumerate(zip(all_snaps, all_catalogues)): if not SILENT_PROGRESSBAR: print((f"Analysing snap ({i + 1}/{len(all_snaps)}):\n" f"\t{os.path.basename(highz_snap)}\n" f"\t{os.path.basename(highz_catalogue)}")) with h5py.File(f'{highz_catalogue}', 'r') as h5file: XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) / data.metadata.a YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) / data.metadata.a ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) / data.metadata.a M500c = unyt.unyt_quantity( h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass) data = sw.load(highz_snap) bh_positions = data.black_holes.coordinates.to_physical() bh_coordX = bh_positions[:, 0] - XPotMin bh_coordY = bh_positions[:, 1] - YPotMin bh_coordZ = bh_positions[:, 2] - ZPotMin bh_radial_distance = np.sqrt(bh_coordX**2 + bh_coordY**2 + bh_coordZ**2) bh_masses = data.black_holes.subgrid_masses.to_physical() if BH_LOCK_ID: central_bh_index = np.where( data.black_holes.particle_ids.v == central_bh_id_target.v)[0] else: central_bh_index = np.argmin(bh_radial_distance) central_bh['x'].append(bh_positions[central_bh_index, 0]) central_bh['y'].append(bh_positions[central_bh_index, 1]) central_bh['z'].append(bh_positions[central_bh_index, 2]) central_bh['dx'].append(bh_coordX[central_bh_index]) central_bh['dy'].append(bh_coordY[central_bh_index]) central_bh['dz'].append(bh_coordZ[central_bh_index]) central_bh['dr'].append(bh_radial_distance[central_bh_index]) central_bh['mass'].append(bh_masses[central_bh_index]) central_bh['m500c'].append(M500c) central_bh['id'].append( data.black_holes.particle_ids[central_bh_index]) central_bh['redshift'].append(data.metadata.redshift) central_bh['time'].append(data.metadata.time) if INCLUDE_SNIPS: unitLength = data.metadata.units.length unitMass = data.metadata.units.mass unitTime = data.metadata.units.time snip_handles = get_snip_handles(path_to_snap, z_max=z_clip) for snip_handle in tqdm(snip_handles, desc=f"Analysing snipshots", disable=SILENT_PROGRESSBAR): # Open the snipshot file from the I/O stream. # Cannot use Swiftsimio, since it automatically looks for PartType0, # which is not included in snipshot outputs. with h5py.File(snip_handle, 'r') as f: bh_positions = f['/PartType5/Coordinates'][...] bh_masses = f['/PartType5/SubgridMasses'][...] bh_ids = f['/PartType5/ParticleIDs'][...] redshift = f['Header'].attrs['Redshift'][0] time = f['Header'].attrs['Time'][0] a = f['Header'].attrs['Scale-factor'][0] if BH_LOCK_ID: central_bh_index = np.where( bh_ids == central_bh_id_target.v)[0] else: raise ValueError(( "Trying to lock the central BH to the halo centre of potential " "in snipshots, which do not have corresponding halo catalogues. " "Please, lock the central BH to the particle ID found at z = 0. " "While this set-up is not yet implemented, it would be possible to " "interpolate the position of the CoP between snapshots w.r.t. cosmic time." )) # This time we need to manually convert to physical coordinates and assign units. # Catalogue-dependent quantities are not appended. central_bh['x'].append( unyt.unyt_quantity(bh_positions[central_bh_index, 0] / a, unitLength)) central_bh['y'].append( unyt.unyt_quantity(bh_positions[central_bh_index, 1] / a, unitLength)) central_bh['z'].append( unyt.unyt_quantity(bh_positions[central_bh_index, 2] / a, unitLength)) # central_bh['dx'].append(np.nan) # central_bh['dy'].append(np.nan) # central_bh['dz'].append(np.nan) # central_bh['dr'].append(np.nan) central_bh['mass'].append( unyt.unyt_quantity(bh_masses[central_bh_index], unitMass)) # central_bh['m500c'].append(np.nan) central_bh['id'].append( unyt.unyt_quantity(bh_ids[central_bh_index], unyt.dimensionless)) central_bh['redshift'].append( unyt.unyt_quantity(redshift, unyt.dimensionless)) central_bh['time'].append(unyt.unyt_quantity(time, unitTime)) # Convert lists to Swiftsimio cosmo arrays for key in central_bh: central_bh[key] = sw.cosmo_array(central_bh[key]).flatten() if not SILENT_PROGRESSBAR: print( f"Central BH memory [{key}]: {central_bh[key].nbytes / 1024:.3f} kB" ) return central_bh
import swiftsimio as sw from swiftsimio.visualisation.projection import project_gas import argparse from matplotlib.pyplot import imsave from matplotlib.colors import LogNorm parser = argparse.ArgumentParser() parser.add_argument('-i', '--ic-file', type=str, required=True) parser.add_argument('-t', '--top-cells-per-tile', type=int, default=3, required=False) parser.add_argument('-o', '--outdir', type=str, default='.', required=False) args = parser.parse_args() mask = sw.mask(args.ic_file) boxsize = mask.metadata.boxsize load_region = [ [0. * boxsize[0], 1. * boxsize[0]], [0. * boxsize[1], 1. * boxsize[1]], [0. * boxsize[2], 0.1 * boxsize[2]], ] mask.constrain_spatial(load_region) data = sw.load(args.ic_file, mask=mask) mass_map = project_gas( data, resolution=1024, project="densities", parallel=True, backend="subsampled" )
def process_single_halo( path_to_snap: str, path_to_catalogue: str, hse_dataset: pd.Series = None, ) -> Tuple[unyt.unyt_quantity]: # Read in halo properties with h5.File(path_to_catalogue, 'r') as h5file: scale_factor = float(h5file['/SimulationInfo'].attrs['ScaleFactor']) M500c = unyt.unyt_quantity(h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass) R500c = unyt.unyt_quantity(h5file['/SO_R_500_rhocrit'][0], unyt.Mpc) / scale_factor XPotMin = unyt.unyt_quantity(h5file['/Xcminpot'][0], unyt.Mpc) / scale_factor YPotMin = unyt.unyt_quantity(h5file['/Ycminpot'][0], unyt.Mpc) / scale_factor ZPotMin = unyt.unyt_quantity(h5file['/Zcminpot'][0], unyt.Mpc) / scale_factor # If no custom aperture, select r500c as default if hse_dataset is not None: assert R500c.units == hse_dataset["R500hse"].units assert M500c.units == hse_dataset["M500hse"].units R500c = hse_dataset["R500hse"] M500c = hse_dataset["M500hse"] # Read in gas particles to compute the core-excised temperature mask = sw.mask(path_to_snap, spatial_only=False) region = [[XPotMin - 1.1 * R500c, XPotMin + 1.1 * R500c], [YPotMin - 1.1 * R500c, YPotMin + 1.1 * R500c], [ZPotMin - 1.1 * R500c, ZPotMin + 1.1 * R500c]] mask.constrain_spatial(region) mask.constrain_mask( "gas", "temperatures", Tcut_halogas * mask.units.temperature, 1.e12 * mask.units.temperature ) data = sw.load(path_to_snap, mask=mask) posGas = data.gas.coordinates massGas = data.gas.masses velGas = data.gas.velocities mass_weighted_temperatures = data.gas.temperatures * data.gas.masses # Select hot gas within sphere and without core deltaX = posGas[:, 0] - XPotMin deltaY = posGas[:, 1] - YPotMin deltaZ = posGas[:, 2] - ZPotMin deltaR = np.sqrt(deltaX ** 2 + deltaY ** 2 + deltaZ ** 2) # Count only particles inside R500crit index = np.where(deltaR < R500c)[0] # Compute kinetic energy in the halo's rest frame peculiar_velocity = np.sum(velGas[index] * massGas[index, None], axis=0) / np.sum(massGas[index]) velGas[:, 0] -= peculiar_velocity[0] velGas[:, 1] -= peculiar_velocity[1] velGas[:, 2] -= peculiar_velocity[2] Ekin = np.sum( 0.5 * massGas[index] * (velGas[index, 0] ** 2 + velGas[index, 1] ** 2 + velGas[index, 2] ** 2) ).to("1.e10*Mpc**2*Msun/Gyr**2") Etherm = np.sum( 1.5 * unyt.boltzmann_constant * mass_weighted_temperatures[index] / (unyt.hydrogen_mass / 1.16) ).to("1.e10*Mpc**2*Msun/Gyr**2") return M500c, Ekin, Etherm
def create_single_particle_dataset(filename: str, output_name: str): """ Create an hdf5 snapshot with two particles at an identical location Parameters ---------- filename: str name of file from which to copy metadata output_name: str name of single particle snapshot """ # Create a dummy mask in order to write metadata data_mask = mask(filename) boxsize = data_mask.metadata.boxsize region = [[0, b] for b in boxsize] data_mask.constrain_spatial(region) # Write the metadata infile = h5py.File(filename, "r") outfile = h5py.File(output_name, "w") list_of_links, _ = find_links(infile) write_metadata(infile, outfile, list_of_links, data_mask) # Write a single particle particle_coords = cosmo_array([[1, 1, 1], [1, 1, 1]], data_mask.metadata.units.length) particle_masses = cosmo_array([1, 1], data_mask.metadata.units.mass) mean_h = mean(infile["/PartType0/SmoothingLengths"]) particle_h = cosmo_array([mean_h, mean_h], data_mask.metadata.units.length) particle_ids = [1, 2] coords = outfile.create_dataset("/PartType0/Coordinates", data=particle_coords) for name, value in infile["/PartType0/Coordinates"].attrs.items(): coords.attrs.create(name, value) masses = outfile.create_dataset("/PartType0/Masses", data=particle_masses) for name, value in infile["/PartType0/Masses"].attrs.items(): masses.attrs.create(name, value) h = outfile.create_dataset("/PartType0/SmoothingLengths", data=particle_h) for name, value in infile["/PartType0/SmoothingLengths"].attrs.items(): h.attrs.create(name, value) ids = outfile.create_dataset("/PartType0/ParticleIDs", data=particle_ids) for name, value in infile["/PartType0/ParticleIDs"].attrs.items(): ids.attrs.create(name, value) # Get rid of all traces of DM del outfile["/Cells/Counts/PartType1"] del outfile["/Cells/Offsets/PartType1"] nparts_total = [2, 0, 0, 0, 0, 0] nparts_this_file = [2, 0, 0, 0, 0, 0] outfile["/Header"].attrs["NumPart_Total"] = nparts_total outfile["/Header"].attrs["NumPart_ThisFile"] = nparts_this_file # Tidy up infile.close() outfile.close() return