From cd1e8abb0069406e817ceec0089722bca66da2be Mon Sep 17 00:00:00 2001 From: John Helly Date: Wed, 10 Jun 2026 10:42:59 +0100 Subject: [PATCH 1/4] Add example reading halo particles --- source/service_docs/python_examples.rst | 7 ++- source/snapshots/halo_particles.rst | 80 ++++++++++++++++++++++++ source/snapshots/images/L1_m10_halo.png | Bin 0 -> 33863 bytes source/snapshots/index.rst | 1 + source/snapshots/swiftsimio.rst | 2 + 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 source/snapshots/halo_particles.rst create mode 100644 source/snapshots/images/L1_m10_halo.png diff --git a/source/service_docs/python_examples.rst b/source/service_docs/python_examples.rst index 65c5b7f..3722b98 100644 --- a/source/service_docs/python_examples.rst +++ b/source/service_docs/python_examples.rst @@ -10,11 +10,14 @@ in python. Here, we provide links to these examples. specified region, and save part of a snapshot to a local file ` + * :doc:`Read SOAP halo properties using swiftsimio` + + * :doc:`Identify a halo of interest and read in the particles + belonging to it ` + * :doc:`Read and plot the matter power spectrum at several redshifts ` - * :doc:`Read SOAP halo properties using swiftsimio` - * :doc:`Identify the same halo between different simulations ` * :ref:`Plot the evolution of a halo across snapshots ` diff --git a/source/snapshots/halo_particles.rst b/source/snapshots/halo_particles.rst new file mode 100644 index 0000000..da698d4 --- /dev/null +++ b/source/snapshots/halo_particles.rst @@ -0,0 +1,80 @@ +Reading particles belonging to a halo +===================================== + +Here we show how to identify a halo of interest in the SOAP halo +catalogue and then find its particles in the corresponding +snapshot. This could be used to compute a halo density profile, for +example. + +In the SOAP halo catalogues, each halo is assigned an index which is +unique within the snapshot (but not between snapshots). This index is +stored in the HDF5 dataset ``InputHalos/HaloCatalogueIndex``. There is +a corresponding index ``PartTypeX/HaloCatalogueIndex`` associated with +each particle in the snapshot. + +We can read in all particles belonging to a halo as follows: + + * Use swiftsimio to read the halo catalogue (see :doc:`/soap/swiftsimio`) + * Identify a halo of interest in the catalogue + * Extract the position and ``HaloCatalogueIndex`` of this halo from the SOAP catalogue + * Read particles from the snapshot in some radius around the halo position (see :ref:`swiftsimio_cutouts`) + * Discard particles which do not have the required ``HaloCatalogueIndex`` + +A python example which carries out this operation on the +``L1_m10_DMO`` simulation is shown below: + +.. code-block:: python + + import numpy as np + import unyt + import hdfstream + import swiftsimio as sw + + # Connect to the hdfstream service and open the root directory + root_dir = hdfstream.open("cosma", "/") + + # Open the z=0 halo catalogue from the L1_m10_DMO simulation + soap_file = root_dir["FLAMINGO/L1_m10/L1_m10_DMO/SOAP-HBT/halo_properties_0077.hdf5"] + soap = sw.load(soap_file) + + # Get halo positions, masses and indexes + halo_pos = soap.input_halos.halo_centre + halo_m200c = soap.spherical_overdensity_200_crit.total_mass + halo_index = soap.input_halos.halo_catalogue_index + + # Select the most massive halo + i = np.argmax(halo_m200c) + target_pos = halo_pos[i,:] + target_index = halo_index[i] + + # Choose a region to read in. Note that we need to choose a radius large + # enough to enclose all halo particles. + radius = 5.0*unyt.Mpc + region = [[x-radius, x+radius] for x in target_pos] + + # Open the snapshot and select this region + snap_file = root_dir["FLAMINGO/L1_m10/L1_m10_DMO/snapshots/flamingo_0077/flamingo_0077.hdf5"] + mask = sw.mask(snap_file) + mask.constrain_spatial(region) + snap = sw.load(snap_file, mask=mask) + + # Read position and halo index of dark matter particles in this region + dm_pos = snap.dark_matter.coordinates + dm_halo_index = snap.dark_matter.halo_catalogue_index + + # Of the particles we read, identify those which belong to the halo + in_halo = (dm_halo_index == target_index) + halo_dm_pos = dm_pos[in_halo,:] + + # Make a simple dotplot as a sanity check + import matplotlib.pyplot as plt + plt.plot(halo_dm_pos[:,0], halo_dm_pos[:,1], "k,") + plt.gca().set_aspect("equal") + plt.xlabel("x [Mpc]") + plt.ylabel("y [Mpc]") + plt.title("Most massive halo in L1_m10_DMO") + plt.show() + +This results in the following plot showing dark matter particles in the halo: + +.. image:: images/L1_m10_halo.png diff --git a/source/snapshots/images/L1_m10_halo.png b/source/snapshots/images/L1_m10_halo.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b30391a9efd27cc5c597fe79443772a4bd60f6 GIT binary patch literal 33863 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 Particle properties Reading with swiftsimio + Reading halo particles Partial snapshots For more information about the SWIFT simulation snapshot format used diff --git a/source/snapshots/swiftsimio.rst b/source/snapshots/swiftsimio.rst index bb0a38a..d8bf9d3 100644 --- a/source/snapshots/swiftsimio.rst +++ b/source/snapshots/swiftsimio.rst @@ -156,6 +156,8 @@ Mpc:: [987.07806218 995.72685218 88.39880218] [987.07910218 995.73126218 88.39935218]] Mpc (Comoving) +.. _swiftsimio_cutouts: + Cutting out a region -------------------- From d1b034acdefb06dbe533fbc3d9119d71d9d104c7 Mon Sep 17 00:00:00 2001 From: John Helly Date: Wed, 10 Jun 2026 10:59:51 +0100 Subject: [PATCH 2/4] Add example reading halo particles with h5py --- source/snapshots/halo_particles.rst | 164 +++++++++++++++++++--------- source/soap/swiftsimio.rst | 3 + 2 files changed, 113 insertions(+), 54 deletions(-) diff --git a/source/snapshots/halo_particles.rst b/source/snapshots/halo_particles.rst index da698d4..4ffc441 100644 --- a/source/snapshots/halo_particles.rst +++ b/source/snapshots/halo_particles.rst @@ -20,60 +20,116 @@ We can read in all particles belonging to a halo as follows: * Read particles from the snapshot in some radius around the halo position (see :ref:`swiftsimio_cutouts`) * Discard particles which do not have the required ``HaloCatalogueIndex`` -A python example which carries out this operation on the -``L1_m10_DMO`` simulation is shown below: - -.. code-block:: python - - import numpy as np - import unyt - import hdfstream - import swiftsimio as sw - - # Connect to the hdfstream service and open the root directory - root_dir = hdfstream.open("cosma", "/") - - # Open the z=0 halo catalogue from the L1_m10_DMO simulation - soap_file = root_dir["FLAMINGO/L1_m10/L1_m10_DMO/SOAP-HBT/halo_properties_0077.hdf5"] - soap = sw.load(soap_file) - - # Get halo positions, masses and indexes - halo_pos = soap.input_halos.halo_centre - halo_m200c = soap.spherical_overdensity_200_crit.total_mass - halo_index = soap.input_halos.halo_catalogue_index - - # Select the most massive halo - i = np.argmax(halo_m200c) - target_pos = halo_pos[i,:] - target_index = halo_index[i] - - # Choose a region to read in. Note that we need to choose a radius large - # enough to enclose all halo particles. - radius = 5.0*unyt.Mpc - region = [[x-radius, x+radius] for x in target_pos] - - # Open the snapshot and select this region - snap_file = root_dir["FLAMINGO/L1_m10/L1_m10_DMO/snapshots/flamingo_0077/flamingo_0077.hdf5"] - mask = sw.mask(snap_file) - mask.constrain_spatial(region) - snap = sw.load(snap_file, mask=mask) - - # Read position and halo index of dark matter particles in this region - dm_pos = snap.dark_matter.coordinates - dm_halo_index = snap.dark_matter.halo_catalogue_index - - # Of the particles we read, identify those which belong to the halo - in_halo = (dm_halo_index == target_index) - halo_dm_pos = dm_pos[in_halo,:] - - # Make a simple dotplot as a sanity check - import matplotlib.pyplot as plt - plt.plot(halo_dm_pos[:,0], halo_dm_pos[:,1], "k,") - plt.gca().set_aspect("equal") - plt.xlabel("x [Mpc]") - plt.ylabel("y [Mpc]") - plt.title("Most massive halo in L1_m10_DMO") - plt.show() +This can be done either by using the :doc:`hdfstream python module +` for remote access, or by downloading +the SOAP catalogue and snapshot as HDF5 files and reading them +directly with h5py. Both methods are illustrated below. + +.. tab-set:: + + .. tab-item:: Using remote file access + + .. code-block:: python + + import numpy as np + import unyt + import hdfstream + import swiftsimio as sw + + # Connect to the hdfstream service and open the root directory + root_dir = hdfstream.open("cosma", "/") + + # Open the z=0 halo catalogue from the L1_m10_DMO simulation + soap_file = root_dir["FLAMINGO/L1_m10/L1_m10_DMO/SOAP-HBT/halo_properties_0077.hdf5"] + soap = sw.load(soap_file) + + # Get halo positions, masses and indexes + halo_pos = soap.input_halos.halo_centre + halo_m200c = soap.spherical_overdensity_200_crit.total_mass + halo_index = soap.input_halos.halo_catalogue_index + + # Select the most massive halo + i = np.argmax(halo_m200c) + target_pos = halo_pos[i,:] + target_index = halo_index[i] + + # Choose a region to read in. Note that we need to choose a radius large + # enough to enclose all halo particles. + radius = 5.0*unyt.Mpc + region = [[x-radius, x+radius] for x in target_pos] + + # Open the z=0 snapshot from the L1_m10_DMO simulation and select this region + snap_file = root_dir["FLAMINGO/L1_m10/L1_m10_DMO/snapshots/flamingo_0077/flamingo_0077.hdf5"] + mask = sw.mask(snap_file) + mask.constrain_spatial(region) + snap = sw.load(snap_file, mask=mask) + + # Read position and halo index of dark matter particles in this region + dm_pos = snap.dark_matter.coordinates + dm_halo_index = snap.dark_matter.halo_catalogue_index + + # Of the particles we read, identify those which belong to the halo + in_halo = (dm_halo_index == target_index) + halo_dm_pos = dm_pos[in_halo,:] + + # Make a simple dotplot as a sanity check + import matplotlib.pyplot as plt + plt.plot(halo_dm_pos[:,0], halo_dm_pos[:,1], "k,") + plt.gca().set_aspect("equal") + plt.xlabel("x [Mpc]") + plt.ylabel("y [Mpc]") + plt.title("Most massive halo in L1_m10_DMO") + plt.show() + + .. tab-item:: Reading local HDF5 files + + .. code-block:: python + + import numpy as np + import unyt + import swiftsimio as sw + + # Open the z=0 halo catalogue from the L1_m10_DMO simulation + soap_file = "FLAMINGO/L1_m10/L1_m10_DMO/SOAP-HBT/halo_properties_0077.hdf5" + soap = sw.load(soap_file) + + # Get halo positions, masses and indexes + halo_pos = soap.input_halos.halo_centre + halo_m200c = soap.spherical_overdensity_200_crit.total_mass + halo_index = soap.input_halos.halo_catalogue_index + + # Select the most massive halo + i = np.argmax(halo_m200c) + target_pos = halo_pos[i,:] + target_index = halo_index[i] + + # Choose a region to read in. Note that we need to choose a radius large + # enough to enclose all halo particles. + radius = 5.0*unyt.Mpc + region = [[x-radius, x+radius] for x in target_pos] + + # Open the z=0 snapshot from the L1_m10_DMO simulation and select this region + snap_file = "./FLAMINGO/L1_m10/L1_m10_DMO/snapshots/flamingo_0077/flamingo_0077.hdf5" + mask = sw.mask(snap_file) + mask.constrain_spatial(region) + snap = sw.load(snap_file, mask=mask) + + # Read position and halo index of dark matter particles in this region + dm_pos = snap.dark_matter.coordinates + dm_halo_index = snap.dark_matter.halo_catalogue_index + + # Of the particles we read, identify those which belong to the halo + in_halo = (dm_halo_index == target_index) + halo_dm_pos = dm_pos[in_halo,:] + + # Make a simple dotplot as a sanity check + import matplotlib.pyplot as plt + plt.plot(halo_dm_pos[:,0], halo_dm_pos[:,1], "k,") + plt.gca().set_aspect("equal") + plt.xlabel("x [Mpc]") + plt.ylabel("y [Mpc]") + plt.title("Most massive halo in L1_m10_DMO") + plt.show() This results in the following plot showing dark matter particles in the halo: diff --git a/source/soap/swiftsimio.rst b/source/soap/swiftsimio.rst index 12aecdc..548a810 100644 --- a/source/soap/swiftsimio.rst +++ b/source/soap/swiftsimio.rst @@ -16,6 +16,9 @@ The latter method might be preferable if you only need a small fraction of the data, such as a subset of halo properties or halos in a small region of interest. +To read the particles associated with a halo selected from a SOAP +catalogue, see :doc:`/snapshots/halo_particles`. + Installation ------------ From 8896df55beb701800d7fcca1fb3fb90978e1678c Mon Sep 17 00:00:00 2001 From: John Helly Date: Wed, 10 Jun 2026 11:07:30 +0100 Subject: [PATCH 3/4] Fix inconsistency in new example --- source/snapshots/halo_particles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/snapshots/halo_particles.rst b/source/snapshots/halo_particles.rst index 4ffc441..55d3a36 100644 --- a/source/snapshots/halo_particles.rst +++ b/source/snapshots/halo_particles.rst @@ -90,7 +90,7 @@ directly with h5py. Both methods are illustrated below. import swiftsimio as sw # Open the z=0 halo catalogue from the L1_m10_DMO simulation - soap_file = "FLAMINGO/L1_m10/L1_m10_DMO/SOAP-HBT/halo_properties_0077.hdf5" + soap_file = "./FLAMINGO/L1_m10/L1_m10_DMO/SOAP-HBT/halo_properties_0077.hdf5" soap = sw.load(soap_file) # Get halo positions, masses and indexes From b02ed5431dea57a6b24f03210063c63678c6d258 Mon Sep 17 00:00:00 2001 From: John Helly Date: Wed, 10 Jun 2026 11:13:09 +0100 Subject: [PATCH 4/4] Add soap and snapshot links to the example --- source/snapshots/halo_particles.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/snapshots/halo_particles.rst b/source/snapshots/halo_particles.rst index 55d3a36..40b8c0f 100644 --- a/source/snapshots/halo_particles.rst +++ b/source/snapshots/halo_particles.rst @@ -1,10 +1,10 @@ Reading particles belonging to a halo ===================================== -Here we show how to identify a halo of interest in the SOAP halo -catalogue and then find its particles in the corresponding -snapshot. This could be used to compute a halo density profile, for -example. +Here we show how to identify a halo of interest in a :doc:`SOAP halo +catalogue ` and then find its particles in the +corresponding :doc:`snapshot `. This could be used +to compute a halo density profile, for example. In the SOAP halo catalogues, each halo is assigned an index which is unique within the snapshot (but not between snapshots). This index is