コード例 #1
0
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
コード例 #2
0
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
コード例 #3
0
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