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 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 profile_3d_single_halo( path_to_snap: str, path_to_catalogue: str, weights: str, hse_dataset: pd.Series = None, ) -> tuple: # Read in halo properties with h5.File(path_to_catalogue, 'r') as h5file: scale_factor = float(h5file['/SimulationInfo'].attrs['ScaleFactor']) M200c = unyt.unyt_quantity(h5file['/Mass_200crit'][0] * 1.e10, unyt.Solar_Mass) M500c = unyt.unyt_quantity(h5file['/SO_Mass_500_rhocrit'][0] * 1.e10, unyt.Solar_Mass) R200c = unyt.unyt_quantity(h5file['/R_200crit'][0], unyt.Mpc) / scale_factor 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 mask = sw.mask(path_to_snap, spatial_only=False) 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) mask.constrain_mask("gas", "temperatures", Tcut_halogas * mask.units.temperature, 1.e12 * mask.units.temperature) data = sw.load(path_to_snap, mask=mask) # Convert datasets to physical quantities R500c *= scale_factor XPotMin *= scale_factor YPotMin *= scale_factor ZPotMin *= scale_factor 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.gas.pressures.convert_to_physical() data.gas.entropies.convert_to_physical() data.dark_matter.masses.convert_to_physical() # Select hot gas within sphere posGas = data.gas.coordinates deltaX = posGas[:, 0] - XPotMin deltaY = posGas[:, 1] - YPotMin deltaZ = posGas[:, 2] - ZPotMin deltaR = np.sqrt(deltaX**2 + deltaY**2 + deltaZ**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]'] / scale_factor**3, unitMass / unitLength**3) dm_masses = data.dark_matter.masses.to('Msun') zoom_mass_resolution = dm_masses[0] # Since useful for different applications, attach datasets data.gas.mass_weighted_temperatures = data.gas.masses * data.gas.temperatures # Rescale profiles to r500c radial_distance = deltaR / R500c assert radial_distance.units == unyt.dimensionless # Compute convergence radius conv_radius = convergence_radius(deltaR, data.gas.masses.to('Msun'), rho_crit.to('Msun/Mpc**3')) / R500c # Construct bins for mass-weighted quantities and retrieve bin_edges lbins = np.logspace(np.log10(radius_bounds[0]), np.log10(radius_bounds[1]), bins) * radial_distance.units mass_weights, bin_edges = histogram_unyt(radial_distance, bins=lbins, weights=data.gas.masses) # Replace zeros with Nans mass_weights[mass_weights == 0] = np.nan bin_centre = np.sqrt(bin_edges[1:] * bin_edges[:-1]) # Allocate weights if weights.lower() == 'gas_mass': hist = mass_weights / M500c.to(mass_weights.units) ylabel = r'$M(dR) / M_{500{\rm crit}}$' elif weights.lower() == 'gas_mass_cumulative': hist = cumsum_unyt(mass_weights) / M500c.to(mass_weights.units) ylabel = r'$M(<R) / M_{500{\rm crit}}$' elif weights.lower() == 'gas_density': hist, _ = histogram_unyt(radial_distance, bins=lbins, weights=data.gas.densities) hist /= rho_crit.to(hist.units) hist *= bin_centre**2 ylabel = r'$(\rho_{\rm gas}/\rho_{\rm crit})\ (R/R_{500{\rm crit}})^3 $' elif weights.lower() == 'mass_weighted_temps': weights_field = data.gas.mass_weighted_temperatures hist, _ = histogram_unyt(radial_distance, bins=lbins, weights=weights_field) hist /= mass_weights if sampling_method.lower() == 'no_binning': bin_centre = radial_distance hist = data.gas.temperatures ylabel = r'$T$ [K]' elif weights.lower() == 'mass_weighted_temps_kev': weights_field = data.gas.mass_weighted_temperatures hist, _ = histogram_unyt(radial_distance, bins=lbins, weights=weights_field) hist /= mass_weights hist = (hist * unyt.boltzmann_constant).to('keV') # Make dimensionless, divide by (k_B T_500crit) # unyt.G.in_units('Mpc*(km/s)**2/(1e10*Msun)') norm = unyt.G * mean_molecular_weight * M500c * unyt.mass_proton / 2 / R500c norm = norm.to('keV') hist /= norm ylabel = r'$(k_B T/k_B T_{500{\rm crit}})$' elif weights.lower() == 'entropy': if sampling_method.lower() == 'shell_density': volume_shell = (4. * np.pi / 3.) * (R500c**3) * ( (bin_edges[1:])**3 - (bin_edges[:-1])**3) density_gas = mass_weights / volume_shell mean_density_R500c = (3 * M500c * obs.cosmic_fbary / (4 * np.pi * R500c**3)).to(density_gas.units) kBT, _ = histogram_unyt( radial_distance, bins=lbins, weights=data.gas.mass_weighted_temperatures) kBT *= unyt.boltzmann_constant kBT /= mass_weights kBT = kBT.to('keV') kBT_500crit = unyt.G * mean_molecular_weight * M500c * unyt.mass_proton / 2 / R500c kBT_500crit = kBT_500crit.to(kBT.units) # Note: the ratio of densities is the same as ratio of electron number densities hist = kBT / kBT_500crit * (mean_density_R500c / density_gas)**(2 / 3) elif sampling_method.lower() == 'particle_density': n_e = data.gas.densities ne_500crit = 3 * M500c * obs.cosmic_fbary / (4 * np.pi * R500c**3) kBT = unyt.boltzmann_constant * data.gas.mass_weighted_temperatures kBT_500crit = unyt.G * mean_molecular_weight * M500c * unyt.mass_proton / 2 / R500c weights_field = kBT / kBT_500crit * (ne_500crit / n_e)**(2 / 3) hist, _ = histogram_unyt(radial_distance, bins=lbins, weights=weights_field) hist /= mass_weights elif sampling_method.lower() == 'no_binning': n_e = data.gas.densities ne_500crit = 3 * M500c * obs.cosmic_fbary / (4 * np.pi * R500c**3) kBT = unyt.boltzmann_constant * data.gas.temperatures kBT_500crit = unyt.G * mean_molecular_weight * M500c * unyt.mass_proton / 2 / R500c weights_field = kBT / kBT_500crit * (ne_500crit / n_e)**(2 / 3) bin_centre = radial_distance hist = weights_field ylabel = r'$K/K_{500{\rm crit}}$' elif weights.lower() == 'entropy_physical': if sampling_method.lower() == 'shell_density': volume_shell = (4. * np.pi / 3.) * (R500c**3) * ( (bin_edges[1:])**3 - (bin_edges[:-1])**3) density_gas = mass_weights / volume_shell number_density_gas = density_gas / (mean_molecular_weight * unyt.mass_proton) number_density_gas = number_density_gas.to('1/cm**3') kBT, _ = histogram_unyt( radial_distance, bins=lbins, weights=data.gas.mass_weighted_temperatures) kBT *= unyt.boltzmann_constant kBT /= mass_weights kBT = kBT.to('keV') # Note: the ratio of densities is the same as ratio of electron number densities hist = kBT / number_density_gas**(2 / 3) hist = hist.to('keV*cm**2') elif sampling_method.lower() == 'particle_density': number_density_gas = data.gas.densities / (mean_molecular_weight * unyt.mass_proton) number_density_gas = number_density_gas.to('1/cm**3') kBT = unyt.boltzmann_constant * data.gas.mass_weighted_temperatures weights_field = kBT / number_density_gas**(2 / 3) hist, _ = histogram_unyt(radial_distance, bins=lbins, weights=weights_field) hist /= mass_weights hist = hist.to('keV*cm**2') elif sampling_method.lower() == 'no_binning': number_density_gas = data.gas.densities / (mean_molecular_weight * unyt.mass_proton) number_density_gas = number_density_gas.to('1/cm**3') kBT = unyt.boltzmann_constant * data.gas.temperatures weights_field = kBT / number_density_gas**(2 / 3) bin_centre = radial_distance hist = weights_field ylabel = r'$K$ [keV cm$^2$]' elif weights.lower() == 'pressure': if sampling_method.lower() == 'shell_density': volume_shell = (4. * np.pi / 3.) * (R500c**3) * ( (bin_edges[1:])**3 - (bin_edges[:-1])**3) density_gas = mass_weights / volume_shell number_density_gas = density_gas / (mean_molecular_weight * unyt.mass_proton) number_density_gas = number_density_gas.to('1/cm**3') kBT, _ = histogram_unyt( radial_distance, bins=lbins, weights=data.gas.mass_weighted_temperatures) kBT *= unyt.boltzmann_constant kBT /= mass_weights kBT = kBT.to('keV') # Note: the ratio of densities is the same as ratio of electron number densities hist = kBT * number_density_gas hist = hist.to('keV/cm**3') elif sampling_method.lower() == 'particle_density': weights_field = data.gas.pressures * data.gas.masses hist, _ = histogram_unyt(radial_distance, bins=lbins, weights=weights_field) hist /= mass_weights # Make dimensionless, divide by P_500crit norm = 500 * obs.cosmic_fbary * rho_crit * unyt.G * M500c / 2 / R500c hist /= norm.to(hist.units) hist *= bin_centre**3 ylabel = r'$(P/P_{500{\rm crit}})\ (R/R_{500{\rm crit}})^3 $' else: raise ValueError(f"Unrecognized weighting field: {weights}.") return bin_centre, hist, ylabel, conv_radius, M500c, R500c