diff --git a/.gitignore b/.gitignore
index 8feacfd8ae84bb9dccfd8a021ea8a5314715fc68..b306465e5ec0f98bed8c763e55ccc56c65f2789d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,12 +39,14 @@ examples/*/*/*.rst
 examples/*/*/*.hdf5
 examples/*/*/*.csv
 examples/*/*/*.dot
-examples/*/*/cell_hierarchy.html
+examples/**/cell_hierarchy.html
 examples/*/*/energy.txt
-examples/*/*/task_level.txt
+examples/**/task_level.txt
 examples/*/*/timesteps_*.txt
-examples/*/*/SFR.txt
-examples/*/*/partition_fixed_costs.h
+examples/**/timesteps.txt
+examples/**/SFR.txt
+examples/**/statistics.txt
+examples/**/partition_fixed_costs.h
 examples/*/*/memuse_report-step*.dat
 examples/*/*/memuse_report-step*.log
 examples/*/*/restart/*
@@ -56,7 +58,6 @@ examples/*/*/used_parameters.yml
 examples/*/*/unused_parameters.yml
 examples/*/*/fof_used_parameters.yml
 examples/*/*/fof_unused_parameters.yml
-examples/*/*/partition_fixed_costs.h
 examples/*/*.mpg
 examples/*/*/gravity_checks_*.dat
 examples/*/*/coolingtables.tar.gz
@@ -65,6 +66,9 @@ examples/*/*/yieldtables.tar.gz
 examples/*/*/yieldtables
 examples/*/*/photometry.tar.gz
 examples/*/*/photometry
+examples/*/*/plots
+examples/*/*/snapshots
+examples/*/*/restart
 examples/Cooling/CoolingRates/cooling_rates
 examples/Cooling/CoolingRates/cooling_element_*.dat
 examples/Cooling/CoolingRates/cooling_output.dat
@@ -73,16 +77,15 @@ examples/SubgridTests/CosmologicalStellarEvolution/StellarEvolutionSolution*
 examples/SmallCosmoVolume/SmallCosmoVolume_DM/power_spectra
 examples/SmallCosmoVolume/SmallCosmoVolume_cooling/snapshots/
 examples/SmallCosmoVolume/SmallCosmoVolume_hydro/snapshots/
-examples/SmallCosmoVolume/SmallCosmoVolume_cooling/CloudyData_UVB=HM2012.h5
-examples/SmallCosmoVolume/SmallCosmoVolume_cooling/CloudyData_UVB=HM2012_shielded.h5
-examples/GEAR/AgoraDisk/CloudyData_UVB=HM2012.h5
-examples/GEAR/AgoraDisk/CloudyData_UVB=HM2012_shielded.h5
-examples/GEAR/AgoraDisk/chemistry-AGB+OMgSFeZnSrYBaEu-16072013.h5
-examples/GEAR/AgoraCosmo/CloudyData_UVB=HM2012_shielded.h5
-examples/GEAR/AgoraCosmo/POPIIsw.h5
-examples/GEAR/ZoomIn/CloudyData_UVB=HM2012.h5
-examples/GEAR/ZoomIn/POPIIsw.h5
-examples/GEAR/ZoomIn/snap/
+examples/**/CloudyData_UVB=HM2012.h5
+examples/**/CloudyData_UVB=HM2012_shielded.h5
+examples/**/CloudyData_UVB=HM2012_high_density.h5
+examples/**/chemistry-AGB+OMgSFeZnSrYBaEu-16072013.h5
+examples/**/POPIIsw.h5
+examples/**/GRACKLE_INFO
+examples/**/snap/
+examples/SinkParticles/HomogeneousBox/snapshot_0003restart.hdf5
+
 
 tests/testActivePair
 tests/testActivePair.sh
diff --git a/AUTHORS b/AUTHORS
index 5520d77633752e7ea863766984e516a67cef5a87..21268f93b663ceaa1bc9326555ac98c30ca3c3fe 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -13,7 +13,7 @@ Josh Borrow             joshua.borrow@durham.ac.uk
 Loic Hausammann		loic.hausammann@epfl.ch
 Yves Revaz   		yves.revaz@epfl.ch
 Jacob Kegerreis         jacob.kegerreis@durham.ac.uk
-Mladen Ivkovic          mladen.ivkovic@epfl.ch
+Mladen Ivkovic          mladen.ivkovic@durham.ac.uk
 Stuart McAlpine       	stuart.mcalpine@helsinki.fi
 Folkert Nobels		nobels@strw.leidenuniv.nl
 John Helly		j.c.helly@durham.ac.uk
@@ -25,4 +25,10 @@ Sylvia Ploeckinger	ploeckinger@lorentz.leidenuniv.nl
 Willem Elbers		willem.h.elbers@durham.ac.uk
 TK Chan			chantsangkeung@gmail.com
 Marcel van Daalen	daalen@strw.leidenuniv.nl
-Filip Husko 		filip.husko@durham.ac.uk
\ No newline at end of file
+Filip Husko 		filip.husko@durham.ac.uk
+Orestis Karapiperis	karapiperis@lorentz.leidenuniv.nl
+Stan Verhoeve           s06verhoeve@gmail.com
+Nikyta Shchutskyi  	shchutskyi@lorentz.leidenuniv.nl
+Will Roper              w.roper@sussex.ac.uk
+Darwin Roduit 		    darwin.roduit@alumni.epfl.ch
+Jonathan Davies         j.j.davies@ljmu.ac.uk
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6db6cfbdaacd8f68cbd505b07a3982e8409fb5f9..dcf0a7d693e07e861be292b32cf1b7debfe7fcf2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,5 +1,5 @@
 The SWIFT source code is using a variation of the 'Google' formatting style. 
-The script 'format.sh' in the root directory applies the clang-format-13
+The script 'format.sh' in the root directory applies the clang-format-18
 tool with our style choices to all the SWIFT C source file. Please apply 
 the formatting script to the files before submitting a merge request.
 
diff --git a/INSTALL.swift b/INSTALL.swift
index 6aba15db9d64d5a3ef0dec744554afdba3ba933a..bafa8b7c236dd8f7a8d0c97b79b444de4e727e32 100644
--- a/INSTALL.swift
+++ b/INSTALL.swift
@@ -99,7 +99,7 @@ before you can build it.
 
 
  - HDF5:
-	A HDF5 library (v. 1.8.x or higher) is required to read and
+	A HDF5 library (v. 1.10.x or higher) is required to read and
         write particle data. One of the commands "h5cc" or "h5pcc"
         should be available. If "h5pcc" is located then a parallel
         HDF5 built for the version of MPI located should be
@@ -191,7 +191,7 @@ before you can build it.
                              ==================
 
 The SWIFT source code uses a variation of 'Google' style. The script
-'format.sh' in the root directory applies the clang-format-13 tool with our
+'format.sh' in the root directory applies the clang-format-18 tool with our
 style choices to all the SWIFT C source file. Please apply the formatting
 script to the files before submitting a merge request.
 
diff --git a/Makefile.am b/Makefile.am
index 3ca9fd5e746dcbfa55d85e3632b3466d9ebc21ab..8746f602d6c46d82d042eaa98a8d7b97d92f3a39 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -37,15 +37,15 @@ MYFLAGS =
 
 # Add the source directory and the non-standard paths to the included library headers to CFLAGS
 AM_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/argparse $(HDF5_CPPFLAGS) \
-	$(GSL_INCS) $(FFTW_INCS) $(NUMA_INCS) $(GRACKLE_INCS) $(OPENMP_CFLAGS) \
-	$(CHEALPIX_CFLAGS)
+	$(GSL_INCS) $(FFTW_INCS) $(NUMA_INCS) $(GRACKLE_INCS) \
+	$(CHEALPIX_CFLAGS) $(LUSTREAPI_CFLAGS)
 
 AM_LDFLAGS = $(HDF5_LDFLAGS)
 
 # Extra libraries.
 EXTRA_LIBS = $(GSL_LIBS) $(HDF5_LIBS) $(FFTW_LIBS) $(NUMA_LIBS) $(PROFILER_LIBS) \
 	$(TCMALLOC_LIBS) $(JEMALLOC_LIBS) $(TBBMALLOC_LIBS) $(GRACKLE_LIBS) \
-	$(CHEALPIX_LIBS)
+	$(CHEALPIX_LIBS) $(LUSTREAPI_LIBS)
 
 # MPI libraries.
 MPI_LIBS = $(PARMETIS_LIBS) $(METIS_LIBS) $(MPI_THREAD_LIBS) $(FFTW_MPI_LIBS)
diff --git a/README b/README
index 7efa8d2639496c7f3cbf0ee7e0d1a6a99a808e78..971e4eb5d30e1fbd4b5674686f25bb2f798b26a4 100644
--- a/README
+++ b/README
@@ -6,7 +6,7 @@
  /____/ |__/|__/___/_/    /_/
  SPH With Inter-dependent Fine-grained Tasking
 
- Version : 1.0.0
+ Version : 2025.01
  Website: www.swiftsim.com
  Twitter: @SwiftSimulation
 
diff --git a/README.md b/README.md
index fa01ed3deba8085efd6ccbdff5131239b8b04fd0..d7be2b1d53bc1dc333a66b6b231aa3f1c7c1f443 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ More general information about SWIFT is available on the project
 [webpages](http://www.swiftsim.com).
 
 For information on how to _run_ SWIFT, please consult the onboarding guide
-available [here](http://www.swiftsim.com/onboarding.pdf). This includes
+available [here](https://swift.strw.leidenuniv.nl/onboarding.pdf). This includes
 dependencies, and a few examples to get you going.
 
 We suggest that you use the latest release branch of SWIFT, rather than the
@@ -55,7 +55,7 @@ experimentation with various values is highly encouraged. Each problem will
 likely require different values and the sensitivity to the details of the
 physical model is something left to the users to explore.
 
-Acknowledgment & Citation
+Acknowledgement & Citation
 -------------------------
 
 The SWIFT code was last described in this paper:
@@ -66,7 +66,7 @@ their results.
 
 In order to keep track of usage and measure the impact of the software, we
 kindly ask users publishing scientific results using SWIFT to add the following
-sentence to the acknowledgment section of their papers:
+sentence to the acknowledgement section of their papers:
 
 "The research in this paper made use of the SWIFT open-source
 simulation code (http://www.swiftsim.com, Schaller et al. 2018)
@@ -81,7 +81,7 @@ Contribution Guidelines
 -----------------------
 
 The SWIFT source code uses a variation of the 'Google' formatting style.
-The script 'format.sh' in the root directory applies the clang-format-10
+The script 'format.sh' in the root directory applies the clang-format-18
 tool with our style choices to all the SWIFT C source file. Please apply
 the formatting script to the files before submitting a pull request.
 
@@ -106,7 +106,7 @@ Runtime parameters
  /____/ |__/|__/___/_/    /_/
  SPH With Inter-dependent Fine-grained Tasking
 
- Version : 1.0.0
+ Version : 2025.01
  Website: www.swiftsim.com
  Twitter: @SwiftSimulation
 
diff --git a/argparse/argparse.h b/argparse/argparse.h
index 186214b4bc90cea90ef141380bf0017cc50af128..a935fdbd615081676e6478f4a4b7c9324649c819 100644
--- a/argparse/argparse.h
+++ b/argparse/argparse.h
@@ -105,20 +105,13 @@ int argparse_help_cb(struct argparse *self,
                      const struct argparse_option *option);
 
 // built-in option macros
-#define OPT_END() \
-  { ARGPARSE_OPT_END, 0, NULL, NULL, 0, NULL, 0, 0 }
-#define OPT_BOOLEAN(...) \
-  { ARGPARSE_OPT_BOOLEAN, __VA_ARGS__ }
-#define OPT_BIT(...) \
-  { ARGPARSE_OPT_BIT, __VA_ARGS__ }
-#define OPT_INTEGER(...) \
-  { ARGPARSE_OPT_INTEGER, __VA_ARGS__ }
-#define OPT_FLOAT(...) \
-  { ARGPARSE_OPT_FLOAT, __VA_ARGS__ }
-#define OPT_STRING(...) \
-  { ARGPARSE_OPT_STRING, __VA_ARGS__ }
-#define OPT_GROUP(h) \
-  { ARGPARSE_OPT_GROUP, 0, NULL, NULL, h, NULL, 0, 0 }
+#define OPT_END() {ARGPARSE_OPT_END, 0, NULL, NULL, 0, NULL, 0, 0}
+#define OPT_BOOLEAN(...) {ARGPARSE_OPT_BOOLEAN, __VA_ARGS__}
+#define OPT_BIT(...) {ARGPARSE_OPT_BIT, __VA_ARGS__}
+#define OPT_INTEGER(...) {ARGPARSE_OPT_INTEGER, __VA_ARGS__}
+#define OPT_FLOAT(...) {ARGPARSE_OPT_FLOAT, __VA_ARGS__}
+#define OPT_STRING(...) {ARGPARSE_OPT_STRING, __VA_ARGS__}
+#define OPT_GROUP(h) {ARGPARSE_OPT_GROUP, 0, NULL, NULL, h, NULL, 0, 0}
 #define OPT_HELP()                                                  \
   OPT_BOOLEAN('h', "help", NULL, "show this help message and exit", \
               argparse_help_cb, 0, 0)
diff --git a/configure.ac b/configure.ac
index bc63e5143b691f355efd0b7cf074da8d40432e20..4e920c2b8d6ef08091942f22f31a7ca25aff0185 100644
--- a/configure.ac
+++ b/configure.ac
@@ -16,7 +16,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # Init the project.
-AC_INIT([SWIFT],[1.0.0],[https://gitlab.cosma.dur.ac.uk/swift/swiftsim])
+AC_INIT([SWIFT],[2025.01],[https://gitlab.cosma.dur.ac.uk/swift/swiftsim])
 swift_config_flags="$*"
 
 AC_COPYRIGHT
@@ -40,6 +40,8 @@ AX_CHECK_ENABLE_DEBUG
 AC_USE_SYSTEM_EXTENSIONS
 AC_PROG_CC
 AM_PROG_CC_C_O
+
+# We need this for compilation hints and possibly FFTW.
 AX_OPENMP
 
 # If debug is selected then we also define SWIFT_DEVELOP_MODE to control
@@ -90,43 +92,67 @@ if test "$with_csds" = "yes"; then
    fi
 
    AC_CONFIG_SUBDIRS([csds])
+   CFLAGS="$CFLAGS $OPENMP_CFLAGS"
 
 fi
 AM_CONDITIONAL([HAVECSDS],[test $with_csds = "yes"])
 
 
+# Use best known optimization for the current architecture. Actual optimization
+# happens later so we can avoid any issues it introduces with the compiler,
+# but we need to know if that will happen now.
+AC_ARG_ENABLE([optimization],
+   [AS_HELP_STRING([--enable-optimization],
+     [Enable compile time optimization flags for host @<:@yes/no@:>@]
+   )],
+   [enable_opt="$enableval"],
+   [enable_opt="yes"]
+)
 
-# Interprocedural optimization support. Needs special handling for linking and
-# archiving as well as compilation with Intels, needs to be done before
-# libtool is configured (to use correct LD).
+# Interprocedural optimization support. Can need special handling for linking
+# and archiving as well as compilation. Needs to be done before libtool is
+# configured so we use the correct LD. It can give good improvements for
+# clang based compilers, so the default is enabled, but we make that
+# disabled when not optimizing or debugging support is enabled.
+enable_ipo_default="yes";
+if test "x$ax_enable_debug" = "xyes" -o "x$enable_opt" != "xyes"; then
+   enable_ipo_default="no";
+   AC_MSG_WARN([Interprocedural optimization support default is changed to false])
+fi
 AC_ARG_ENABLE([ipo],
    [AS_HELP_STRING([--enable-ipo],
-     [Enable interprocedural optimization @<:@no/yes@:>@]
+     [Enable interprocedural optimization [default=yes unless debugging]]
    )],
    [enable_ipo="$enableval"],
-   [enable_ipo="no"]
+   [enable_ipo="$enable_ipo_default"]
 )
 
 if test "$enable_ipo" = "yes"; then
    if test "$ax_cv_c_compiler_vendor" = "intel"; then
       CFLAGS="$CFLAGS -ip -ipo"
       LDFLAGS="$LDFLAGS -ipo"
-      : ${AR="xiar"}
-      : ${LD="xild"}
+      AC_CHECK_PROGS([AR], [xiar])
+      AC_CHECK_PROGS([LD], [xild])
       AC_MSG_RESULT([added Intel interprocedural optimization support])
+   elif test "$ax_cv_c_compiler_vendor" = "oneapi"; then
+      CFLAGS="$CFLAGS -ipo"
+      LDFLAGS="$LDFLAGS -ipo"
+      AC_CHECK_PROGS([AR], [xiar])
+      AC_CHECK_PROGS([RANLIB], [llvm-ranlib])
+      AC_MSG_RESULT([added oneapi interprocedural optimization support])
    elif test "$ax_cv_c_compiler_vendor" = "gnu"; then
       CFLAGS="$CFLAGS -flto"
       LDFLAGS="$LDFLAGS -flto"
       AX_COMPARE_VERSION($ax_cv_c_compiler_version, [ge], [5.0.0],
-                          [
-      : ${AR="gcc-ar"}
-      : ${RANLIB="gcc-ranlib"}
-                          ], [:] )
+         [
+         AC_CHECK_PROGS([AR], [gcc-ar])
+         AC_CHECK_PROGS([RANLIB], [gcc-ranlib])
+         ], [:] )
       AC_MSG_RESULT([added GCC interprocedural optimization support])
    elif test "$ax_cv_c_compiler_vendor" = "clang"; then
-      CFLAGS="$CFLAGS -flto=thin"
-      LDFLAGS="$LDFLAGS -flto=thin"
-      : ${RANLIB="llvm-ranlib"}
+      CFLAGS="$CFLAGS -flto"
+      LDFLAGS="$LDFLAGS -flto"
+      AC_CHECK_PROGS([RANLIB], [llvm-ranlib])
       AC_MSG_RESULT([added LLVM interprocedural optimization support])
    else
       AC_MSG_WARN([Compiler does not support interprocedural optimization])
@@ -146,7 +172,7 @@ AC_ARG_ENABLE([mpi],
     [enable_mpi="$enableval"],
     [enable_mpi="yes"]
 )
-# Use extra flags set by AC_PROG_CC as part of $CC. Currently undocumented as ac_cv_prog_cc_stdc, 
+# Use extra flags set by AC_PROG_CC as part of $CC. Currently undocumented as ac_cv_prog_cc_stdc,
 # so could change.
 good_mpi="yes"
 if test "$enable_mpi" = "yes"; then
@@ -223,7 +249,7 @@ AC_C_INLINE
 
 
 # If debugging try to show inlined functions.
-if test "x$enable_debug" = "xyes"; then
+if test "x$ax_enable_debug" = "xyes"; then
    #  Show inlined functions.
    if test "$ax_cv_c_compiler_vendor" = "gnu"; then
       # Would like to use -gdwarf and let the compiler pick a good version
@@ -234,6 +260,8 @@ if test "x$enable_debug" = "xyes"; then
       CFLAGS="$CFLAGS $inline_EXTRA_FLAGS"
    elif test "$ax_cv_c_compiler_vendor" = "intel"; then
       CFLAGS="$CFLAGS -debug inline-debug-info"
+   elif test "$ax_cv_c_compiler_vendor" = "oneapi"; then
+      CFLAGS="$CFLAGS -debug inline-debug-info"
    fi
 fi
 
@@ -315,7 +343,7 @@ if test "$enable_cell_graph" = "yes"; then
    AC_DEFINE([SWIFT_CELL_GRAPH],1,[Enable cell graph])
 fi
 
-# Check if using our custom icbrtf is enalbled.
+# Check if using our custom icbrtf is enabled.
 AC_ARG_ENABLE([custom-icbrtf],
    [AS_HELP_STRING([--enable-custom-icbrtf],
      [Use SWIFT's custom icbrtf function instead of the system cbrtf @<:@yes/no@:>@]
@@ -405,6 +433,20 @@ elif test "$stars_density_checks" != "no"; then
    AC_DEFINE_UNQUOTED([SWIFT_STARS_DENSITY_CHECKS], [$enableval] ,[Enable stars density brute-force checks])
 fi
 
+# Check if sink density checks are on for some particles.
+AC_ARG_ENABLE([sink-density-checks],
+   [AS_HELP_STRING([--enable-sink-density-checks],
+     [Activate expensive brute-force sink density checks for a fraction 1/N of all particles @<:@N@:>@]
+   )],
+   [sink_density_checks="$enableval"],
+   [sink_density_checks="no"]
+)
+if test "$sink_density_checks" = "yes"; then
+   AC_MSG_ERROR(Need to specify the fraction of particles to check when using --enable-sink-density-checks!)
+elif test "$sink_density_checks" != "no"; then
+   AC_DEFINE_UNQUOTED([SWIFT_SINK_DENSITY_CHECKS], [$enableval] ,[Enable sink density brute-force checks])
+fi
+
 # Check if ghost statistics are enabled
 AC_ARG_ENABLE([ghost-statistics],
    [AS_HELP_STRING([--enable-ghost-statistics],
@@ -538,16 +580,6 @@ fi
 # Define HAVE_POSIX_MEMALIGN if it works.
 AX_FUNC_POSIX_MEMALIGN
 
-# Only optimize if allowed, otherwise assume user will set CFLAGS as
-# appropriate.
-AC_ARG_ENABLE([optimization],
-   [AS_HELP_STRING([--enable-optimization],
-     [Enable compile time optimization flags for host @<:@yes/no@:>@]
-   )],
-   [enable_opt="$enableval"],
-   [enable_opt="yes"]
-)
-
 #  Disable vectorisation for known compilers. This switches off optimizations
 #  that could be enabled above, so in general should be appended. Slightly odd
 #  implementation as want to describe as --disable-vec, but macro is enable
@@ -572,6 +604,9 @@ AC_ARG_ENABLE([hand-vec],
 
 HAVEVECTORIZATION=0
 
+# Only optimize if allowed, otherwise assume user will set CFLAGS as
+# appropriate. Note argument check is done earlier so we can configure
+# other options related to optimization.
 if test "$enable_opt" = "yes" ; then
 
    # Choose the best flags for this compiler and architecture
@@ -579,29 +614,55 @@ if test "$enable_opt" = "yes" ; then
    AX_CC_MAXOPT
    ac_test_CFLAGS="yes"
 
-   # Choose the best flags for the gravity sub-library on this compiler and architecture
+   # Choose the best flags for the gravity sub-library on this compiler and
+   # architecture. Note we use OpenMP as a compiler hints for loop vectorization.
+   GRAVITY_CFLAGS="$GRAVITY_CFLAGS $OPENMP_CFLAGS"
    if test "$ax_cv_c_compiler_vendor" = "intel"; then
       case "$icc_flags" in
       	 *CORE-AVX512*)
             GRAVITY_CFLAGS="$GRAVITY_CFLAGS -qopt-zmm-usage=high"
 	    ;;
 	 *)
-	    AC_MSG_WARN([No additional flags needed for gravity on this platform])
+	    AC_MSG_NOTICE([No additional flags needed for gravity on this platform])
 	    ;;
       esac
+   elif test "$ax_cv_c_compiler_vendor" = "oneapi"; then
+      case "$icc_flags" in
+         *CORE-AVX512*)
+            GRAVITY_CFLAGS="$GRAVITY_CFLAGS -qopt-zmm-usage=high"
+            ;;
+         *)
+            AC_MSG_NOTICE([No additional flags needed for gravity on this platform])
+            ;;
+      esac
    elif test "$ax_cv_c_compiler_vendor" = "gnu"; then
       if test "$gcc_handles_avx512" = "yes"; then
-         case "$ax_gcc_arch" in
+         case "$ax_cv_gcc_archflag" in
 	    *skylake-avx512*)
                GRAVITY_CFLAGS="$GRAVITY_CFLAGS -mprefer-vector-width=512"
 	       ;;
 	    *)
-	       AC_MSG_WARN([No additional flags needed for gravity on this platform])
+	       AC_MSG_NOTICE([No additional flags needed for gravity on this platform])
 	       ;;
          esac
       else
-         AC_MSG_WARN([No additional flags needed for gravity on this platform])
+         AC_MSG_NOTICE([No additional flags needed for gravity on this platform])
       fi
+   elif test "$ax_cv_c_compiler_vendor" = "clang"; then
+      #  Could be a number of compilers. Check for aocc specific flags we want
+      #  to use.
+      AX_CHECK_COMPILE_FLAG("-zopt", [GRAVITY_CFLAGS="$GRAVITY_CFLAGS -fvectorize -zopt"])
+      case "$ax_cv_gcc_archflag" in
+	    *skylake-avx512*)
+               GRAVITY_CFLAGS="$GRAVITY_CFLAGS -mprefer-vector-width=512"
+	       ;;
+	    *znver[[4-9]])
+               GRAVITY_CFLAGS="$GRAVITY_CFLAGS -mprefer-vector-width=512"
+               ;;
+            *)
+               :
+            ;;
+       esac
    else
       AC_MSG_WARN([Do not know what best gravity vectorization flags to choose for this compiler])
    fi
@@ -622,6 +683,9 @@ if test "$enable_opt" = "yes" ; then
       if test "$ax_cv_c_compiler_vendor" = "intel"; then
       	 CFLAGS="$CFLAGS -no-vec -no-simd"
       	 AC_MSG_RESULT([disabled Intel vectorization])
+      elif test "$ax_cv_c_compiler_vendor" = "oneapi"; then
+         CFLAGS="$CFLAGS -no-vec"
+         AC_MSG_RESULT([disabled oneAPI vectorization])
       elif test "$ax_cv_c_compiler_vendor" = "gnu"; then
       	 CFLAGS="$CFLAGS -fno-tree-vectorize"
       	 AC_MSG_RESULT([disabled GCC vectorization])
@@ -638,7 +702,6 @@ if test "$enable_opt" = "yes" ; then
 fi
 AM_CONDITIONAL([HAVEVECTORIZATION],[test -n "$HAVEVECTORIZATION"])
 
-
 # Add address sanitizer options to flags, if requested. Only useful for GCC
 # version 4.8 and later and clang.
 AC_ARG_ENABLE([sanitizer],
@@ -659,7 +722,20 @@ if test "$enable_san" = "yes"; then
    fi
    if test "$enable_san" = "yes"; then
       CFLAGS="$CFLAGS -fsanitize=address -fno-omit-frame-pointer"
-      AC_MSG_RESULT([added address sanitizer support])
+      AC_MSG_RESULT([Adding address sanitizer support... yes])
+
+        # Check if we have access to the __lsan_ignore_object() call for
+        # marking memory allocations as deliberately leaked.
+        AC_LINK_IFELSE([AC_LANG_SOURCE([[
+                           #include <stdlib.h>
+                           #include <sanitizer/lsan_interface.h>
+                           int main(int argc, char *argv[]) {
+                              void *p = malloc(1);
+                              __lsan_ignore_object(p);
+                              return 0;
+                           }]])],
+           [AC_DEFINE(HAVE_LSAN_IGNORE_OBJECT, 1, [Have __lsan_ignore_object() call])],
+           [AC_MSG_WARN([Sanitizer enabled but no __lsan_ignore_object()])])
    else
       AC_MSG_WARN([Compiler does not support address sanitizer option])
    fi
@@ -756,6 +832,42 @@ AC_SUBST([GSL_LIBS])
 AC_SUBST([GSL_INCS])
 AM_CONDITIONAL([HAVEGSL],[test -n "$GSL_LIBS"])
 
+# Check for GMP. We test for this in the standard directories by default,
+# and only disable if using --with-gmp=no or --without-gmp. When a value
+# is given GMP must be found.
+have_gmp="no"
+AC_ARG_WITH([gmp],
+    [AS_HELP_STRING([--with-gmp=PATH],
+       [root directory where GMP is installed @<:@yes/no@:>@]
+    )],
+    [with_gmp="$withval"],
+    [with_gmp="test"]
+)
+if test "x$with_gmp" != "xno"; then
+   if test "x$with_gmp" != "xyes" -a "x$with_gmp" != "xtest" -a "x$with_gmp" != "x"; then
+      GMP_LIBS="-L$with_gmp/lib -lgmp"
+   else
+      GMP_LIBS="-lgmp"
+   fi
+   #  GMP is not specified, so just check if we have it.
+   if test "x$with_gmp" = "xtest"; then
+      AC_CHECK_LIB([gmp],[__gmpz_inits],[have_gmp="yes"],[have_gmp="no"],$GMP_LIBS)
+      if test "x$have_gmp" != "xno"; then
+         AC_DEFINE([HAVE_LIBGMP],1,[The GMP library appears to be present.])
+      fi
+   else
+      AC_CHECK_LIB([gmp],[__gmpz_inits],
+         AC_DEFINE([HAVE_LIBGMP],1,[The GMP library appears to be present.]),
+         AC_MSG_ERROR(something is wrong with the GMP library!), $GMP_LIBS)
+      have_gmp="yes"
+   fi
+   if test "$have_gmp" = "no"; then
+      GMP_LIBS=""
+   fi
+fi
+AC_SUBST([GMP_LIBS])
+AM_CONDITIONAL([HAVEGMP],[test -n "$GMP_LIBS"])
+
 # Check for pthreads.
 AX_PTHREAD([LIBS="$PTHREAD_LIBS $LIBS" CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
     CC="$PTHREAD_CC" LDFLAGS="$LDFLAGS $PTHREAD_LIBS $LIBS"],
@@ -801,6 +913,21 @@ if test "x$with_metis" != "xno"; then
    fi
    AC_CHECK_LIB([metis],[METIS_PartGraphKway], [have_metis="yes"],
                 [have_metis="no"], $METIS_LIBS)
+
+   #  Recent METIS releases have an external GKlib, test for that before
+   # giving up. Assume sane and in the same directories.
+   if test "x$have_metis" = "xno"; then
+      if test "x$with_metis" != "xyes" -a "x$with_metis" != "x"; then
+         METIS_LIBS="-L$with_metis/lib -lmetis -lGKlib"
+         METIS_INCS="-I$with_metis/include"
+      else
+         METIS_LIBS="-lmetis -lGKlib"
+         METIS_INCS=""
+      fi
+      AC_CHECK_LIB([metis],[METIS_PartGraphRecursive], [have_metis="yes"],
+                   [have_metis="no"], $METIS_LIBS)
+   fi
+
    if test "$have_metis" = "yes"; then
       AC_DEFINE([HAVE_METIS],1,[The METIS library is present.])
    else
@@ -834,10 +961,9 @@ if test "x$with_parmetis" != "xno"; then
    fi
    AC_CHECK_LIB([parmetis],[ParMETIS_V3_RefineKway], [have_parmetis="yes"],
                 [have_parmetis="no"], $PARMETIS_LIBS)
-   if test "$have_parmetis" = "no"; then
 
 # A build may use an external METIS library, check for that.
-
+   if test "$have_parmetis" = "no"; then
       if test "x$with_parmetis" != "xyes" -a "x$with_parmetis" != "x"; then
          PARMETIS_LIBS="-L$with_parmetis/lib -lparmetis -lmetis"
          PARMETIS_INCS="-I$with_parmetis/include"
@@ -848,6 +974,20 @@ if test "x$with_parmetis" != "xno"; then
       # Note use different function to avoid caching of first check.
       AC_CHECK_LIB([parmetis],[ParMETIS_V3_PartKway], [have_parmetis="yes"],
                    [have_parmetis="no"], [$METIS_LIBS $PARMETIS_LIBS])
+   fi
+
+# A build may use an external GKlib in later releases...
+   if test "$have_parmetis" = "no"; then
+      if test "x$with_parmetis" != "xyes" -a "x$with_parmetis" != "x"; then
+         PARMETIS_LIBS="-L$with_parmetis/lib -lparmetis -lGKlib"
+         PARMETIS_INCS="-I$with_parmetis/include"
+      else
+         PARMETIS_LIBS="-lparmetis -lGKlib"
+         PARMETIS_INCS=""
+      fi
+      # Note use different function to avoid caching of first check.
+      AC_CHECK_LIB([parmetis],[ParMETIS_V3_PartGeom], [have_parmetis="yes"],
+                   [have_parmetis="no"], [$METIS_LIBS $PARMETIS_LIBS])
 
    fi
    if test "$have_parmetis" = "yes"; then
@@ -948,17 +1088,22 @@ if test "x$with_fftw" != "xno"; then
             FFTW_OPENMP_INCS=""
          fi
 
-         # Verify that the library works. Note requires AC_OPENMP called above.
+         # Verify that the library works. Note requires AX_OPENMP called above.
+         old_CFLAGS=$CFLAGS
+         CFLAGS="$CFLAGS $OPENMP_CFLAGS"
          AC_CHECK_LIB([fftw3],[fftw_init_threads],[have_openmp_fftw="yes"],
 		      [have_openmp_fftw="no"], $FFTW_OPENMP_LIBS)
 
          # If found, update things
          if test "x$have_openmp_fftw" = "xyes"; then
-            #  Note OpenMP and pthreads use mostly the same calls, so define both.
+            # Note OpenMP and pthreads use mostly the same calls, so define both.
             AC_DEFINE([HAVE_THREADED_FFTW],1,[The threaded OpenMP FFTW library appears to be present.])
             AC_DEFINE([HAVE_OPENMP_FFTW],1,[The OpenMP FFTW library appears to be present.])
             FFTW_LIBS=$FFTW_OPENMP_LIBS
             FFTW_INCS=$FFTW_OPENMP_INCS
+         else
+            # Put CFLAGS back.
+            CFLAGS=$old_CFLAGS
          fi
       fi
 
@@ -1136,7 +1281,7 @@ if test "x$with_tcmalloc" != "xno"; then
 
       # Prevent compilers that replace the calls with built-ins (GNU 99) from doing so.
       case "$ax_cv_c_compiler_vendor" in
-        intel | gnu | clang)
+        intel | gnu | clang | oneapi)
              CFLAGS="$CFLAGS -fno-builtin-malloc -fno-builtin-calloc -fno-builtin-realloc -fno-builtin-free"
           ;;
       esac
@@ -1179,7 +1324,7 @@ if test "x$with_jemalloc" != "xno"; then
 
       # Prevent compilers that replace the regular calls with built-ins (GNU 99) from doing so.
       case "$ax_cv_c_compiler_vendor" in
-        intel | gnu | clang)
+        intel | gnu | clang | oneapi)
              CFLAGS="$CFLAGS -fno-builtin-malloc -fno-builtin-calloc -fno-builtin-realloc -fno-builtin-free"
           ;;
       esac
@@ -1222,7 +1367,7 @@ if test "x$with_tbbmalloc" != "xno"; then
 
       # Prevent compilers that replace the calls with built-ins (GNU 99) from doing so.
       case "$ax_cv_c_compiler_vendor" in
-        intel | gnu | clang)
+        intel | gnu | clang | oneapi)
              CFLAGS="$CFLAGS -fno-builtin-malloc -fno-builtin-calloc -fno-builtin-realloc -fno-builtin-free"
           ;;
       esac
@@ -1286,6 +1431,38 @@ if test "$with_hdf5" = "yes"; then
 fi
 AM_CONDITIONAL([HAVEPARALLELHDF5],[test "$have_parallel_hdf5" = "yes"])
 
+# Check for lustre support. Attempted by default.
+have_lustreapi="no"
+AC_ARG_WITH([lustreapi],
+   [AS_HELP_STRING([--with-lustreapi=PATH],
+      [use the lustre api library for some striping control @<:@yes/no@:>@]
+   )],
+   [with_lustreapi="$withval"],
+   [with_lustreapi="yes"]
+)
+
+if test "x$with_lustreapi" != "xno"; then
+   if test "x$with_lustreapi" != "xyes" -a "x$with_lustreapi" != "x"; then
+      LUSTREAPI_LIBS="-L$with_lustreapi -llustreapi"
+      LUSTREAPI_INCS="-I$with_lustreapi/include"
+   else
+      LUSTREAPI_LIBS="-llustreapi"
+      LUSTREAPI_INCS=""
+   fi
+   AC_CHECK_LIB([lustreapi], [llapi_obd_statfs], [have_lustreapi="yes"],
+                [have_lustreapi="no"],[$LUSTREAPI_LIBS])
+
+   if test "$have_lustreapi" = "yes"; then
+      AC_DEFINE([HAVE_LUSTREAPI],1,[The lustre API library appears to be present.])
+   else
+      LUSTREAPI_LIBS=""
+   fi
+fi
+AC_SUBST([LUSTREAPI_LIBS])
+AC_SUBST([LUSTREAPI_INCS])
+AM_CONDITIONAL([HAVELUSTREAPI],[test -n "$LUSTREAPI_LIBS"])
+
+
 # Check for grackle.
 have_grackle="no"
 AC_ARG_WITH([grackle],
@@ -1351,7 +1528,7 @@ AC_ARG_WITH([velociraptor],
 if test "x$with_velociraptor" != "xno"; then
    if test "x$with_velociraptor" != "xyes" -a "x$with_velociraptor" != "x"; then
       VELOCIRAPTOR_LIBS="-L$with_velociraptor -lvelociraptor -lstdc++ -lhdf5"
-      CFLAGS="$CFLAGS -fopenmp"
+      CFLAGS="$CFLAGS $OPENMP_CFLAGS"
    else
       VELOCIRAPTOR_LIBS=""
    fi
@@ -1487,14 +1664,30 @@ if test "$enable_lightcone" = "yes"; then
    # Also need to make sure we have GSL if we're making lightcones
    if test "$have_gsl" != "yes"; then
       AC_MSG_ERROR([Lightcone output requires GSL. Please configure with --with-gsl.])
-   fi   
+   fi
 else
    have_chealpix="no"
 fi
 
-# Check for floating-point execeptions
-AC_CHECK_FUNC(feenableexcept, AC_DEFINE([HAVE_FE_ENABLE_EXCEPT],[1],
-    [Defined if the floating-point exception can be enabled using non-standard GNU functions.]))
+# Check for floating-point exception trapping support.
+#
+# We do not allow this to be enabled when optimizing as compilers do operations
+# which are unsafe for speed. This can result in FPEs on valid vector
+# operations when additional padding is used. This has been seen on clang and
+# GCC based compilers.
+if test "$enable_opt" != "yes"; then
+   if test "$ax_cv_c_compiler_vendor" != "oneapi"; then
+      AC_CHECK_FUNC(feenableexcept, AC_DEFINE([HAVE_FE_ENABLE_EXCEPT],[1],
+         [Defined if floating-point exceptions can be trapped.]))
+   else
+      #  Default optimization for Intel is too high , -O2, so we also need
+      #  to have debugging enabled which uses -O0 as well.
+      if test "$ax_enable_debug" != "no"; then
+         AC_CHECK_FUNC(feenableexcept, AC_DEFINE([HAVE_FE_ENABLE_EXCEPT],[1],
+            [Defined if floating-point exceptions can be trapped.]))
+      fi
+   fi
+fi
 
 # Check for setaffinity.
 AC_CHECK_FUNC(pthread_setaffinity_np, AC_DEFINE([HAVE_SETAFFINITY],[1],
@@ -1555,10 +1748,10 @@ AC_SUBST([NUMA_LIBS])
 
 
 # Check for Sundials (required for the SPHM1RT library).
-# There is a problems with the headers of this library 
-# as they do not pass the strict prototypes check when 
-# installed outside of the system directories. So we 
-# need to do this check in two phases. 
+# There is a problems with the headers of this library
+# as they do not pass the strict prototypes check when
+# installed outside of the system directories. So we
+# need to do this check in two phases.
 have_sundials="no"
 SUNDIALS_LIBS=""
 SUNDIALS_INCS=""
@@ -1587,22 +1780,22 @@ if test "x$with_sundials" != "xno"; then
       AC_DEFINE([HAVE_SUNDIALS],1,[The SUNDIALS library is present.])
    else
       if test "x$with_sundials" != "xyes" -a "x$with_sundials" != "x"; then
-      	 # It might be that the libraries are in 
-      	 # /lib64 rather than /lib 
+      	 # It might be that the libraries are in
+      	 # /lib64 rather than /lib
       	 SUNDIALS_LIBS="-L$with_sundials/lib64 -lsundials_cvode -lsundials_nvecserial -lsundials_sunlinsoldense -lsundials_sunmatrixdense"
 
 	 # unset cached result of previous AC_CHECK_LIB
 	 unset ac_cv_lib_sundials_cvode_CVode
 
-   	 AC_CHECK_LIB([sundials_cvode], [CVode], [have_sundials="yes"], [have_sundials="no"], $SUNDIALS_LIBS)      
-	 
+   	 AC_CHECK_LIB([sundials_cvode], [CVode], [have_sundials="yes"], [have_sundials="no"], $SUNDIALS_LIBS)
+
    	 if test "$have_sundials" == "yes"; then
       	    AC_DEFINE([HAVE_SUNDIALS],1,[The SUNDIALS library is present.])
-	 else 
+	 else
 	    AC_MSG_ERROR("Failed to find a SUNDIALS library")
-	 fi 
+	 fi
 
-      else 
+      else
       	 AC_MSG_ERROR("Failed to find a SUNDIALS library")
       fi
    fi
@@ -1669,25 +1862,23 @@ AC_MSG_RESULT($rtc_ok)
 # Special timers for the ARM v7 platforms (taken from FFTW-3 to match their cycle.h)
 AC_ARG_ENABLE(armv7a-cntvct, [AS_HELP_STRING([--enable-armv7a-cntvct],[enable the cycle counter on Armv7a via the CNTVCT register])], have_armv7acntvct=$enableval)
 if test "$have_armv7acntvct"x = "yes"x; then
-	AC_DEFINE(HAVE_ARMV7A_CNTVCT,1,[Define if you have enabled the CNTVCT cycle counter on ARMv7a])
+      AC_DEFINE(HAVE_ARMV7A_CNTVCT,1,[Define if you have enabled the CNTVCT cycle counter on ARMv7a])
 fi
 
 AC_ARG_ENABLE(armv7a-pmccntr, [AS_HELP_STRING([--enable-armv7a-pmccntr],[enable the cycle counter on Armv7a via the PMCCNTR register])], have_armv7apmccntr=$enableval)
 if test "$have_armv7apmccntr"x = "yes"x; then
-	AC_DEFINE(HAVE_ARMV7A_PMCCNTR,1,[Define if you have enabled the PMCCNTR cycle counter on ARMv7a])
+      AC_DEFINE(HAVE_ARMV7A_PMCCNTR,1,[Define if you have enabled the PMCCNTR cycle counter on ARMv7a])
 fi
 
-# Check if we have native exp10 and exp10f functions. If not failback to our
-# implementations. On Apple/CLANG we have __exp10, so also check for that
+# Check if we have native exp10 and exp10f functions. If not fallback to our
+# implementations. On Apple/CLANG/Homebrew gcc we have __exp10, so also check for that
 # if the compiler is clang.
 AC_CHECK_LIB([m],[exp10], [AC_DEFINE([HAVE_EXP10],1,[The exp10 function is present.])])
 AC_CHECK_LIB([m],[exp10f], [AC_DEFINE([HAVE_EXP10F],1,[The exp10f function is present.])])
-if test "$ax_cv_c_compiler_vendor" = "clang"; then
-      AC_CHECK_LIB([m],[__exp10], [AC_DEFINE([HAVE___EXP10],1,[The __exp10 function is present.])])
-      AC_CHECK_LIB([m],[__exp10f], [AC_DEFINE([HAVE___EXP10F],1,[The __exp10f function is present.])])
-fi
+AC_CHECK_LIB([m],[__exp10], [AC_DEFINE([HAVE___EXP10],1,[The __exp10 function is present.])])
+AC_CHECK_LIB([m],[__exp10f], [AC_DEFINE([HAVE___EXP10F],1,[The __exp10f function is present.])])
 
-# Check if we have native sincos and sincosf functions. If not failback to our
+# Check if we have native sincos and sincosf functions. If not fallback to our
 # implementations. On Apple/CLANG we have __sincos, so also check for that
 # if the compiler is clang.
 AC_CHECK_LIB([m],[sincos], [AC_DEFINE([HAVE_SINCOS],1,[The sincos function is present.])])
@@ -1697,6 +1888,27 @@ if test "$ax_cv_c_compiler_vendor" = "clang"; then
       AC_CHECK_LIB([m],[__sincosf], [AC_DEFINE([HAVE___SINCOSF],1,[The __sincosf function is present.])])
 fi
 
+# On Apple (CLANG or Homebrew gcc), check for __sincos and __sincosf inline functions in the headers.
+AC_CHECK_HEADERS([math.h], [
+  AC_CHECK_DECLS([__sincos, __sincosf], [
+    AC_DEFINE([HAVE___SINCOS], 1, [The __sincos function is present.])
+    AC_DEFINE([HAVE___SINCOSF], 1, [The __sincosf function is present.])
+  ], [], [#include <math.h>])
+])
+
+# The aocc compiler has optimized maths libraries that we should use. Check
+# any clang for this support. Note do this after the basic check for maths
+# as we need to make sure -lm follows. Also note needs -Ofast or -ffast-math
+# so only when optimizing.
+if test "$enable_opt" = "yes" -a "$ax_cv_c_compiler_vendor" = "clang"; then
+   have_almfast="yes"
+   AC_CHECK_LIB([almfast],[amd_fastexp],[LIBS="-fveclib=AMDLIBM -fsclrlib=AMDLIBM -lalmfast -lamdlibm $LIBS"],[have_almfast="no"],[-lamdlibm -lm])
+   if test "$have_almfast" = "no"; then
+      # Less optimized version.
+      AC_CHECK_LIB([amdlibm],[sqrt],,,[-lm])
+   fi
+fi
+
 # Check for glibc extension backtrace().
 AC_CHECK_FUNCS([backtrace backtrace_symbols])
 
@@ -1715,12 +1927,13 @@ if test "$enable_warn" != "no"; then
     # AX_CFLAGS_WARN_ALL does not give good warning flags for the Intel compiler
     # We will do this by hand instead and only default to the macro for unknown compilers
     case "$ax_cv_c_compiler_vendor" in
-          gnu | clang)
+          gnu | clang | oneapi)
              CFLAGS="$CFLAGS -Wall -Wextra -Wno-unused-parameter -Wshadow"
           ;;
 	  intel)
              CFLAGS="$CFLAGS -w2 -Wunused-variable -Wshadow"
           ;;
+
 	  *)
 	     AX_CFLAGS_WARN_ALL
 	  ;;
@@ -1729,8 +1942,12 @@ if test "$enable_warn" != "no"; then
     # Add a "choke on warning" flag if it exists
     if test "$enable_warn" = "error"; then
        case "$ax_cv_c_compiler_vendor" in
-          intel | gnu | clang)
+          intel | clang | oneapi)
              CFLAGS="$CFLAGS -Werror"
+	  ;;
+	  gnu)
+             #  Fix for issue with IPO and GCC 14
+             CFLAGS="$CFLAGS -Werror -Wno-alloc-size-larger-than"
           ;;
        esac
     fi
@@ -1771,9 +1988,9 @@ fi
 AC_SUBST([NUMA_INCS])
 
 
-# Second part of the Sundials library checks. 
-# We now decide if we need to use -isystem to 
-# get around the strict-prototypes problem. Assumes 
+# Second part of the Sundials library checks.
+# We now decide if we need to use -isystem to
+# get around the strict-prototypes problem. Assumes
 # isystem is available when strict-prototypes is.
 if test "x$with_sundials" != "xno"; then
    if test "x$with_sundials" != "xyes" -a "x$with_sundials" != "x"; then
@@ -1786,7 +2003,7 @@ if test "x$with_sundials" != "xno"; then
             ;;
         esac
    fi
-fi 
+fi
 AC_SUBST([SUNDIALS_INCS])
 
 
@@ -1798,7 +2015,8 @@ AC_SUBST([SUNDIALS_INCS])
 # As an example for this, see the call to AC_ARG_WITH for cooling.
 AC_ARG_WITH([subgrid],
 	[AS_HELP_STRING([--with-subgrid=<subgrid>],
-		[Master switch for subgrid methods. Inexperienced user should start here. Options are: @<:@none, GEAR, AGORA, QLA, QLA-EAGLE, EAGLE, EAGLE-XL, SPIN_JET_EAGLE default: none@:>@]
+		[Master switch for subgrid methods. Inexperienced user should
+		start here. Options are: @<:@none, GEAR, GEAR-G3, AGORA, QLA, QLA-EAGLE, EAGLE, EAGLE-XL, SPIN_JET_EAGLE default: none@:>@]
 	)],
 	[with_subgrid="$withval"],
 	[with_subgrid=none]
@@ -1825,12 +2043,24 @@ case "$with_subgrid" in
    GEAR)
 	with_subgrid_cooling=grackle_0
 	with_subgrid_chemistry=GEAR_10
+	with_subgrid_pressure_floor=GEAR
+	with_subgrid_stars=GEAR
+	with_subgrid_star_formation=GEAR
+	with_subgrid_feedback=GEAR
+	with_subgrid_black_holes=none
+	with_subgrid_sink=GEAR
+	with_subgrid_extra_io=none
+	enable_fof=no
+   ;;
+   GEAR-G3)
+	with_subgrid_cooling=grackle_3
+	with_subgrid_chemistry=GEAR_10
 	with_subgrid_pressure_floor=none
 	with_subgrid_stars=GEAR
 	with_subgrid_star_formation=GEAR
 	with_subgrid_feedback=GEAR
 	with_subgrid_black_holes=none
-	with_subgrid_sink=none
+	with_subgrid_sink=GEAR
 	with_subgrid_extra_io=none
 	enable_fof=no
    ;;
@@ -1845,7 +2075,7 @@ case "$with_subgrid" in
 	with_subgrid_sink=none
 	with_subgrid_extra_io=none
 	enable_fof=no
-   ;;   
+   ;;
    QLA)
 	with_subgrid_cooling=QLA
 	with_subgrid_chemistry=QLA
@@ -1910,6 +2140,19 @@ case "$with_subgrid" in
 	with_subgrid_extra_io=none
 	enable_fof=yes
    ;;
+  SPIN_JET_EAGLE-XL)
+	with_subgrid_cooling=PS2020
+	with_subgrid_chemistry=EAGLE
+	with_subgrid_tracers=EAGLE
+	with_subgrid_entropy_floor=EAGLE
+	with_subgrid_stars=EAGLE
+	with_subgrid_star_formation=EAGLE
+	with_subgrid_feedback=EAGLE
+	with_subgrid_black_holes=SPIN_JET
+	with_subgrid_sink=none
+	with_subgrid_extra_io=none
+	enable_fof=yes
+   ;;
    *)
       AC_MSG_ERROR([Unknown subgrid choice: $with_subgrid])
    ;;
@@ -1980,7 +2223,7 @@ fi
 # Hydro scheme.
 AC_ARG_WITH([hydro],
    [AS_HELP_STRING([--with-hydro=<scheme>],
-      [Hydro dynamics to use @<:@gadget2, minimal, pressure-entropy, pressure-energy, pressure-energy-monaghan, phantom, gizmo-mfv, gizmo-mfm, shadowfax, planetary, sphenix, gasoline, anarchy-pu default: sphenix@:>@]
+      [Hydro dynamics to use @<:@gadget2, minimal, pressure-entropy, pressure-energy, pressure-energy-monaghan, phantom, gizmo-mfv, gizmo-mfm, shadowswift, planetary, remix, sphenix, gasoline, anarchy-pu default: sphenix@:>@]
    )],
    [with_hydro="$withval"],
    [with_hydro="sphenix"]
@@ -2011,18 +2254,25 @@ case "$with_hydro" in
    gizmo-mfv)
       AC_DEFINE([GIZMO_MFV_SPH], [1], [GIZMO MFV SPH])
       need_riemann_solver=yes
+      hydro_does_mass_flux=yes
    ;;
    gizmo-mfm)
       AC_DEFINE([GIZMO_MFM_SPH], [1], [GIZMO MFM SPH])
       need_riemann_solver=yes
    ;;
-   shadowfax)
-      AC_DEFINE([SHADOWFAX_SPH], [1], [Shadowfax SPH])
+   shadowswift)
+      AC_DEFINE([SHADOWSWIFT], [1], [ShadowSWIFT hydrodynamics])
+      AC_DEFINE([MOVING_MESH_HYDRO], [1], [Moving mesh hydrodynamics])
+      need_moving_mesh=yes
       need_riemann_solver=yes
+      hydro_does_mass_flux=yes
    ;;
    planetary)
       AC_DEFINE([PLANETARY_SPH], [1], [Planetary SPH])
    ;;
+   remix)
+      AC_DEFINE([REMIX_SPH], [1], [REMIX SPH])
+   ;;
    sphenix)
       AC_DEFINE([SPHENIX_SPH], [1], [SPHENIX SPH])
    ;;
@@ -2065,8 +2315,8 @@ fi
 if test "$with_hydro" = "gizmo-mfv" -a "$with_spmhd" != "none"; then
   AC_MSG_ERROR([Cannot use an SPMHD scheme alongside a gizmo hydro solver!"])
 fi
-if test "$with_hydro" = "shadowfax" -a "$with_spmhd" != "none"; then
-  AC_MSG_ERROR([Cannot use an SPMHD scheme alongside a gizmo hydro solver!"])
+if test "$with_hydro" = "shadowswift" -a "$with_spmhd" != "none"; then
+  AC_MSG_ERROR([Cannot use an SPMHD scheme alongside a moving mesh hydro solver!"])
 fi
 
 # Check if debugging interactions stars is switched on.
@@ -2188,7 +2438,7 @@ esac
 #  Equation of state
 AC_ARG_WITH([equation-of-state],
    [AS_HELP_STRING([--with-equation-of-state=<EoS>],
-      [equation of state @<:@ideal-gas, isothermal-gas, planetary default: ideal-gas@:>@]
+      [equation of state @<:@ideal-gas, isothermal-gas, barotropic-gas, planetary default: ideal-gas@:>@]
    )],
    [with_eos="$withval"],
    [with_eos="ideal-gas"]
@@ -2199,6 +2449,9 @@ case "$with_eos" in
    ;;
    isothermal-gas)
       AC_DEFINE([EOS_ISOTHERMAL_GAS], [1], [Isothermal gas equation of state])
+   ;;
+     barotropic-gas)
+       AC_DEFINE([EOS_BAROTROPIC_GAS], [1], [Barotropic gas equation of state])
    ;;
    planetary)
       AC_DEFINE([EOS_PLANETARY], [1], [All planetary equations of state])
@@ -2234,6 +2487,34 @@ case "$with_gamma" in
    ;;
 esac
 
+# Adaptive softening
+AC_ARG_WITH([adaptive-softening],
+   [AS_HELP_STRING([--with-adaptive-softening=<yes/no>],
+      [Adaptive softening @<:@no, yes, default: no@:>@]
+   )],
+   [with_adaptive_softening="$withval"],
+   [with_adaptive_softening="no"]
+)
+case "$with_adaptive_softening" in
+   no)
+      AC_DEFINE([FIXED_SOFTENING], [1], [No adaptive softening])
+   ;;
+   yes)
+      AC_DEFINE([ADAPTIVE_SOFTENING], [1], [Adaptive softening])
+   ;;
+   *)
+      AC_MSG_ERROR([Unknown adaptive softening: $with_adaptive_softening])
+   ;;
+esac
+# Verify that the configuration is allowed
+if test "x$with_adaptive_softening" = "xyes" -a "$with_kernel" != "wendland-C2"; then
+  AC_MSG_ERROR([Adaptive softening scheme requires the usage of the Wendland-C2 kernel!])
+fi
+if test "x$with_adaptive_softening" = "xyes" -a "$with_gravity" != "with-multi-softening"; then
+  AC_MSG_ERROR([Adaptive softening scheme requires the usage of the multi-softening gravity scheme!])
+fi
+
+
 #  Riemann solver
 AC_ARG_WITH([riemann-solver],
    [AS_HELP_STRING([--with-riemann-solver=<solver>],
@@ -2264,6 +2545,32 @@ if test "x$need_riemann_solver" = "xyes" -a "$with_riemann" = "none"; then
   AC_MSG_ERROR([Hydro scheme $with_hydro requires selection of a Riemann solver!])
 fi
 
+# Moving mesh
+AC_ARG_ENABLE([moving-mesh],
+    [AS_HELP_STRING([--enable-moving-mesh],
+        [enable the moving mesh computation]
+    )],
+    [enable_moving_mesh="${enableval}"],
+    [enable_moving_mesh="no"]
+)
+if test "x$need_moving_mesh" = "xyes"; then
+    enable_moving_mesh="yes"
+fi
+if test "$enable_moving_mesh" = "yes"; then
+    if test "$have_gmp" = "no"; then
+        AC_MSG_ERROR([GMP is required when using moving mesh!])
+    fi
+    if test "$have_gsl" = "no"; then
+        AC_MSG_ERROR([GSL is required when using moving mesh!])
+    fi
+    AC_DEFINE([MOVING_MESH], [1], [Unstructured Voronoi mesh])
+fi
+
+# Hydro does mass flux?
+if test "x$hydro_does_mass_flux" = "xyes"; then
+    AC_DEFINE([HYDRO_DOES_MASS_FLUX], [1], [Hydro scheme with mass fluxes])
+fi
+
 #  chemistry function
 AC_ARG_WITH([chemistry],
    [AS_HELP_STRING([--with-chemistry=<function>],
@@ -2297,7 +2604,7 @@ case "$with_chemistry" in
    AGORA)
       AC_DEFINE([CHEMISTRY_AGORA], [1], [Chemistry taken from the AGORA model])
       with_chemistry_name="AGORA"
-   ;;   
+   ;;
    QLA)
       AC_DEFINE([CHEMISTRY_QLA], [1], [Chemistry taken from the Quick-Lyman-alpha model])
       with_chemistry_name="QLA"
@@ -2479,7 +2786,7 @@ case "$with_stars" in
    ;;
    GEAR)
       AC_DEFINE([STARS_GEAR], [1], [GEAR stellar model])
-   ;;  
+   ;;
    basic)
       AC_DEFINE([STARS_BASIC], [1], [Basic stellar model])
    ;;
@@ -2529,7 +2836,7 @@ case "$with_feedback" in
    AGORA)
       AC_DEFINE([FEEDBACK_AGORA], [1], [AGORA stellar feedback and evolution model])
       with_feedback_name="AGORA"
-   ;;   
+   ;;
    none)
       AC_DEFINE([FEEDBACK_NONE], [1], [No feedback])
    ;;
@@ -2613,6 +2920,9 @@ case "$with_sink" in
    none)
       AC_DEFINE([SINK_NONE], [1], [No sink particle model])
    ;;
+   Basic)
+    AC_DEFINE([SINK_BASIC], [1], [Simple, self-contained sink model with only Bondi-Hoyle accretion and no SF.])
+    ;;
    GEAR)
     AC_DEFINE([SINK_GEAR], [1], [GEAR sink particle model])
     ;;
@@ -2621,10 +2931,37 @@ case "$with_sink" in
    ;;
 esac
 
+# Forcing terms
+AC_ARG_WITH([forcing],
+   [AS_HELP_STRING([--with-forcing=<term>],
+      [Hydrodynamics forcing terms @<:@none, roberts-flow, roberts-flow-acceleration , abc-flowdefault: none@:>@]
+   )],
+   [with_forcing="$withval"],
+   [with_forcing="none"]
+)
+case "$with_forcing" in
+   none)
+      AC_DEFINE([FORCING_NONE], [1], [No external forcing terms])
+   ;;
+   roberts-flow)
+      AC_DEFINE([FORCING_ROBERTS_FLOW], [1], [Roberts' flow external forcing terms])
+   ;;
+   roberts-flow-acceleration)
+      AC_DEFINE([FORCING_ROBERTS_FLOW_ACCELERATION], [1], [Roberts' flow external forcing terms entering the equations as an acceleration term])
+   ;;
+   abc-flow)
+      AC_DEFINE([FORCING_ABC_FLOW], [1], [ABC flow external forcing terms])
+   ;;
+   *)
+      AC_MSG_ERROR([Unknown external forcing term: $with_forcing])
+   ;;
+esac
+
+
 #  External potential
 AC_ARG_WITH([ext-potential],
    [AS_HELP_STRING([--with-ext-potential=<pot>],
-      [external potential @<:@none, point-mass, point-mass-softened, isothermal, nfw, nfw-mn, hernquist, hernquist-sdmh05, disc-patch, sine-wave, constant, default: none@:>@]
+      [external potential @<:@none, point-mass, point-mass-softened, isothermal, nfw, nfw-mn, hernquist, hernquist-sdmh05, disc-patch, sine-wave, MWPotential2014, constant, default: none@:>@]
    )],
    [with_potential="$withval"],
    [with_potential="none"]
@@ -2660,6 +2997,9 @@ case "$with_potential" in
    point-mass-softened)
       AC_DEFINE([EXTERNAL_POTENTIAL_POINTMASS_SOFT], [1], [Softened point-mass potential with form 1/(r^2 + softening^2).])
    ;;
+   MWPotential2014)
+      AC_DEFINE([EXTERNAL_POTENTIAL_MWPotential2014], [1], [Milky-Way like potential composed of a Navarro-Frenk-White + Miyamoto-Nagai disk + Power spherical cuttoff external potential.])
+   ;;
    constant)
       AC_DEFINE([EXTERNAL_POTENTIAL_CONSTANT], [1], [Constant gravitational acceleration.])
    ;;
@@ -2669,9 +3009,9 @@ case "$with_potential" in
 esac
 
 #  Entropy floor
-AC_ARG_WITH([entropy-floor], 
+AC_ARG_WITH([entropy-floor],
     [AS_HELP_STRING([--with-entropy-floor=<floor>],
-       [entropy floor @<:@none, QLA, EAGLE, default: none@:>@] 
+       [entropy floor @<:@none, QLA, EAGLE, default: none@:>@]
     )],
     [with_entropy_floor="$withval"],
     [with_entropy_floor="none"]
@@ -2697,10 +3037,10 @@ case "$with_entropy_floor" in
    *)
       AC_MSG_ERROR([Unknown entropy floor model])
    ;;
-esac 
+esac
 
 #  Pressure floor
-AC_ARG_WITH([pressure-floor], 
+AC_ARG_WITH([pressure-floor],
     [AS_HELP_STRING([--with-pressure-floor=<floor>],
        [pressure floor @<:@none, GEAR, default: none@:>@
        The hydro model needs to be compatible.]
@@ -2726,12 +3066,12 @@ case "$with_pressure_floor" in
    *)
       AC_MSG_ERROR([Unknown pressure floor model])
    ;;
-esac 
+esac
 
 #  Star formation
-AC_ARG_WITH([star-formation], 
+AC_ARG_WITH([star-formation],
     [AS_HELP_STRING([--with-star-formation=<sfm>],
-       [star formation @<:@none, QLA, EAGLE, GEAR, default: none@:>@] 
+       [star formation @<:@none, QLA, EAGLE, GEAR, default: none@:>@]
     )],
     [with_star_formation="$withval"],
     [with_star_formation="none"]
@@ -2760,7 +3100,7 @@ case "$with_star_formation" in
    *)
       AC_MSG_ERROR([Unknown star formation model])
    ;;
-esac 
+esac
 
 AC_ARG_WITH([gadget2-physical-constants],
     [AS_HELP_STRING([--with-gadget2-physical-constants],
@@ -2786,7 +3126,7 @@ AC_DEFINE_UNQUOTED([SELF_GRAVITY_MULTIPOLE_ORDER], [$with_multipole_order], [Mul
 #  Radiative transfer scheme
 AC_ARG_WITH([rt],
    [AS_HELP_STRING([--with-rt=<scheme>],
-      [Radiative transfer scheme to use @<:@none, GEAR_*, SPHM1RT_*, debug default: none@:>@. 
+      [Radiative transfer scheme to use @<:@none, GEAR_*, SPHM1RT_*, debug default: none@:>@.
       For GEAR and SPHM1RT, the number of photon groups (e.g. GEAR_4) needs to be provided.]
    )],
    [with_rt="$withval"],
@@ -2796,7 +3136,7 @@ AC_ARG_WITH([rt],
 # For GEAR-RT scheme: Select a RT Riemann solver
 AC_ARG_WITH([rt-riemann-solver],
    [AS_HELP_STRING([--with-rt-riemann-solver=<scheme>],
-      [Riemann solver for the moments of the ratiadiative transfer equation with the M1 closure to use @<:@none, HLL, GLF, default: none@:>@. 
+      [Riemann solver for the moments of the ratiadiative transfer equation with the M1 closure to use @<:@none, HLL, GLF, default: none@:>@.
       For the GEAR RT scheme, you need to select one Riemann solver.]
    )],
    [with_rt_riemann_solver="$withval"],
@@ -2826,6 +3166,7 @@ case "$with_rt" in
       AC_DEFINE([RT_GEAR], [1], [GEAR M1 closure scheme])
       number_group=${with_rt#*_}
       AC_DEFINE_UNQUOTED([RT_NGROUPS], [$number_group], [Number of photon groups to follow])
+      AC_DEFINE([MPI_SYMMETRIC_FORCE_INTERACTION_RT], [1], [Do symmetric MPI interactions])
 
       if test "$number_group" = "0"; then
           AC_MSG_ERROR([GEAR-RT: Cannot work with zero photon groups])
@@ -2840,9 +3181,14 @@ case "$with_rt" in
           AC_DEFINE([SWIFT_RT_DEBUG_CHECKS], [1], [additional debugging checks for RT])
       fi
 
-      if test "$with_hydro" != "gizmo-mfv"; then
-          AC_MSG_ERROR([GEAR-RT: Cannot work without gizmo-mfv hydro. Compile using --with-hydro=gizmo-mfv])
-      fi
+      case "$with_hydro" in
+          "gizmo-mfv" | "sphenix")
+          # allowed.
+        ;;
+        *)
+          AC_MSG_ERROR([GEAR-RT: Cannot work without gizmo-mfv or sphenix hydro. Compile using --with-hydro=gizmo-mfv or --with-hydro=sphenix])
+        ;;
+      esac
 
       if test "$with_rt_riemann_solver" = "none"; then
           AC_MSG_ERROR([GEAR-RT: You need to select an RT Riemann solver (--with-rt-riemann-solver=...)])
@@ -2875,8 +3221,8 @@ case "$with_rt" in
       fi
       AC_MSG_CHECKING([for Sundials libraries])
       AC_MSG_RESULT($have_sundials)
-      if test "$have_sundials" != "yes"; then 
-         AC_MSG_ERROR([The Sundials library is not present. Sundials is required for the SPHM1RT module.]) 
+      if test "$have_sundials" != "yes"; then
+         AC_MSG_ERROR([The Sundials library is not present. Sundials is required for the SPHM1RT module.])
       fi
    ;;
    *)
@@ -2914,6 +3260,9 @@ AM_CONDITIONAL([HAVEEAGLEKINETICFEEDBACK], [test "$with_feedback" = "EAGLE-kinet
 # check if using grackle cooling
 AM_CONDITIONAL([HAVEGRACKLECOOLING], [test "$with_cooling" = "grackle"])
 
+# check if using EAGLE floor
+AM_CONDITIONAL([HAVEEAGLEFLOOR], [test "$with_entropy_floor" = "EAGLE"])
+
 # check if using gear feedback
 AM_CONDITIONAL([HAVEGEARFEEDBACK], [test "$with_feedback" = "GEAR"])
 
@@ -2956,6 +3305,9 @@ AM_CONDITIONAL([HAVESPHM1RTRT], [test "${with_rt:0:7}" = "SPHM1RT"])
 # Check if using GEAR-RT radiative transfer
 AM_CONDITIONAL([HAVEGEARRT], [test "${with_rt:0:4}" = "GEAR"])
 
+# Check if using Moving mesh
+AM_CONDITIONAL([HAVE_MOVING_MESH], [test "$enable_moving_mesh" = "yes"])
+
 
 
 
@@ -3003,16 +3355,18 @@ AC_MSG_RESULT([
    Compiler             : $CC
     - vendor            : $ax_cv_c_compiler_vendor
     - version           : $ax_cv_c_compiler_version
-    - flags             : $CFLAGS $OPENMP_CFLAGS
+    - flags             : $CFLAGS
    MPI enabled          : $enable_mpi
    HDF5 enabled         : $with_hdf5
     - parallel          : $have_parallel_hdf5
+   LUSTRE API enabled   : $have_lustreapi
    METIS/ParMETIS       : $have_metis / $have_parmetis
-   FFTW3 enabled        : $have_fftw   
-    - threaded/openmp   : $have_threaded_fftw / $have_openmp_fftw 
+   FFTW3 enabled        : $have_fftw
+    - threaded/openmp   : $have_threaded_fftw / $have_openmp_fftw
     - MPI               : $have_mpi_fftw
     - ARM               : $have_arm_fftw
    GSL enabled          : $have_gsl
+   GMP enabled          : $have_gmp
    HEALPix C enabled    : $have_chealpix
    libNUMA enabled      : $have_numa
    GRACKLE enabled      : $have_grackle
@@ -3023,6 +3377,7 @@ AC_MSG_RESULT([
    VELOCIraptor enabled : $have_velociraptor
    FoF activated:       : $enable_fof
    Lightcones enabled   : $enable_lightcone
+   Moving-mesh enabled  : $enable_moving_mesh
 
    Hydro scheme       : $with_hydro
    Dimensionality     : $with_dimension
@@ -3031,6 +3386,7 @@ AC_MSG_RESULT([
    Adiabatic index    : $with_gamma
    Riemann solver     : $with_riemann
    SPMHD scheme       : $with_spmhd
+   Adaptive softening : $with_adaptive_softening
 
    Gravity scheme      : $with_gravity
    Multipole order     : $with_multipole_order
@@ -3038,6 +3394,7 @@ AC_MSG_RESULT([
    No gravity below ID : $no_gravity_below_id
    Make gravity glass  : $gravity_glass_making
    External potential  : $with_potential
+   Forcing terms       : $with_forcing
 
    Pressure floor       : $with_pressure_floor
    Entropy floor        : $with_entropy_floor
@@ -3051,7 +3408,7 @@ AC_MSG_RESULT([
    Black holes model    : $with_black_holes
    Radiative transfer   : $with_rt
    Extra i/o            : $with_extra_io
-   
+
    Atomic operations in tasks  : $enable_atomics_within_tasks
    Individual timers           : $enable_timers
    Task debugging              : $enable_task_debugging
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
index 8c1c29d90d53bd33f75e81deeb9492fe2bf3b006..3d9dba7ac850a54c7fe7f1c42bc13fea99fbadde 100644
--- a/doc/Doxyfile.in
+++ b/doc/Doxyfile.in
@@ -1,4 +1,4 @@
-# Doxyfile 1.8.17
+# Doxyfile 1.9.8
 
 # This file describes the settings to be used by the documentation system
 # doxygen (www.doxygen.org) for a project.
@@ -12,6 +12,16 @@
 # For lists, items can also be appended using:
 # TAG += value [value, ...]
 # Values that contain spaces should be placed between quotes (\" \").
+#
+# Note:
+#
+# Use doxygen to compare the used configuration file with the template
+# configuration file:
+# doxygen -x [configFile]
+# Use doxygen to compare the used configuration file with the template
+# configuration file without replacing the environment variables or CMake type
+# replacement variables:
+# doxygen -x_noenv [configFile]
 
 #---------------------------------------------------------------------------
 # Project related configuration options
@@ -60,16 +70,28 @@ PROJECT_LOGO           =
 
 OUTPUT_DIRECTORY       = .
 
-# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
-# directories (in 2 levels) under the output directory of each output format and
-# will distribute the generated files over these directories. Enabling this
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
+# sub-directories (in 2 levels) under the output directory of each output format
+# and will distribute the generated files over these directories. Enabling this
 # option can be useful when feeding doxygen a huge amount of source files, where
 # putting all generated files in the same directory would otherwise causes
-# performance problems for the file system.
+# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to
+# control the number of sub-directories.
 # The default value is: NO.
 
 CREATE_SUBDIRS         = NO
 
+# Controls the number of sub-directories that will be created when
+# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every
+# level increment doubles the number of directories, resulting in 4096
+# directories at level 8 which is the default and also the maximum value. The
+# sub-directories are organized in 2 levels, the first level always has a fixed
+# number of 16 directories.
+# Minimum value: 0, maximum value: 8, default value: 8.
+# This tag requires that the tag CREATE_SUBDIRS is set to YES.
+
+CREATE_SUBDIRS_LEVEL   = 8
+
 # If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
 # characters to appear in the names of generated files. If set to NO, non-ASCII
 # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
@@ -81,26 +103,18 @@ ALLOW_UNICODE_NAMES    = NO
 # The OUTPUT_LANGUAGE tag is used to specify the language in which all
 # documentation generated by doxygen is written. Doxygen will use this
 # information to generate all constant output in the proper language.
-# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
-# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
-# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
-# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
-# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
-# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
-# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
-# Ukrainian and Vietnamese.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian,
+# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English
+# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek,
+# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with
+# English messages), Korean, Korean-en (Korean with English messages), Latvian,
+# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese,
+# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish,
+# Swedish, Turkish, Ukrainian and Vietnamese.
 # The default value is: English.
 
 OUTPUT_LANGUAGE        = English
 
-# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
-# documentation generated by doxygen is written. Doxygen will use this
-# information to generate all generated output in the proper direction.
-# Possible values are: None, LTR, RTL and Context.
-# The default value is: None.
-
-OUTPUT_TEXT_DIRECTION  = None
-
 # If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
 # descriptions after the members that are listed in the file and class
 # documentation (similar to Javadoc). Set to NO to disable this.
@@ -217,6 +231,14 @@ QT_AUTOBRIEF           = NO
 
 MULTILINE_CPP_IS_BRIEF = NO
 
+# By default Python docstrings are displayed as preformatted text and doxygen's
+# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the
+# doxygen's special commands can be used and the contents of the docstring
+# documentation blocks is shown as doxygen documentation.
+# The default value is: YES.
+
+PYTHON_DOCSTRING       = YES
+
 # If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
 # documentation from any documented member that it re-implements.
 # The default value is: YES.
@@ -240,25 +262,19 @@ TAB_SIZE               = 8
 # the documentation. An alias has the form:
 # name=value
 # For example adding
-# "sideeffect=@par Side Effects:\n"
+# "sideeffect=@par Side Effects:^^"
 # will allow you to put the command \sideeffect (or @sideeffect) in the
 # documentation, which will result in a user-defined paragraph with heading
-# "Side Effects:". You can put \n's in the value part of an alias to insert
-# newlines (in the resulting output). You can put ^^ in the value part of an
-# alias to insert a newline as if a physical newline was in the original file.
-# When you need a literal { or } or , in the value part of an alias you have to
-# escape them by means of a backslash (\), this can lead to conflicts with the
-# commands \{ and \} for these it is advised to use the version @{ and @} or use
-# a double escape (\\{ and \\})
+# "Side Effects:". Note that you cannot put \n's in the value part of an alias
+# to insert newlines (in the resulting output). You can put ^^ in the value part
+# of an alias to insert a newline as if a physical newline was in the original
+# file. When you need a literal { or } or , in the value part of an alias you
+# have to escape them by means of a backslash (\), this can lead to conflicts
+# with the commands \{ and \} for these it is advised to use the version @{ and
+# @} or use a double escape (\\{ and \\})
 
 ALIASES                =
 
-# This tag can be used to specify a number of word-keyword mappings (TCL only).
-# A mapping has the form "name=value". For example adding "class=itcl::class"
-# will allow you to use the command class in the itcl::class meaning.
-
-TCL_SUBST              =
-
 # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
 # only. Doxygen will then generate output that is more tailored for C. For
 # instance, some of the names that are used will be different. The list of all
@@ -300,18 +316,21 @@ OPTIMIZE_OUTPUT_SLICE  = NO
 # extension. Doxygen has a built-in mapping, but you can override or extend it
 # using this tag. The format is ext=language, where ext is a file extension, and
 # language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
-# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice,
-# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice,
+# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
 # FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
 # tries to guess whether the code is fixed or free formatted code, this is the
-# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat
-# .inc files as Fortran files (default is PHP), and .f files as C (default is
-# Fortran), use: inc=Fortran f=C.
+# default for Fortran type files). For instance to make doxygen treat .inc files
+# as Fortran files (default is PHP), and .f files as C (default is Fortran),
+# use: inc=Fortran f=C.
 #
 # Note: For files without extension you can use no_extension as a placeholder.
 #
 # Note that for custom extensions you also need to set FILE_PATTERNS otherwise
-# the files are not read by doxygen.
+# the files are not read by doxygen. When specifying no_extension you should add
+# * to the FILE_PATTERNS.
+#
+# Note see also the list of default file extension mappings.
 
 EXTENSION_MAPPING      =
 
@@ -334,6 +353,17 @@ MARKDOWN_SUPPORT       = YES
 
 TOC_INCLUDE_HEADINGS   = 5
 
+# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to
+# generate identifiers for the Markdown headings. Note: Every identifier is
+# unique.
+# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a
+# sequence number starting at 0 and GITHUB use the lower case version of title
+# with any whitespace replaced by '-' and punctuation characters removed.
+# The default value is: DOXYGEN.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+MARKDOWN_ID_STYLE      = DOXYGEN
+
 # When enabled doxygen tries to link words that correspond to documented
 # classes, or namespaces to their corresponding documentation. Such a link can
 # be prevented in individual cases by putting a % sign in front of the word or
@@ -445,6 +475,27 @@ TYPEDEF_HIDES_STRUCT   = NO
 
 LOOKUP_CACHE_SIZE      = 0
 
+# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use
+# during processing. When set to 0 doxygen will based this on the number of
+# cores available in the system. You can set it explicitly to a value larger
+# than 0 to get more control over the balance between CPU load and processing
+# speed. At this moment only the input processing can be done using multiple
+# threads. Since this is still an experimental feature the default is set to 1,
+# which effectively disables parallel processing. Please report any issues you
+# encounter. Generating dot graphs in parallel is controlled by the
+# DOT_NUM_THREADS setting.
+# Minimum value: 0, maximum value: 32, default value: 1.
+
+NUM_PROC_THREADS       = 1
+
+# If the TIMESTAMP tag is set different from NO then each generated page will
+# contain the date or date and time when the page was generated. Setting this to
+# NO can help when comparing the output of multiple runs.
+# Possible values are: YES, NO, DATETIME and DATE.
+# The default value is: NO.
+
+TIMESTAMP              = YES
+
 #---------------------------------------------------------------------------
 # Build related configuration options
 #---------------------------------------------------------------------------
@@ -508,6 +559,13 @@ EXTRACT_LOCAL_METHODS  = NO
 
 EXTRACT_ANON_NSPACES   = NO
 
+# If this flag is set to YES, the name of an unnamed parameter in a declaration
+# will be determined by the corresponding definition. By default unnamed
+# parameters remain unnamed in the output.
+# The default value is: YES.
+
+RESOLVE_UNNAMED_PARAMS = YES
+
 # If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
 # undocumented members inside documented classes or files. If set to NO these
 # members will be included in the various overviews, but no documentation
@@ -519,7 +577,8 @@ HIDE_UNDOC_MEMBERS     = NO
 # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
 # undocumented classes that are normally visible in the class hierarchy. If set
 # to NO, these classes will be included in the various overviews. This option
-# has no effect if EXTRACT_ALL is enabled.
+# will also hide undocumented C++ concepts if enabled. This option has no effect
+# if EXTRACT_ALL is enabled.
 # The default value is: NO.
 
 HIDE_UNDOC_CLASSES     = NO
@@ -545,12 +604,20 @@ HIDE_IN_BODY_DOCS      = NO
 
 INTERNAL_DOCS          = NO
 
-# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
-# names in lower-case letters. If set to YES, upper-case letters are also
-# allowed. This is useful if you have classes or files whose names only differ
-# in case and if your file system supports case sensitive file names. Windows
-# (including Cygwin) ands Mac users are advised to set this option to NO.
-# The default value is: system dependent.
+# With the correct setting of option CASE_SENSE_NAMES doxygen will better be
+# able to match the capabilities of the underlying filesystem. In case the
+# filesystem is case sensitive (i.e. it supports files in the same directory
+# whose names only differ in casing), the option must be set to YES to properly
+# deal with such files in case they appear in the input. For filesystems that
+# are not case sensitive the option should be set to NO to properly deal with
+# output files written for symbols that only differ in casing, such as for two
+# classes, one named CLASS and the other named Class, and to also support
+# references to files without having to specify the exact matching casing. On
+# Windows (including Cygwin) and MacOS, users should typically set this option
+# to NO, whereas on Linux or other Unix flavors it should typically be set to
+# YES.
+# Possible values are: SYSTEM, NO and YES.
+# The default value is: SYSTEM.
 
 CASE_SENSE_NAMES       = YES
 
@@ -568,6 +635,12 @@ HIDE_SCOPE_NAMES       = YES
 
 HIDE_COMPOUND_REFERENCE= NO
 
+# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class
+# will show which file needs to be included to use the class.
+# The default value is: YES.
+
+SHOW_HEADERFILE        = YES
+
 # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
 # the files that are included by a file in the documentation of that file.
 # The default value is: YES.
@@ -725,7 +798,8 @@ FILE_VERSION_FILTER    =
 # output files in an output format independent way. To create the layout file
 # that represents doxygen's defaults, run doxygen with the -l option. You can
 # optionally specify a file name after the option, if omitted DoxygenLayout.xml
-# will be used as the name of the layout file.
+# will be used as the name of the layout file. See also section "Changing the
+# layout of pages" for information.
 #
 # Note that if you run doxygen from a directory containing a file called
 # DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
@@ -771,24 +845,50 @@ WARNINGS               = YES
 WARN_IF_UNDOCUMENTED   = YES
 
 # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
-# potential errors in the documentation, such as not documenting some parameters
-# in a documented function, or documenting parameters that don't exist or using
-# markup commands wrongly.
+# potential errors in the documentation, such as documenting some parameters in
+# a documented function twice, or documenting parameters that don't exist or
+# using markup commands wrongly.
 # The default value is: YES.
 
 WARN_IF_DOC_ERROR      = YES
 
+# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete
+# function parameter documentation. If set to NO, doxygen will accept that some
+# parameters have no documentation without warning.
+# The default value is: YES.
+
+WARN_IF_INCOMPLETE_DOC = YES
+
 # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
 # are documented, but have no documentation for their parameters or return
-# value. If set to NO, doxygen will only warn about wrong or incomplete
-# parameter documentation, but not about the absence of documentation. If
-# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# value. If set to NO, doxygen will only warn about wrong parameter
+# documentation, but not about the absence of documentation. If EXTRACT_ALL is
+# set to YES then this flag will automatically be disabled. See also
+# WARN_IF_INCOMPLETE_DOC
 # The default value is: NO.
 
 WARN_NO_PARAMDOC       = NO
 
+# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
+# undocumented enumeration values. If set to NO, doxygen will accept
+# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: NO.
+
+WARN_IF_UNDOC_ENUM_VAL = NO
+
 # If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
-# a warning is encountered.
+# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
+# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
+# at the end of the doxygen process doxygen will return with a non-zero status.
+# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves
+# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not
+# write the warning messages in between other messages but write them at the end
+# of a run, in case a WARN_LOGFILE is defined the warning messages will be
+# besides being in the defined file also be shown at the end of a run, unless
+# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case
+# the behavior will remain as with the setting FAIL_ON_WARNINGS.
+# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
 # The default value is: NO.
 
 WARN_AS_ERROR          = NO
@@ -799,13 +899,27 @@ WARN_AS_ERROR          = NO
 # and the warning text. Optionally the format may contain $version, which will
 # be replaced by the version of the file (if it could be obtained via
 # FILE_VERSION_FILTER)
+# See also: WARN_LINE_FORMAT
 # The default value is: $file:$line: $text.
 
 WARN_FORMAT            = "$file:$line: $text"
 
+# In the $text part of the WARN_FORMAT command it is possible that a reference
+# to a more specific place is given. To make it easier to jump to this place
+# (outside of doxygen) the user can define a custom "cut" / "paste" string.
+# Example:
+# WARN_LINE_FORMAT = "'vi $file +$line'"
+# See also: WARN_FORMAT
+# The default value is: at line $line of file $file.
+
+WARN_LINE_FORMAT       = "at line $line of file $file"
+
 # The WARN_LOGFILE tag can be used to specify a file to which warning and error
 # messages should be written. If left blank the output is written to standard
-# error (stderr).
+# error (stderr). In case the file specified cannot be opened for writing the
+# warning and error messages are written to standard error. When as file - is
+# specified the warning and error messages are written to standard output
+# (stdout).
 
 WARN_LOGFILE           =
 
@@ -825,40 +939,74 @@ INPUT                  = @top_srcdir@ \
                          @top_srcdir@/examples \
                          @top_srcdir@/src/hydro/Minimal \
                          @top_srcdir@/src/hydro/Gadget2 \
+                         @top_srcdir@/src/hydro/SPHENIX \
                          @top_srcdir@/src/gravity/Default \
+                         @top_srcdir@/src/gravity/MultiSoftening \
                          @top_srcdir@/src/riemann \
                          @top_srcdir@/src/potential/point_mass \
                          @top_srcdir@/src/equation_of_state/ideal_gas \
+                         @top_srcdir@/src/equation_of_state/isothermal \
+                         @top_srcdir@/src/equation_of_state/barotropic \
                          @top_srcdir@/src/cooling/const_du \
                          @top_srcdir@/src/cooling/const_lambda \
-                         @top_srcdir@/src/cooling/Compton \
                          @top_srcdir@/src/cooling/EAGLE \
                          @top_srcdir@/src/cooling/PS2020 \
                          @top_srcdir@/src/cooling/grackle \
+                         @top_srcdir@/src/chemistry/AGORA \
                          @top_srcdir@/src/chemistry/EAGLE \
                          @top_srcdir@/src/chemistry/GEAR \
+                         @top_srcdir@/src/chemistry/QLA \
+			 @top_srcdir@/src/chemistry/none \
                          @top_srcdir@/src/entropy_floor/EAGLE \
                          @top_srcdir@/src/pressure_floor/GEAR \
                          @top_srcdir@/src/star_formation/EAGLE \
                          @top_srcdir@/src/star_formation/GEAR \
+                         @top_srcdir@/src/star_formation/QLA \
+                         @top_srcdir@/src/star_formation/none \
                          @top_srcdir@/src/tracers/EAGLE \
                          @top_srcdir@/src/stars/EAGLE \
                          @top_srcdir@/src/stars/GEAR \
+                         @top_srcdir@/src/stars/Basic \
+                         @top_srcdir@/src/stars/None \
+                         @top_srcdir@/src/feedback/none \
+			 @top_srcdir@/src/feedback/AGORA \
                          @top_srcdir@/src/feedback/EAGLE \
+                         @top_srcdir@/src/feedback/EAGLE_thermal \
+                         @top_srcdir@/src/feedback/EAGLE_kinetic \
                          @top_srcdir@/src/feedback/GEAR \
+                         @top_srcdir@/src/black_holes/Default \
                          @top_srcdir@/src/black_holes/EAGLE \
+                         @top_srcdir@/src/black_holes/SPIN_JET \
+                         @top_srcdir@/src/sink/Basic \
                          @top_srcdir@/src/sink/Default \
+                         @top_srcdir@/src/sink/GEAR \
+                         @top_srcdir@/src/forcing \
+                         @top_srcdir@/src/fvpm_geometry \
+                         @top_srcdir@/src/rt \
+                         @top_srcdir@/src/extra_io \
+                         @top_srcdir@/src/neutrino \
                          @top_srcdir@/csds
 
 # This tag can be used to specify the character encoding of the source files
 # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
 # libiconv (or the iconv built into libc) for the transcoding. See the libiconv
-# documentation (see: https://www.gnu.org/software/libiconv/) for the list of
-# possible encodings.
+# documentation (see:
+# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
+# See also: INPUT_FILE_ENCODING
 # The default value is: UTF-8.
 
 INPUT_ENCODING         = UTF-8
 
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify
+# character encoding on a per file pattern basis. Doxygen will compare the file
+# name with each pattern and apply the encoding instead of the default
+# INPUT_ENCODING) if there is a match. The character encodings are a list of the
+# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding
+# "INPUT_ENCODING" for further information on supported encodings.
+
+INPUT_FILE_ENCODING    =
+
 # If the value of the INPUT tag contains directories, you can use the
 # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
 # *.h) to filter out the source-files in the directories.
@@ -867,13 +1015,15 @@ INPUT_ENCODING         = UTF-8
 # need to set EXTENSION_MAPPING for the extension otherwise the files are not
 # read by doxygen.
 #
-# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
-# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
-# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
-# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
-# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen
-# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f, *.for, *.tcl, *.vhd,
-# *.vhdl, *.ucf, *.qsf and *.ice.
+# Note the list of default checked file patterns might differ from the list of
+# default file extension mappings.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,
+# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl,
+# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php,
+# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be
+# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
+# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
 
 FILE_PATTERNS          =
 
@@ -912,10 +1062,7 @@ EXCLUDE_PATTERNS       = *.md
 # (namespaces, classes, functions, etc.) that should be excluded from the
 # output. The symbol name can be a fully qualified name, a word, or if the
 # wildcard * is used, a substring. Examples: ANamespace, AClass,
-# AClass::ANamespace, ANamespace::*Test
-#
-# Note that the wildcards are matched against the file with absolute path, so to
-# exclude all test directories use the pattern */test/*
+# ANamespace::AClass, ANamespace::*Test
 
 EXCLUDE_SYMBOLS        =
 
@@ -960,6 +1107,11 @@ IMAGE_PATH             =
 # code is scanned, but not when the output code is generated. If lines are added
 # or removed, the anchors will not be placed correctly.
 #
+# Note that doxygen will use the data processed and written to standard output
+# for further processing, therefore nothing else, like debug statements or used
+# commands (so in case of a Windows batch file always use @echo OFF), should be
+# written to standard output.
+#
 # Note that for custom extensions or not directly supported extensions you also
 # need to set EXTENSION_MAPPING for the extension otherwise the files are not
 # properly processed by doxygen.
@@ -1001,6 +1153,15 @@ FILTER_SOURCE_PATTERNS =
 
 USE_MDFILE_AS_MAINPAGE =
 
+# The Fortran standard specifies that for fixed formatted Fortran code all
+# characters from position 72 are to be considered as comment. A common
+# extension is to allow longer lines before the automatic comment starts. The
+# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can
+# be processed before the automatic comment starts.
+# Minimum value: 7, maximum value: 10000, default value: 72.
+
+FORTRAN_COMMENT_AFTER  = 72
+
 #---------------------------------------------------------------------------
 # Configuration options related to source browsing
 #---------------------------------------------------------------------------
@@ -1088,16 +1249,24 @@ USE_HTAGS              = NO
 VERBATIM_HEADERS       = YES
 
 # If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
-# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the
-# cost of reduced performance. This can be particularly helpful with template
-# rich C++ code for which doxygen's built-in parser lacks the necessary type
-# information.
+# clang parser (see:
+# http://clang.llvm.org/) for more accurate parsing at the cost of reduced
+# performance. This can be particularly helpful with template rich C++ code for
+# which doxygen's built-in parser lacks the necessary type information.
 # Note: The availability of this option depends on whether or not doxygen was
 # generated with the -Duse_libclang=ON option for CMake.
 # The default value is: NO.
 
 CLANG_ASSISTED_PARSING = NO
 
+# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS
+# tag is set to YES then doxygen will add the directory of each input to the
+# include path.
+# The default value is: YES.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_ADD_INC_PATHS    = YES
+
 # If clang assisted parsing is enabled you can provide the compiler with command
 # line options that you would normally use when invoking the compiler. Note that
 # the include paths will already be set by doxygen for the files and directories
@@ -1107,10 +1276,13 @@ CLANG_ASSISTED_PARSING = NO
 CLANG_OPTIONS          =
 
 # If clang assisted parsing is enabled you can provide the clang parser with the
-# path to the compilation database (see:
-# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the files
-# were built. This is equivalent to specifying the "-p" option to a clang tool,
-# such as clang-check. These options will then be passed to the parser.
+# path to the directory containing a file called compile_commands.json. This
+# file is the compilation database (see:
+# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the
+# options used when the source files were built. This is equivalent to
+# specifying the -p option to a clang tool, such as clang-check. These options
+# will then be passed to the parser. Any options specified with CLANG_OPTIONS
+# will be added as well.
 # Note: The availability of this option depends on whether or not doxygen was
 # generated with the -Duse_libclang=ON option for CMake.
 
@@ -1127,17 +1299,11 @@ CLANG_DATABASE_PATH    =
 
 ALPHABETICAL_INDEX     = YES
 
-# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
-# which the alphabetical index list will be split.
-# Minimum value: 1, maximum value: 20, default value: 5.
-# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
-
-COLS_IN_ALPHA_INDEX    = 5
-
-# In case all classes in a project start with a common prefix, all classes will
-# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
-# can be used to specify a prefix (or a list of prefixes) that should be ignored
-# while generating the index headers.
+# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes)
+# that should be ignored while generating the index headers. The IGNORE_PREFIX
+# tag works for classes, function and member names. The entity will be placed in
+# the alphabetical list under the first letter of the entity name that remains
+# after removing the prefix.
 # This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
 
 IGNORE_PREFIX          =
@@ -1216,7 +1382,12 @@ HTML_STYLESHEET        =
 # Doxygen will copy the style sheet files to the output directory.
 # Note: The order of the extra style sheet files is of importance (e.g. the last
 # style sheet in the list overrules the setting of the previous ones in the
-# list). For an example see the documentation.
+# list).
+# Note: Since the styling of scrollbars can currently not be overruled in
+# Webkit/Chromium, the styling will be left out of the default doxygen.css if
+# one or more extra stylesheets have been specified. So if scrollbar
+# customization is desired it has to be added explicitly. For an example see the
+# documentation.
 # This tag requires that the tag GENERATE_HTML is set to YES.
 
 HTML_EXTRA_STYLESHEET  =
@@ -1231,9 +1402,22 @@ HTML_EXTRA_STYLESHEET  =
 
 HTML_EXTRA_FILES       =
 
+# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output
+# should be rendered with a dark or light theme.
+# Possible values are: LIGHT always generate light mode output, DARK always
+# generate dark mode output, AUTO_LIGHT automatically set the mode according to
+# the user preference, use light mode if no preference is set (the default),
+# AUTO_DARK automatically set the mode according to the user preference, use
+# dark mode if no preference is set and TOGGLE allow to user to switch between
+# light and dark mode via a button.
+# The default value is: AUTO_LIGHT.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE        = AUTO_LIGHT
+
 # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
 # will adjust the colors in the style sheet and background images according to
-# this color. Hue is specified as an angle on a colorwheel, see
+# this color. Hue is specified as an angle on a color-wheel, see
 # https://en.wikipedia.org/wiki/Hue for more information. For instance the value
 # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
 # purple, and 360 is red again.
@@ -1243,7 +1427,7 @@ HTML_EXTRA_FILES       =
 HTML_COLORSTYLE_HUE    = 220
 
 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
-# in the HTML output. For a value of 0 the output will use grayscales only. A
+# in the HTML output. For a value of 0 the output will use gray-scales only. A
 # value of 255 will produce the most vivid colors.
 # Minimum value: 0, maximum value: 255, default value: 100.
 # This tag requires that the tag GENERATE_HTML is set to YES.
@@ -1261,15 +1445,6 @@ HTML_COLORSTYLE_SAT    = 100
 
 HTML_COLORSTYLE_GAMMA  = 80
 
-# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
-# page will contain the date and time when the page was generated. Setting this
-# to YES can help to show when doxygen was last run and thus if the
-# documentation is up to date.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_HTML is set to YES.
-
-HTML_TIMESTAMP         = YES
-
 # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
 # documentation will contain a main index with vertical navigation menus that
 # are dynamically created via JavaScript. If disabled, the navigation index will
@@ -1289,6 +1464,13 @@ HTML_DYNAMIC_MENUS     = YES
 
 HTML_DYNAMIC_SECTIONS  = NO
 
+# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be
+# dynamically folded and expanded in the generated HTML source code.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_CODE_FOLDING      = YES
+
 # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
 # shown in the various tree structured indices initially; the user can expand
 # and collapse entries dynamically later on. Doxygen will expand the tree to
@@ -1304,10 +1486,11 @@ HTML_INDEX_NUM_ENTRIES = 100
 
 # If the GENERATE_DOCSET tag is set to YES, additional index files will be
 # generated that can be used as input for Apple's Xcode 3 integrated development
-# environment (see: https://developer.apple.com/xcode/), introduced with OSX
-# 10.5 (Leopard). To create a documentation set, doxygen will generate a
-# Makefile in the HTML output directory. Running make will produce the docset in
-# that directory and running make install will install the docset in
+# environment (see:
+# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To
+# create a documentation set, doxygen will generate a Makefile in the HTML
+# output directory. Running make will produce the docset in that directory and
+# running make install will install the docset in
 # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
 # startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
 # genXcode/_index.html for more information.
@@ -1324,6 +1507,13 @@ GENERATE_DOCSET        = NO
 
 DOCSET_FEEDNAME        = "Doxygen generated docs"
 
+# This tag determines the URL of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDURL         =
+
 # This tag specifies a string that should uniquely identify the documentation
 # set bundle. This should be a reverse domain-name style string, e.g.
 # com.mycompany.MyDocSet. Doxygen will append .docset to the name.
@@ -1349,8 +1539,12 @@ DOCSET_PUBLISHER_NAME  = Publisher
 # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
 # additional HTML index files: index.hhp, index.hhc, and index.hhk. The
 # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
-# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on
-# Windows.
+# on Windows. In the beginning of 2021 Microsoft took the original page, with
+# a.o. the download links, offline the HTML help workshop was already many years
+# in maintenance mode). You can download the HTML help workshop from the web
+# archives at Installation executable (see:
+# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo
+# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
 #
 # The HTML Help Workshop contains a compiler that can convert all HTML output
 # generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
@@ -1380,7 +1574,7 @@ CHM_FILE               =
 HHC_LOCATION           =
 
 # The GENERATE_CHI flag controls if a separate .chi index file is generated
-# (YES) or that it should be included in the master .chm file (NO).
+# (YES) or that it should be included in the main .chm file (NO).
 # The default value is: NO.
 # This tag requires that the tag GENERATE_HTMLHELP is set to YES.
 
@@ -1407,6 +1601,16 @@ BINARY_TOC             = NO
 
 TOC_EXPAND             = NO
 
+# The SITEMAP_URL tag is used to specify the full URL of the place where the
+# generated documentation will be placed on the server by the user during the
+# deployment of the documentation. The generated sitemap is called sitemap.xml
+# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL
+# is specified no sitemap is generated. For information about the sitemap
+# protocol see https://www.sitemaps.org
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SITEMAP_URL            =
+
 # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
 # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
 # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
@@ -1425,7 +1629,8 @@ QCH_FILE               =
 
 # The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
 # Project output. For more information please see Qt Help Project / Namespace
-# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
 # The default value is: org.doxygen.Project.
 # This tag requires that the tag GENERATE_QHP is set to YES.
 
@@ -1433,8 +1638,8 @@ QHP_NAMESPACE          = org.doxygen.Project
 
 # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
 # Help Project output. For more information please see Qt Help Project / Virtual
-# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-
-# folders).
+# Folders (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).
 # The default value is: doc.
 # This tag requires that the tag GENERATE_QHP is set to YES.
 
@@ -1442,16 +1647,16 @@ QHP_VIRTUAL_FOLDER     = doc
 
 # If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
 # filter to add. For more information please see Qt Help Project / Custom
-# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
-# filters).
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
 # This tag requires that the tag GENERATE_QHP is set to YES.
 
 QHP_CUST_FILTER_NAME   =
 
 # The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
 # custom filter to add. For more information please see Qt Help Project / Custom
-# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
-# filters).
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
 # This tag requires that the tag GENERATE_QHP is set to YES.
 
 QHP_CUST_FILTER_ATTRS  =
@@ -1463,9 +1668,9 @@ QHP_CUST_FILTER_ATTRS  =
 
 QHP_SECT_FILTER_ATTRS  =
 
-# The QHG_LOCATION tag can be used to specify the location of Qt's
-# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
-# generated .qhp file.
+# The QHG_LOCATION tag can be used to specify the location (absolute path
+# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to
+# run qhelpgenerator on the generated .qhp file.
 # This tag requires that the tag GENERATE_QHP is set to YES.
 
 QHG_LOCATION           =
@@ -1508,16 +1713,28 @@ DISABLE_INDEX          = NO
 # to work a browser that supports JavaScript, DHTML, CSS and frames is required
 # (i.e. any modern browser). Windows users are probably better off using the
 # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
-# further fine-tune the look of the index. As an example, the default style
-# sheet generated by doxygen has an example that shows how to put an image at
-# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
-# the same information as the tab index, you could consider setting
-# DISABLE_INDEX to YES when enabling this option.
+# further fine tune the look of the index (see "Fine-tuning the output"). As an
+# example, the default style sheet generated by doxygen has an example that
+# shows how to put an image at the root of the tree instead of the PROJECT_NAME.
+# Since the tree basically has the same information as the tab index, you could
+# consider setting DISABLE_INDEX to YES when enabling this option.
 # The default value is: NO.
 # This tag requires that the tag GENERATE_HTML is set to YES.
 
 GENERATE_TREEVIEW      = NO
 
+# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the
+# FULL_SIDEBAR option determines if the side bar is limited to only the treeview
+# area (value NO) or if it should extend to the full height of the window (value
+# YES). Setting this to YES gives a layout similar to
+# https://docs.readthedocs.io with more room for contents, but less room for the
+# project logo, title, and description. If either GENERATE_TREEVIEW or
+# DISABLE_INDEX is set to NO, this option has no effect.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FULL_SIDEBAR           = NO
+
 # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
 # doxygen will group on one line in the generated HTML documentation.
 #
@@ -1542,6 +1759,24 @@ TREEVIEW_WIDTH         = 250
 
 EXT_LINKS_IN_WINDOW    = NO
 
+# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email
+# addresses.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+OBFUSCATE_EMAILS       = YES
+
+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
+# the HTML output. These images will generally look nicer at scaled resolutions.
+# Possible values are: png (the default) and svg (looks nicer but requires the
+# pdf2svg or inkscape tool).
+# The default value is: png.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FORMULA_FORMAT    = png
+
 # Use this tag to change the font size of LaTeX formulas included as images in
 # the HTML documentation. When you change the font size after a successful
 # doxygen run you need to manually remove any form_*.png images from the HTML
@@ -1551,17 +1786,6 @@ EXT_LINKS_IN_WINDOW    = NO
 
 FORMULA_FONTSIZE       = 10
 
-# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
-# generated for formulas are transparent PNGs. Transparent PNGs are not
-# supported properly for IE 6.0, but are supported on all modern browsers.
-#
-# Note that when changing this option you need to delete any form_*.png files in
-# the HTML output directory before the changes have effect.
-# The default value is: YES.
-# This tag requires that the tag GENERATE_HTML is set to YES.
-
-FORMULA_TRANSPARENT    = YES
-
 # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
 # to create new LaTeX commands to be used in formulas as building blocks. See
 # the section "Including formulas" for details.
@@ -1579,11 +1803,29 @@ FORMULA_MACROFILE      =
 
 USE_MATHJAX            = YES
 
+# With MATHJAX_VERSION it is possible to specify the MathJax version to be used.
+# Note that the different versions of MathJax have different requirements with
+# regards to the different settings, so it is possible that also other MathJax
+# settings have to be changed when switching between the different MathJax
+# versions.
+# Possible values are: MathJax_2 and MathJax_3.
+# The default value is: MathJax_2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_VERSION        = MathJax_2
+
 # When MathJax is enabled you can set the default output format to be used for
-# the MathJax output. See the MathJax site (see:
-# http://docs.mathjax.org/en/latest/output.html) for more details.
+# the MathJax output. For more details about the output format see MathJax
+# version 2 (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3
+# (see:
+# http://docs.mathjax.org/en/latest/web/components/output.html).
 # Possible values are: HTML-CSS (which is slower, but has the best
-# compatibility), NativeMML (i.e. MathML) and SVG.
+# compatibility. This is the name for Mathjax version 2, for MathJax version 3
+# this will be translated into chtml), NativeMML (i.e. MathML. Only supported
+# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This
+# is the name for Mathjax version 3, for MathJax version 2 this will be
+# translated into HTML-CSS) and SVG.
 # The default value is: HTML-CSS.
 # This tag requires that the tag USE_MATHJAX is set to YES.
 
@@ -1596,22 +1838,29 @@ MATHJAX_FORMAT         = HTML-CSS
 # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
 # Content Delivery Network so you can quickly see the result without installing
 # MathJax. However, it is strongly recommended to install a local copy of
-# MathJax from https://www.mathjax.org before deployment.
-# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/.
+# MathJax from https://www.mathjax.org before deployment. The default value is:
+# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2
+# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3
 # This tag requires that the tag USE_MATHJAX is set to YES.
 
 MATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest
 
 # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
 # extension names that should be enabled during MathJax rendering. For example
+# for MathJax version 2 (see
+# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions):
 # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# For example for MathJax version 3 (see
+# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
+# MATHJAX_EXTENSIONS = ams
 # This tag requires that the tag USE_MATHJAX is set to YES.
 
 MATHJAX_EXTENSIONS     =
 
 # The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
 # of code that will be used on startup of the MathJax code. See the MathJax site
-# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an
 # example see the documentation.
 # This tag requires that the tag USE_MATHJAX is set to YES.
 
@@ -1658,7 +1907,8 @@ SERVER_BASED_SEARCH    = NO
 #
 # Doxygen ships with an example indexer (doxyindexer) and search engine
 # (doxysearch.cgi) which are based on the open source search engine library
-# Xapian (see: https://xapian.org/).
+# Xapian (see:
+# https://xapian.org/).
 #
 # See the section "External Indexing and Searching" for details.
 # The default value is: NO.
@@ -1671,8 +1921,9 @@ EXTERNAL_SEARCH        = NO
 #
 # Doxygen ships with an example indexer (doxyindexer) and search engine
 # (doxysearch.cgi) which are based on the open source search engine library
-# Xapian (see: https://xapian.org/). See the section "External Indexing and
-# Searching" for details.
+# Xapian (see:
+# https://xapian.org/). See the section "External Indexing and Searching" for
+# details.
 # This tag requires that the tag SEARCHENGINE is set to YES.
 
 SEARCHENGINE_URL       =
@@ -1767,7 +2018,7 @@ COMPACT_LATEX          = NO
 # The default value is: a4.
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
-PAPER_TYPE             = a4wide
+PAPER_TYPE             = a4
 
 # The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
 # that should be included in the LaTeX output. The package can be specified just
@@ -1781,29 +2032,31 @@ PAPER_TYPE             = a4wide
 
 EXTRA_PACKAGES         =
 
-# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
-# generated LaTeX document. The header should contain everything until the first
-# chapter. If it is left blank doxygen will generate a standard header. See
-# section "Doxygen usage" for information on how to let doxygen write the
-# default header to a separate file.
+# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for
+# the generated LaTeX document. The header should contain everything until the
+# first chapter. If it is left blank doxygen will generate a standard header. It
+# is highly recommended to start with a default header using
+# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty
+# and then modify the file new_header.tex. See also section "Doxygen usage" for
+# information on how to generate the default header that doxygen normally uses.
 #
-# Note: Only use a user-defined header if you know what you are doing! The
-# following commands have a special meaning inside the header: $title,
-# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
-# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
-# string, for the replacement values of the other commands the user is referred
-# to HTML_HEADER.
+# Note: Only use a user-defined header if you know what you are doing!
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. The following
+# commands have a special meaning inside the header (and footer): For a
+# description of the possible markers and block names see the documentation.
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
 LATEX_HEADER           =
 
-# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
-# generated LaTeX document. The footer should contain everything after the last
-# chapter. If it is left blank doxygen will generate a standard footer. See
+# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for
+# the generated LaTeX document. The footer should contain everything after the
+# last chapter. If it is left blank doxygen will generate a standard footer. See
 # LATEX_HEADER for more information on how to generate a default footer and what
-# special commands can be used inside the footer.
-#
-# Note: Only use a user-defined footer if you know what you are doing!
+# special commands can be used inside the footer. See also section "Doxygen
+# usage" for information on how to generate the default footer that doxygen
+# normally uses. Note: Only use a user-defined footer if you know what you are
+# doing!
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
 LATEX_FOOTER           =
@@ -1836,18 +2089,26 @@ LATEX_EXTRA_FILES      =
 
 PDF_HYPERLINKS         = YES
 
-# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
-# the PDF file directly from the LaTeX files. Set this option to YES, to get a
-# higher quality PDF documentation.
+# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as
+# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX
+# files. Set this option to YES, to get a higher quality PDF documentation.
+#
+# See also section LATEX_CMD_NAME for selecting the engine.
 # The default value is: YES.
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
 USE_PDFLATEX           = YES
 
-# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
-# command to the generated LaTeX files. This will instruct LaTeX to keep running
-# if errors occur, instead of asking the user for help. This option is also used
-# when generating formulas in HTML.
+# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.
+# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch
+# mode nothing is printed on the terminal, errors are scrolled as if <return> is
+# hit at every error; missing files that TeX tries to input or request from
+# keyboard input (\read on a not open input stream) cause the job to abort,
+# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,
+# but there is no possibility of user interaction just like in batch mode,
+# SCROLL In scroll mode, TeX will stop only for missing files to input or if
+# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at
+# each error, asking for user intervention.
 # The default value is: NO.
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
@@ -1860,16 +2121,6 @@ LATEX_BATCHMODE        = NO
 
 LATEX_HIDE_INDICES     = NO
 
-# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
-# code with syntax highlighting in the LaTeX output.
-#
-# Note that which sources are shown also depends on other settings such as
-# SOURCE_BROWSER.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_LATEX is set to YES.
-
-LATEX_SOURCE_CODE      = NO
-
 # The LATEX_BIB_STYLE tag can be used to specify the style to use for the
 # bibliography, e.g. plainnat, or ieeetr. See
 # https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
@@ -1878,14 +2129,6 @@ LATEX_SOURCE_CODE      = NO
 
 LATEX_BIB_STYLE        = plain
 
-# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
-# page will contain the date and time when the page was generated. Setting this
-# to NO can help when comparing the output of multiple runs.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_LATEX is set to YES.
-
-LATEX_TIMESTAMP        = NO
-
 # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
 # path from which the emoji images will be read. If a relative path is entered,
 # it will be relative to the LATEX_OUTPUT directory. If left blank the
@@ -1950,16 +2193,6 @@ RTF_STYLESHEET_FILE    =
 
 RTF_EXTENSIONS_FILE    =
 
-# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
-# with syntax highlighting in the RTF output.
-#
-# Note that which sources are shown also depends on other settings such as
-# SOURCE_BROWSER.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_RTF is set to YES.
-
-RTF_SOURCE_CODE        = NO
-
 #---------------------------------------------------------------------------
 # Configuration options related to the man page output
 #---------------------------------------------------------------------------
@@ -2056,27 +2289,44 @@ GENERATE_DOCBOOK       = NO
 
 DOCBOOK_OUTPUT         = docbook
 
-# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
-# program listings (including syntax highlighting and cross-referencing
-# information) to the DOCBOOK output. Note that enabling this will significantly
-# increase the size of the DOCBOOK output.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
-
-DOCBOOK_PROGRAMLISTING = NO
-
 #---------------------------------------------------------------------------
 # Configuration options for the AutoGen Definitions output
 #---------------------------------------------------------------------------
 
 # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
-# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures
 # the structure of the code including all documentation. Note that this feature
 # is still experimental and incomplete at the moment.
 # The default value is: NO.
 
 GENERATE_AUTOGEN_DEF   = NO
 
+#---------------------------------------------------------------------------
+# Configuration options related to Sqlite3 output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3
+# database with symbols found by doxygen stored in tables.
+# The default value is: NO.
+
+GENERATE_SQLITE3       = NO
+
+# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be
+# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put
+# in front of it.
+# The default directory is: sqlite3.
+# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
+
+SQLITE3_OUTPUT         = sqlite3
+
+# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db
+# database file will be recreated with each doxygen run. If set to NO, doxygen
+# will warn if an a database file is already found and not modify it.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
+
+SQLITE3_RECREATE_DB    = YES
+
 #---------------------------------------------------------------------------
 # Configuration options related to the Perl module output
 #---------------------------------------------------------------------------
@@ -2151,7 +2401,8 @@ SEARCH_INCLUDES        = YES
 
 # The INCLUDE_PATH tag can be used to specify one or more directories that
 # contain include files that are not input files but should be processed by the
-# preprocessor.
+# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of
+# RECURSIVE has no effect here.
 # This tag requires that the tag SEARCH_INCLUDES is set to YES.
 
 INCLUDE_PATH           = @top_srcdir@/src
@@ -2172,7 +2423,7 @@ INCLUDE_FILE_PATTERNS  =
 # recursively expanded use the := operator instead of the = operator.
 # This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
-PREDEFINED             = "__attribute__(x)= " \
+PREDEFINED             = "__attribute__(x)=" \
                          HAVE_HDF5 \
                          HAVE_FFTW \
                          WITH_MPI \
@@ -2223,15 +2474,15 @@ TAGFILES               =
 
 GENERATE_TAGFILE       =
 
-# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
-# the class index. If set to NO, only the inherited external classes will be
-# listed.
+# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces
+# will be listed in the class and namespace index. If set to NO, only the
+# inherited external classes will be listed.
 # The default value is: NO.
 
 ALLEXTERNALS           = NO
 
 # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
-# in the modules index. If set to NO, only the current project's groups will be
+# in the topic index. If set to NO, only the current project's groups will be
 # listed.
 # The default value is: YES.
 
@@ -2245,18 +2496,9 @@ EXTERNAL_GROUPS        = YES
 EXTERNAL_PAGES         = YES
 
 #---------------------------------------------------------------------------
-# Configuration options related to the dot tool
+# Configuration options related to diagram generator tools
 #---------------------------------------------------------------------------
 
-# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
-# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
-# NO turns the diagrams off. Note that this option also works with HAVE_DOT
-# disabled, but it is recommended to install and use dot, since it yields more
-# powerful graphs.
-# The default value is: YES.
-
-CLASS_DIAGRAMS         = NO
-
 # You can include diagrams made with dia in doxygen documentation. Doxygen will
 # then run dia to produce the diagram and insert it in the documentation. The
 # DIA_PATH tag allows you to specify the directory where the dia binary resides.
@@ -2272,7 +2514,7 @@ HIDE_UNDOC_RELATIONS   = YES
 
 # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
 # available from the path. This tool is part of Graphviz (see:
-# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
 # Bell Labs. The other options in this section have no effect if this option is
 # set to NO
 # The default value is: YES.
@@ -2289,49 +2531,73 @@ HAVE_DOT               = NO
 
 DOT_NUM_THREADS        = 0
 
-# When you want a differently looking font in the dot files that doxygen
-# generates you can specify the font name using DOT_FONTNAME. You need to make
-# sure dot is able to find the font, which can be done by putting it in a
-# standard location or by setting the DOTFONTPATH environment variable or by
-# setting DOT_FONTPATH to the directory containing the font.
-# The default value is: Helvetica.
+# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of
+# subgraphs. When you want a differently looking font in the dot files that
+# doxygen generates you can specify fontname, fontcolor and fontsize attributes.
+# For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node,
+# Edge and Graph Attributes specification</a> You need to make sure dot is able
+# to find the font, which can be done by putting it in a standard location or by
+# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
+# directory containing the font. Default graphviz fontsize is 14.
+# The default value is: fontname=Helvetica,fontsize=10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_COMMON_ATTR        = "fontname=Helvetica,fontsize=10"
+
+# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can
+# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a
+# href=https://graphviz.org/doc/info/arrows.html>Complete documentation about
+# arrows shapes.</a>
+# The default value is: labelfontname=Helvetica,labelfontsize=10.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
-DOT_FONTNAME           =
+DOT_EDGE_ATTR          = "labelfontname=Helvetica,labelfontsize=10"
 
-# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
-# dot graphs.
-# Minimum value: 4, maximum value: 24, default value: 10.
+# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes
+# around nodes set 'shape=plain' or 'shape=plaintext' <a
+# href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a>
+# The default value is: shape=box,height=0.2,width=0.4.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
-DOT_FONTSIZE           = 10
+DOT_NODE_ATTR          = "shape=box,height=0.2,width=0.4"
 
-# By default doxygen will tell dot to use the default font as specified with
-# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
-# the path where dot can find it using this tag.
+# You can set the path where dot can find font specified with fontname in
+# DOT_COMMON_ATTR and others dot attributes.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
 DOT_FONTPATH           =
 
-# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
-# each documented class showing the direct and indirect inheritance relations.
-# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will
+# generate a graph for each documented class showing the direct and indirect
+# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and
+# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case
+# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the
+# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.
+# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance
+# relations will be shown as texts / links.
+# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.
 # The default value is: YES.
-# This tag requires that the tag HAVE_DOT is set to YES.
 
-CLASS_GRAPH            = YES
+CLASS_GRAPH            = TEXT
 
 # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
 # graph for each documented class showing the direct and indirect implementation
 # dependencies (inheritance, containment, and class references variables) of the
-# class with other documented classes.
+# class with other documented classes. Explicit enabling a collaboration graph,
+# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the
+# command \collaborationgraph. Disabling a collaboration graph can be
+# accomplished by means of the command \hidecollaborationgraph.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
 COLLABORATION_GRAPH    = YES
 
 # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
-# groups, showing the direct groups dependencies.
+# groups, showing the direct groups dependencies. Explicit enabling a group
+# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means
+# of the command \groupgraph. Disabling a directory graph can be accomplished by
+# means of the command \hidegroupgraph. See also the chapter Grouping in the
+# manual.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
@@ -2354,10 +2620,32 @@ UML_LOOK               = NO
 # but if the number exceeds 15, the total amount of fields shown is limited to
 # 10.
 # Minimum value: 0, maximum value: 100, default value: 10.
-# This tag requires that the tag HAVE_DOT is set to YES.
+# This tag requires that the tag UML_LOOK is set to YES.
 
 UML_LIMIT_NUM_FIELDS   = 10
 
+# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and
+# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS
+# tag is set to YES, doxygen will add type and arguments for attributes and
+# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen
+# will not generate fields with class member information in the UML graphs. The
+# class diagrams will look similar to the default class diagrams but using UML
+# notation for the relationships.
+# Possible values are: NO, YES and NONE.
+# The default value is: NO.
+# This tag requires that the tag UML_LOOK is set to YES.
+
+DOT_UML_DETAILS        = NO
+
+# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters
+# to display on a single line. If the actual line length exceeds this threshold
+# significantly it will wrapped across multiple lines. Some heuristics are apply
+# to avoid ugly line breaks.
+# Minimum value: 0, maximum value: 1000, default value: 17.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_WRAP_THRESHOLD     = 17
+
 # If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
 # collaboration graphs will show the relations between templates and their
 # instances.
@@ -2369,7 +2657,9 @@ TEMPLATE_RELATIONS     = NO
 # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
 # YES then doxygen will generate a graph for each documented file showing the
 # direct and indirect include dependencies of the file with other documented
-# files.
+# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,
+# can be accomplished by means of the command \includegraph. Disabling an
+# include graph can be accomplished by means of the command \hideincludegraph.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
@@ -2378,7 +2668,10 @@ INCLUDE_GRAPH          = YES
 # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
 # set to YES then doxygen will generate a graph for each documented file showing
 # the direct and indirect include dependencies of the file with other documented
-# files.
+# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set
+# to NO, can be accomplished by means of the command \includedbygraph. Disabling
+# an included by graph can be accomplished by means of the command
+# \hideincludedbygraph.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
@@ -2418,23 +2711,32 @@ GRAPHICAL_HIERARCHY    = YES
 # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
 # dependencies a directory has on other directories in a graphical way. The
 # dependency relations are determined by the #include relations between the
-# files in the directories.
+# files in the directories. Explicit enabling a directory graph, when
+# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command
+# \directorygraph. Disabling a directory graph can be accomplished by means of
+# the command \hidedirectorygraph.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
 DIRECTORY_GRAPH        = YES
 
+# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels
+# of child directories generated in directory dependency graphs by dot.
+# Minimum value: 1, maximum value: 25, default value: 1.
+# This tag requires that the tag DIRECTORY_GRAPH is set to YES.
+
+DIR_GRAPH_MAX_DEPTH    = 1
+
 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
 # generated by dot. For an explanation of the image formats see the section
 # output formats in the documentation of the dot tool (Graphviz (see:
-# http://www.graphviz.org/)).
+# https://www.graphviz.org/)).
 # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
 # to make the SVG files visible in IE 9+ (other browsers do not have this
 # requirement).
-# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
-# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
-# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo,
-# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# Possible values are: png, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd,
+# gif, gif:cairo, gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd,
+# png:cairo, png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
 # png:gdiplus:gdiplus.
 # The default value is: png.
 # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2466,11 +2768,12 @@ DOT_PATH               =
 
 DOTFILE_DIRS           =
 
-# The MSCFILE_DIRS tag can be used to specify one or more directories that
-# contain msc files that are included in the documentation (see the \mscfile
-# command).
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
 
-MSCFILE_DIRS           =
+DIA_PATH               =
 
 # The DIAFILE_DIRS tag can be used to specify one or more directories that
 # contain dia files that are included in the documentation (see the \diafile
@@ -2479,10 +2782,10 @@ MSCFILE_DIRS           =
 DIAFILE_DIRS           =
 
 # When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
-# path where java can find the plantuml.jar file. If left blank, it is assumed
-# PlantUML is not used or called during a preprocessing step. Doxygen will
-# generate a warning when it encounters a \startuml command in this case and
-# will not generate output for the diagram.
+# path where java can find the plantuml.jar file or to the filename of jar file
+# to be used. If left blank, it is assumed PlantUML is not used or called during
+# a preprocessing step. Doxygen will generate a warning when it encounters a
+# \startuml command in this case and will not generate output for the diagram.
 
 PLANTUML_JAR_PATH      =
 
@@ -2520,18 +2823,6 @@ DOT_GRAPH_MAX_NODES    = 50
 
 MAX_DOT_GRAPH_DEPTH    = 0
 
-# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
-# background. This is disabled by default, because dot on Windows does not seem
-# to support this out of the box.
-#
-# Warning: Depending on the platform used, enabling this option may lead to
-# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
-# read).
-# The default value is: NO.
-# This tag requires that the tag HAVE_DOT is set to YES.
-
-DOT_TRANSPARENT        = NO
-
 # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
 # files in one run (i.e. multiple -o and -T options on the command line). This
 # makes dot run faster, but since only newer versions of dot (>1.8.10) support
@@ -2544,14 +2835,34 @@ DOT_MULTI_TARGETS      = YES
 # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
 # explaining the meaning of the various boxes and arrows in the dot generated
 # graphs.
+# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal
+# graphical representation for inheritance and collaboration diagrams is used.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
 GENERATE_LEGEND        = YES
 
-# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
 # files that are used to generate the various graphs.
+#
+# Note: This setting is not only used for dot files but also for msc temporary
+# files.
 # The default value is: YES.
-# This tag requires that the tag HAVE_DOT is set to YES.
 
 DOT_CLEANUP            = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will
+# use a built-in version of mscgen tool to produce the charts. Alternatively,
+# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,
+# specifying prog as the value, doxygen will call the tool as prog -T
+# <outfile_format> -o <outputfile> <inputfile>. The external tool should support
+# output file formats "png", "eps", "svg", and "ismap".
+
+MSCGEN_TOOL            =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
diff --git a/doc/RTD/source/AnalysisTools/index.rst b/doc/RTD/source/AnalysisTools/index.rst
index 15922608cf93fd0deb04769272b8d23307d5b74b..ac28fb41a747a70ce62c0085e0427e5465a7de63 100644
--- a/doc/RTD/source/AnalysisTools/index.rst
+++ b/doc/RTD/source/AnalysisTools/index.rst
@@ -24,14 +24,14 @@ While the initial graph is showing all the tasks/dependencies, the next ones are
 Task dependencies for a single cell
 -----------------------------------
 
-There is an option to additionally write the dependency graphs of the task dependencies for a single cell. 
+There is an option to additionally write the dependency graphs of the task dependencies for a single cell.
 You can select which cell to write using the ``Scheduler:dependency_graph_cell: cellID`` parameter, where ``cellID`` is the cell ID of type long long.
 This feature will create an individual file for each step specified by the ``Scheduler:dependency_graph_frequency`` and, differently from the full task graph, will create an individual file for each MPI rank that has this cell.
 
 Using this feature has several requirements:
 
 - You need to compile SWIFT including either ``--enable-debugging-checks`` or ``--enable-cell-graph``. Otherwise, cells won't have IDs.
-- There is a limit on how many cell IDs SWIFT can handle while enforcing them to be reproduceably unique. That limit is up to 32 top level cells in any dimension, and up to 16 levels of depth. If any of these thresholds are exceeded, the cells will still have unique cell IDs, but the actual IDs will most likely vary between any two runs.
+- There is a limit on how many cell IDs SWIFT can handle while enforcing them to be reproducibly unique. That limit is up to 32 top level cells in any dimension, and up to 16 levels of depth. If any of these thresholds are exceeded, the cells will still have unique cell IDs, but the actual IDs will most likely vary between any two runs.
 
 To plot the task dependencies, you can use the same script as before: ``tools/plot_task_dependencies.py``. The dependency graph now may have some tasks with a pink-ish background colour: These tasks represent dependencies that are unlocked by some other task which is executed for the requested cell, but the cell itself doesn't have an (active) task of that type itself in that given step.
 
@@ -39,15 +39,15 @@ To plot the task dependencies, you can use the same script as before: ``tools/pl
 Task levels
 -----------------
 
-At the beginning of each simulation the file ``task_level_0.txt`` is generated. 
+At the beginning of each simulation the file ``task_level_0.txt`` is generated.
 It contains the counts of all tasks at all levels (depths) in the tree.
 The depths and counts of the tasks can be plotted with the script ``tools/plot_task_levels.py``.
 It will display the individual tasks at the x-axis, the number of each task at a given level on the y-axis, and the level is shown as the colour of the plotted point.
-Additionally, the script can write out in brackets next to each tasks's name on the x-axis on how many different levels the task exists using the ``--count`` flag.
+Additionally, the script can write out in brackets next to each task's name on the x-axis on how many different levels the task exists using the ``--count`` flag.
 Finally, in some cases the counts for different levels of a task may be very close to each other and overlap on the plot, making them barely visible.
-This can be alleviated by using the ``--displace`` flag: 
+This can be alleviated by using the ``--displace`` flag:
 It will displace the plot points w.r.t. the y-axis in an attempt to make them better visible, however the counts won't be exact in that case.
-If you wish to have more task level plots, you can use the parameter ``Scheduler:task_level_output_frequency``. 
+If you wish to have more task level plots, you can use the parameter ``Scheduler:task_level_output_frequency``.
 It defines how many steps are done in between two task level output dumps.
 
 
@@ -139,9 +139,9 @@ Each line of the logs contains the following information:
    activation:       1 if record for the start of a request, 0 if request completion
    tag:              MPI tag of the request
    size:             size, in bytes, of the request
-   sum:              sum, in bytes, of all requests that are currently not logged as complete 
+   sum:              sum, in bytes, of all requests that are currently not logged as complete
 
-The stic values should be synchronized between ranks as all ranks have a
+The stic values should be synchronised between ranks as all ranks have a
 barrier in place to make sure they start the step together, so should be
 suitable for matching between ranks. The unique keys to associate records
 between ranks (so that the MPI_Isend and MPI_Irecv pairs can be identified)
@@ -161,27 +161,27 @@ on which the additional task data will be dumped. Swift will then create ``threa
 and ``thread_info-step<nr>.dat`` files. Similarly, for threadpool related tools, you need to compile
 swift with ``--enable-threadpool-debugging`` and then run it with ``-Y <interval>``.
 
-For the analysis and plotting scripts listed below, you need to provide the **\*info-step<nr>.dat** 
+For the analysis and plotting scripts listed below, you need to provide the **\*info-step<nr>.dat**
 files as a cmdline argument, not the ``*stats-step<nr>.dat`` files.
 
 A short summary of the scripts in ``tools/task_plots/``:
 
-- ``analyse_tasks.py``: 
+- ``analyse_tasks.py``:
     The output is an analysis of the task timings, including deadtime per thread
     and step, total amount of time spent for each task type, for the whole step
     and per thread and the minimum and maximum times spent per task type.
-- ``analyse_threadpool_tasks.py``: 
-    The output is an analysis of the threadpool task timings, including 
+- ``analyse_threadpool_tasks.py``:
+    The output is an analysis of the threadpool task timings, including
     deadtime per thread and step, total amount of time spent for each task type, for the
     whole step and per thread and the minimum and maximum times spent per task type.
-- ``iplot_tasks.py``: 
-    An interactive task plot, showing what thread was doing what task and for 
-    how long for a step.  **Needs python2 and the tkinter module**.
-- ``plot_tasks.py``: 
-    Creates a task plot image, showing what thread was doing what task and for how long. 
-- ``plot_threadpool.py``: 
+- ``iplot_tasks.py``:
+    An interactive task plot, showing what thread was doing what task and for
+    how long for a step.  **Needs the tkinter module**.
+- ``plot_tasks.py``:
+    Creates a task plot image, showing what thread was doing what task and for how long.
+- ``plot_threadpool.py``:
     Creates a threadpool plot image, showing what thread was doing what threadpool call and for
-    how long. 
+    how long.
 
 
 For more details on the scripts as well as further options, look at the documentation at the top
@@ -189,7 +189,7 @@ of the individual scripts and call them with the ``-h`` flag.
 
 Task data is also dumped when using MPI and the tasks above can be used on
 that as well, some offer the ability to process all ranks, and others to
-select individual ranks. 
+select individual ranks.
 
 It is also possible to process a complete run of task data from all the
 available steps using the ``process_plot_tasks.py`` and
@@ -205,6 +205,8 @@ by using the size of the task data files to schedule parallel processes more
 effectively (the ``--weights`` argument).
 
 
+.. _dumperThread:
+
 Live internal inspection using the dumper thread
 ------------------------------------------------
 
@@ -236,49 +238,81 @@ than once. For a non-MPI run the file is simply called ``.dump``, note for MPI
 you need to create one file per rank, so ``.dump.0``, ``.dump.1`` and so on.
 
 
+Deadlock Detector
+---------------------------
+
+When configured with ``--enable-debugging-checks``, the parameter
+
+.. code-block:: yaml
+
+    Scheduler:
+        deadlock_waiting_time_s:   300.
+
+can be specified. It specifies the time (in seconds) the scheduler should wait
+for a new task to be executed during a simulation step (specifically: during a
+call to ``engine_launch()``). After this time passes without any new tasks being
+run, the scheduler assumes that the code has deadlocked. It then dumps the same
+diagnostic data as :ref:`the dumper thread <dumperThread>` (active tasks, queued
+tasks, and memuse/MPIuse reports, if swift was configured with the corresponding
+flags) and aborts.
+
+A value of zero or a negative value for ``deadlock_waiting_time_s`` disable the
+deadlock detector.
+
+You are likely well advised to try and err on the upper side for the time to
+choose for the ``deadlock_waiting_time_s`` parameter. A value in the order of
+several (tens of) minutes is recommended. A too small value might cause your run to
+erroneously crash and burn despite not really being deadlocked, just slow or
+badly balanced.
+
+
+
+
+
+
 Neighbour search statistics
 ---------------------------
 
-One of the core algorithms in SWIFT is an iterative neighbour search 
-whereby we try to find an appropriate radius around a particle's 
-position so that the weighted sum over neighbouring particles within 
-that radius is equal to some target value. The most obvious example of 
-this iterative neighbour search is the SPH density loop, but various 
-sub-grid models employ a very similar iterative neighbour search. The 
-computational cost of this iterative search is significantly affected by 
-the number of iterations that is required, and it can therefore be 
+One of the core algorithms in SWIFT is an iterative neighbour search
+whereby we try to find an appropriate radius around a particle's
+position so that the weighted sum over neighbouring particles within
+that radius is equal to some target value. The most obvious example of
+this iterative neighbour search is the SPH density loop, but various
+sub-grid models employ a very similar iterative neighbour search. The
+computational cost of this iterative search is significantly affected by
+the number of iterations that is required, and it can therefore be
 useful to analyse the progression of the iterative scheme in detail.
 
-When configured with ``--enable-ghost-statistics=X``, SWIFT will be 
-compiled with additional diagnostics that statistically track the number 
-of iterations required to find a converged answer. Here, ``X`` is a 
-fixed number of bins to use to collect the required statistics 
-(``ghost`` refers to the fact that the iterations take place inside the 
-ghost tasks). In practice, this means that every cell in the SWIFT tree 
-will be equipped with an additional ``struct`` containing three sets of 
-``X`` bins (one set for each iterative neighbour loop: hydro, stellar 
-feedback, AGN feedback). For each bin ``i``, we store the number of 
-particles that required updating during iteration ``i``, the number of 
-particles that could not find a single neighbouring particle, the 
-minimum and maximum smoothing length of all particles that required 
-updating, and the sum of all their search radii and all their search 
-radii squared. This allows us to calculate the upper and lower limits, 
-as well as the mean and standard deviation on the search radius for each 
-iteration and for each cell. Note that there could be more iterations 
-required than the number of bins ``X``; in this case the additional 
-iterations will be accumulated in the final bin. At the end of each time 
-step, a text file is produced (one per MPI rank) that contains the 
-information for all cells that had any relevant activity. This text file 
-is named ``ghost_stats_ssss_rrrr.txt``, where ``ssss`` is the step 
+When configured with ``--enable-ghost-statistics=X``, SWIFT will be
+compiled with additional diagnostics that statistically track the number
+of iterations required to find a converged answer. Here, ``X`` is a
+fixed number of bins to use to collect the required statistics
+(``ghost`` refers to the fact that the iterations take place inside the
+ghost tasks). In practice, this means that every cell in the SWIFT tree
+will be equipped with an additional ``struct`` containing three sets of
+``X`` bins (one set for each iterative neighbour loop: hydro, stellar
+feedback, AGN feedback). For each bin ``i``, we store the number of
+particles that required updating during iteration ``i``, the number of
+particles that could not find a single neighbouring particle, the
+minimum and maximum smoothing length of all particles that required
+updating, and the sum of all their search radii and all their search
+radii squared. This allows us to calculate the upper and lower limits,
+as well as the mean and standard deviation on the search radius for each
+iteration and for each cell. Note that there could be more iterations
+required than the number of bins ``X``; in this case the additional
+iterations will be accumulated in the final bin. At the end of each time
+step, a text file is produced (one per MPI rank) that contains the
+information for all cells that had any relevant activity. This text file
+is named ``ghost_stats_ssss_rrrr.txt``, where ``ssss`` is the step
 counter for that time step and ``rrrr`` is the MPI rank.
 
-The script ``tools/plot_ghost_stats.py`` takes one or multiple 
-``ghost_stats.txt`` files and computes global statistics for all the 
-cells in those files. The script also takes the name of an output file 
-where it will save those statistics as a set of plots, and an optional 
-label that will be displayed as the title of the plots. Note that there 
-are no restrictions on the number of input files or how they relate; 
-different files could represent different MPI ranks, but also different 
-time steps or even different simulations (which would make little 
-sense). It is up to the user to make sure that the input is actually 
+The script ``tools/plot_ghost_stats.py`` takes one or multiple
+``ghost_stats.txt`` files and computes global statistics for all the
+cells in those files. The script also takes the name of an output file
+where it will save those statistics as a set of plots, and an optional
+label that will be displayed as the title of the plots. Note that there
+are no restrictions on the number of input files or how they relate;
+different files could represent different MPI ranks, but also different
+time steps or even different simulations (which would make little
+sense). It is up to the user to make sure that the input is actually
 relevant.
diff --git a/doc/RTD/source/CitingSWIFT/index.rst b/doc/RTD/source/CitingSWIFT/index.rst
index dae84106dac5c12e135cdb5abdb5239b23704e05..bbc547fda3d7f768120732be6d6ac9806291ad8a 100644
--- a/doc/RTD/source/CitingSWIFT/index.rst
+++ b/doc/RTD/source/CitingSWIFT/index.rst
@@ -52,18 +52,19 @@ following bibtex citation block:
 
   @ARTICLE{2023arXiv230513380S,
     author = {{Schaller}, Matthieu and others},
-    title = "{Swift: A modern highly-parallel gravity and smoothed particle hydrodynamics solver for astrophysical and cosmological applications}",
-    journal = {arXiv e-prints},
-    keywords = {Astrophysics - Instrumentation and Methods for Astrophysics, Astrophysics - Cosmology and Nongalactic Astrophysics, Astrophysics - Earth and Planetary Astrophysics, Astrophysics - Astrophysics of Galaxies, Computer Science - Distributed, Parallel, and Cluster Computing},
-    year = 2023,
+    title = "{SWIFT: A modern highly-parallel gravity and smoothed particle hydrodynamics solver for astrophysical and cosmological applications}",
+    journal = {\mnras},
+    keywords = {software: simulations, methods: numerical, software: public release, Astrophysics - Instrumentation and Methods for Astrophysics, Astrophysics - Cosmology and Nongalactic Astrophysics, Astrophysics - Earth and Planetary Astrophysics, Astrophysics - Astrophysics of Galaxies, Computer Science - Distributed, Parallel, and Cluster Computing},
+    year = 2024,
     month = may,
-    eid = {arXiv:2305.13380},
-    pages = {arXiv:2305.13380},
-    doi = {10.48550/arXiv.2305.13380},
+    volume = {530},
+    number = {2},
+    pages = {2378-2419},
+    doi = {10.1093/mnras/stae922},
     archivePrefix = {arXiv},
     eprint = {2305.13380},
     primaryClass = {astro-ph.IM},
-    adsurl = {https://ui.adsabs.harvard.edu/abs/2023arXiv230513380S},
+    adsurl = {https://ui.adsabs.harvard.edu/abs/2024MNRAS.530.2378S},
     adsnote = {Provided by the SAO/NASA Astrophysics Data System}
   }
 
@@ -101,5 +102,4 @@ code. This corresponds to the following bibtex citation block:
 When using models or parts of the code whose details were introduced in other
 papers, we kindly ask that the relevant work is properly acknowledge and
 cited. This includes the :ref:`subgrid`, the :ref:`planetary` extensions, the
-hydrodynamics and radiative transfer implementations, or the particle-based
-:ref:`neutrinos`.
+:ref:`hydro` and :ref:`rt`, or the particle-based :ref:`neutrinos`.
diff --git a/doc/RTD/source/EquationOfState/index.rst b/doc/RTD/source/EquationOfState/index.rst
index 509497a4891fa79fa9b3762e7134487fe1a80b55..8b378136c8620546cc19285cfa0bda9c81d97429 100644
--- a/doc/RTD/source/EquationOfState/index.rst
+++ b/doc/RTD/source/EquationOfState/index.rst
@@ -7,20 +7,20 @@
 Equations of State
 ==================
 
-Currently, SWIFT offers two different gas equations of state (EoS)
-implemented: ``ideal`` and ``isothermal``; as well as a variety of EoS for
-"planetary" materials.  The EoS describe the relations between our
-main thermodynamical variables: the internal energy per unit mass
-(\\(u\\)), the mass density (\\(\\rho\\)), the entropy (\\(A\\)) and
-the pressure (\\(P\\)).
+Currently, SWIFT offers three different gas equations of state (EoS)
+implemented: ``ideal``, ``isothermal``, and ``barotropic``; as well as a variety
+of EoS for "planetary" materials.  The EoS describe the relations between our
+main thermodynamical variables: the internal energy per unit mass :math:`u`, the
+mass density :math:`\rho`, the entropy :math:`A` and the pressure :math:`P`.
+It is selected af configure time via the option ``--with-equation-of-state``.
 
 Gas EoS
 -------
 
-We write the adiabatic index as \\(\\gamma \\) and \\( c_s \\) denotes
+We write the adiabatic index as :math:`\gamma` and :math:`c_s` denotes
 the speed of sound. The adiabatic index can be changed at configure
 time by choosing one of the allowed values of the option
-``--with-adiabatic-index``. The default value is \\(\\gamma = 5/3 \\).
+``--with-adiabatic-index``. The default value is :math:`\gamma = 5/3`.
 
 The tables below give the expression for the thermodynamic quantities
 on each row entry as a function of the gas density and the
@@ -29,27 +29,38 @@ thermodynamical quantity given in the header of each column.
 .. csv-table:: Ideal Gas
    :header: "Variable", "A", "u", "P"
 	   
-   "A", "", "\\( \\left( \\gamma - 1 \\right) u \\rho^{1-\\gamma} \\)", "\\(P \\rho^{-\\gamma} \\)"
-   "u", "\\( A \\frac{ \\rho^{ \\gamma - 1 } }{\\gamma - 1 } \\)", "", "\\(\\frac{1}{\\gamma - 1} \\frac{P}{\\rho}\\)"
-   "P", "\\( A \\rho^\\gamma \\)", "\\( \\left( \\gamma - 1\\right) u \\rho \\)", ""
-   "\\(c_s\\)", "\\(\\sqrt{ \\gamma \\rho^{\\gamma - 1} A}\\)", "\\(\\sqrt{ u \\gamma \\left( \\gamma - 1 \\right) } \\)", "\\(\\sqrt{ \\frac{\\gamma P}{\\rho} }\\)"
+   "A", "", :math:`\left( \gamma - 1 \right) u \rho^{1-\gamma}`, :math:`P \rho^{-\gamma}`
+   "u", :math:`A \frac{ \rho^{ \gamma - 1 } }{\gamma - 1 }`, "", :math:`\frac{1}{\gamma - 1} \frac{P}{\rho}`
+   "P", :math:`A \rho^\gamma`, :math:`\left( \gamma - 1\right) u \rho`, ""
+   :math:`c_s`, :math:`\sqrt{ \gamma \rho^{\gamma - 1} A}`, :math:`\sqrt{ u \gamma \left( \gamma - 1 \right) }`, :math:`\sqrt{ \frac{\gamma P}{\rho} }`
 
 
 .. csv-table:: Isothermal Gas
-   :header: "Variable", "A", "u", "P"
+   :header: "Variable", "-", "-", "-"
 
 	    
-   "A", "", "\\(\\left( \\gamma - 1 \\right) u \\rho^{1-\\gamma}\\)", "" 
+   "A", "", :math:`\left( \gamma - 1 \right) u \rho^{1-\gamma}`, "" 
    "u", "", "const", ""
-   "P", "", "\\(\\left( \\gamma - 1\\right) u \\rho \\)", ""
-   "\\( c_s\\)", "", "\\(\\sqrt{ u \\gamma \\left( \\gamma - 1 \\right) } \\)", ""
-
-Note that when running with an isothermal equation of state, the value
-of the tracked thermodynamic variable (e.g. the entropy in a
+   "P", "", :math:`\left( \gamma - 1\right) u \rho`, ""
+   :math:`c_s`, "", :math:`\sqrt{ u \gamma \left( \gamma - 1 \right) }`, ""
+
+.. csv-table:: Barotropic Gas
+   :header: "Variable", "-", "-", "-"
+
+   "A", "", :math:`\rho^{1-\gamma} c_0^2 \sqrt{1 + \left( \frac{\rho}{\rho_c}  \right) }`, ""
+   "u", "", :math:`\frac{1}(\gamma -1)c_0^2 \sqrt{1 + \left( \frac{\rho}{\rho_c}  \right) }`, ""
+   "P", "", :math:`\rho c_0^2 \sqrt{1 + \left( \frac{\rho}{\rho_c}  \right) }`, ""
+   :math:`c_s`, "", :math:`\sqrt{ c_0^2 \sqrt{1 + \left( \frac{\rho}{\rho_c}  \right) }}`, ""
+   
+Note that when running with an isothermal or barotropic equation of state, the
+value of the tracked thermodynamic variable (e.g. the entropy in a
 density-entropy scheme or the internal enegy in a density-energy SPH
-formulation) written to the snapshots is meaningless. The pressure,
-however, is always correct in all scheme.
+formulation) written to the snapshots is meaningless. The pressure, however, is
+always correct in all scheme.
 
+For the isothermal equation of state, the internal energy is specified at
+runtime via the parameter file. In the case of the barotropic gas, the vacuum
+sound speed :math:`c_0` and core density :math:`\rho_c` are similarly specified.
 
 
 Planetary EoS
@@ -66,7 +77,7 @@ See :ref:`new_option` for a full list of required changes.
 
 You will need to provide an ``equation_of_state.h`` file containing: the
 definition of ``eos_parameters``, IO functions and transformations between the
-different variables: \\(u(\\rho, A)\\), \\(u(\\rho, P)\\), \\(P(\\rho,A)\\),
-\\(P(\\rho, u)\\), \\(A(\\rho, P)\\), \\(A(\\rho, u)\\), \\(c_s(\\rho, A)\\),
-\\(c_s(\\rho, u)\\) and \\(c_s(\\rho, P)\\). See other equation of state files
+different variables: :math:`u(\rho, A)`, :math:`u(\rho, P)`, :math:`P(\rho,A)`,
+:math:`P(\rho, u)`, :math:`A(\rho, P)`, :math:`A(\rho, u)`, :math:`c_s(\rho, A)`,
+:math:`c_s(\rho, u)` and :math:`c_s(\rho, P)`. See other equation of state files
 to have implementation details.
diff --git a/doc/RTD/source/ExternalPotentials/index.rst b/doc/RTD/source/ExternalPotentials/index.rst
index 0454da53ab5a591452bb68bc1860859ecd83dc26..28cd38e8ed1b92f0b3ac2a3690d6afa7c5b5a9c7 100644
--- a/doc/RTD/source/ExternalPotentials/index.rst
+++ b/doc/RTD/source/ExternalPotentials/index.rst
@@ -344,7 +344,105 @@ follows the definitions of `Creasey, Theuns & Bower (2013)
 <https://adsabs.harvard.edu/abs/2013MNRAS.429.1922C>`_ equations (16) and (17).
 The potential is implemented along the x-axis.
 
+12. MWPotential2014 (``MWPotential2014``)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This potential is based on ``galpy``'s ``MWPotential2014`` from `Jo Bovy (2015) <https://ui.adsabs.harvard.edu/abs/2015ApJS..216...29B>`_ and consists in a NFW potential for the halo, an axisymmetric Miyamoto-Nagai potential for the disk and a bulge modeled by a power spherical law with exponential cut-off. The bulge is given by the density:
+
+:math:`\rho(r) = A \left( \frac{r_1}{r} \right)^\alpha \exp \left( - \frac{r^2}{r_c^2} \right)`,
+
+where :math:`A` is an amplitude, :math:`r_1` is a reference radius for amplitude, :math:`\alpha` is the inner power and :math:`r_c` is the cut-off radius.
+
+The resulting potential is:
+
+:math:`\Phi_{\mathrm{MW}}(R, z) = f_1 \Phi_{\mathrm{NFW}} + f_2 \Phi_{\mathrm{MN}} + f_3 \Phi_{\text{bulge}}`,
+
+where :math:`R^2 = x^2 + y^2` is the projected radius and :math:`f_1`, :math:`f_2` and :math:`f_3` are three coefficients that adjust the strength of each individual component.
+
+The parameters of the model are:
+
+.. code:: YAML
+
+    MWPotential2014Potential:
+      useabspos:        0          # 0 -> positions based on centre, 1 -> absolute positions
+      position:         [0.,0.,0.] # Location of centre of potential with respect to centre of the box (if 0) otherwise absolute (if 1) (internal units)
+      timestep_mult:    0.005      # Dimensionless pre-factor for the time-step condition, basically determines the fraction of the orbital time we use to do the time integration
+      epsilon:          0.001      # Softening size (internal units)
+      concentration:    9.823403437774843      # concentration of the Halo
+      M_200_Msun:       147.41031542774076e10  # M200 of the galaxy disk (in M_sun)
+      H:                1.2778254614201471     # Hubble constant in units of km/s/Mpc
+      Mdisk_Msun:       6.8e10                 # Mass of the disk (in M_sun)
+      Rdisk_kpc:        3.0                    # Effective radius of the disk (in kpc)
+      Zdisk_kpc:        0.280                  # Scale-height of the disk (in kpc)
+      amplitude_Msun_per_kpc3: 1.0e10          # Amplitude of the bulge (in M_sun/kpc^3)
+      r_1_kpc:          1.0                    # Reference radius for amplitude of the bulge (in kpc)
+      alpha:            1.8                    # Exponent of the power law of the bulge
+      r_c_kpc:          1.9                    # Cut-off radius of the bulge (in kpc)
+      potential_factors: [0.4367419745056084, 1.002641971008805, 0.022264787598364262] #Coefficients that adjust the strength of the halo (1st component), the disk (2nd component) and the bulge (3rd component)
+
+Note that the default value of the "Hubble constant" here seems odd. As it
+enters multiplicatively with the :math:`f_1` term, the absolute normalisation is
+actually not important.
+
+Dynamical friction
+..................
+
+This potential can be supplemented by a dynamical friction force, following the Chandrasekhar’s dynamical friction formula,
+where the velocity distribution function is assumed to be Maxwellian (Binney & Tremaine 2008, eq. 8.7):
+
+:math:`\frac{\rm{d} \vec{v}_{\rm M}}{\rm{d} t}=-\frac{4\pi G^2M_{\rm sat}\rho \ln \Lambda}{v^3_{\rm{M}}} \left[ \rm{erf}(X) - \frac{2 X}{\sqrt\pi} e^{-X^2} \right] \vec{v}_{\rm M}`,
+
+with:
 
+:math:`X = \frac{v_{\rm{M}}}{\sqrt{2} \sigma}`, :math:`\sigma` being the radius-dependent velocity dispersion of the galaxy.
+This latter is computed using the Jeans equations, assuming a spherical component. It is provided by a polynomial fit of order 16.
+The velocity dispersion is floored to :math:`\sigma_{\rm min}`, a free parameter.
+:math:`\ln \Lambda` is the Coulomb parameter. 
+:math:`M_{\rm sat}` is the mass of the in-falling satellite on which the dynamical friction is supposed to act.
+
+To prevent very high values of the dynamical friction that can occurs at the center of the model, the acceleration is multiplied by:
+
+:math:`\rm{max} \left(0, \rm{erf}\left( 2\, \frac{ r-r_{\rm{core}} }{r_{\rm{core}}} \right) \right)`
+
+This can also mimic the decrease of the dynamical friction due to a core.
+
+
+The additional parameters for the dynamical friction are:
+
+.. code:: YAML
+
+      with_dynamical_friction: 0               # Are we running with dynamical friction ? 0 -> no, 1 -> yes
+      df_lnLambda: 5.0                         # Coulomb logarithm
+      df_sigma_floor_km_p_s : 10.0             # Minimum velocity dispersion for the velocity dispersion model
+      df_satellite_mass_in_Msun : 1.0e10       # Satellite mass in solar mass
+      df_core_radius_in_kpc: 10                # Radius below which the dynamical friction vanishes.
+      df_polyfit_coeffs00: -2.96536595e-31     # Polynomial fit coefficient for the velocity dispersion model (order 16)
+      df_polyfit_coeffs01:  8.88944631e-28     # Polynomial fit coefficient for the velocity dispersion model (order 15)
+      df_polyfit_coeffs02: -1.18280578e-24     # Polynomial fit coefficient for the velocity dispersion model (order 14)
+      df_polyfit_coeffs03:  9.29479457e-22     # Polynomial fit coefficient for the velocity dispersion model (order 13)
+      df_polyfit_coeffs04: -4.82805265e-19     # Polynomial fit coefficient for the velocity dispersion model (order 12)
+      df_polyfit_coeffs05:  1.75460211e-16     # Polynomial fit coefficient for the velocity dispersion model (order 11)
+      df_polyfit_coeffs06: -4.59976540e-14     # Polynomial fit coefficient for the velocity dispersion model (order 10)
+      df_polyfit_coeffs07:  8.83166045e-12     # Polynomial fit coefficient for the velocity dispersion model (order 9)
+      df_polyfit_coeffs08: -1.24747700e-09     # Polynomial fit coefficient for the velocity dispersion model (order 8)
+      df_polyfit_coeffs09:  1.29060404e-07     # Polynomial fit coefficient for the velocity dispersion model (order 7)
+      df_polyfit_coeffs10: -9.65315026e-06     # Polynomial fit coefficient for the velocity dispersion model (order 6)
+      df_polyfit_coeffs11:  5.10187806e-04     # Polynomial fit coefficient for the velocity dispersion model (order 5)
+      df_polyfit_coeffs12: -1.83800281e-02     # Polynomial fit coefficient for the velocity dispersion model (order 4)
+      df_polyfit_coeffs13:  4.26501444e-01     # Polynomial fit coefficient for the velocity dispersion model (order 3)
+      df_polyfit_coeffs14: -5.78038064e+00     # Polynomial fit coefficient for the velocity dispersion model (order 2)
+      df_polyfit_coeffs15:  3.57956721e+01     # Polynomial fit coefficient for the velocity dispersion model (order 1)
+      df_polyfit_coeffs16:  1.85478908e+02     # Polynomial fit coefficient for the velocity dispersion model (order 0)
+      df_timestep_mult : 0.1                   # Dimensionless pre-factor for the time-step condition for the dynamical friction force
+
+
+
+
+
+ 
+
+
+      
 How to implement your own potential
 -----------------------------------
 
diff --git a/doc/RTD/source/FriendsOfFriends/algorithm_description.rst b/doc/RTD/source/FriendsOfFriends/algorithm_description.rst
index bf7fd77fbc36f026088c317bf9b50c24c1406061..05aef12eb5d8ae052a120488abdba0505d5e0a24 100644
--- a/doc/RTD/source/FriendsOfFriends/algorithm_description.rst
+++ b/doc/RTD/source/FriendsOfFriends/algorithm_description.rst
@@ -19,8 +19,20 @@ friends (its *friends-of-friends*). This creates networks of linked particles
 which are called *groups*. The size (or length) of
 a group is the number of particles in that group. If a particle does not
 find any other particle within ``l`` then it forms its own group of
-size 1. For a given distribution of particles the resulting list of
-groups is unique and unambiguously defined.
+size 1. **For a given distribution of particles the resulting list of
+groups is unique and unambiguously defined.**
+
+In our implementation, we use three separate categories influencing their
+behaviour in the FOF code:
+
+- ``linkable`` particles which behave as described above.
+- ``attachable`` particles which can `only` form a link with the `nearest` ``linkable`` particle they find.
+- And the others which are ignored entirely.
+
+The category of each particle type is specified at run time in the parameter
+file. The classic scenario for the two categories is to run FOF on the dark
+matter particles (i.e. they are `linkable`) and then attach the gas, stars and
+black holes to their nearest DM (i.e. the baryons are `attachable`).
 
 Small groups are typically discarded, the final catalogue only contains
 objects with a length above a minimal threshold, typically of the
@@ -36,20 +48,25 @@ domain decomposition and tree structure that is created for the other
 parts of the code. The tree can be easily used to find neighbours of
 particles within the linking length.
 
-Depending on the application, the choice of linking length and
-minimal group size can vary. For cosmological applications, bound
-structures (dark matter haloes) are traditionally identified using a
-linking length expressed as :math:`0.2` of the mean inter-particle
-separation :math:`d` in the simulation which is given by :math:`d =
-\sqrt[3]{\frac{V}{N}}`, where :math:`N` is the number of particles in
-the simulation and :math:`V` is the simulation (co-moving)
-volume. Usually only dark matter particles are considered for the
-number :math:`N`. Other particle types are linked but do not
-participate in the calculation of the linking length. Experience shows
-that this produces groups that are similar to the commonly adopted
-(but much more complex) definition of virialised haloes. A minimal
-group length of :math:`32` is often adopted in order to get a robust
-catalogue of haloes and compute a good halo mass function.
+Depending on the application, the choice of linking length and minimal group
+size can vary. For cosmological applications, bound structures (dark matter
+haloes) are traditionally identified using a linking length expressed as
+:math:`0.2` of the mean inter-particle separation :math:`d` in the simulation
+which is given by :math:`d = \sqrt[3]{\frac{V}{N}}`, where :math:`N` is the
+number of particles in the simulation and :math:`V` is the simulation
+(co-moving) volume. Experience shows that this produces groups that are similar
+to the commonly adopted (but much more complex) definition of virialised
+haloes. A minimal group length of :math:`32` is often adopted in order to get a
+robust catalogue of haloes and compute a good halo mass function.  Usually only
+dark matter particles are considered for the number :math:`N`. In practice, the
+mean inter-particle separation is evaluated based on the cosmology adopted in
+the simulation.  We use: :math:`d=\sqrt[3]{\frac{m_{\rm DM}}{\Omega_{\rm cdm}
+\rho_{\rm crit}}}` for simulations with baryonic particles and
+:math:`d=\sqrt[3]{\frac{m_{\rm DM}}{(\Omega_{\rm cdm} + \Omega_{\rm b})
+\rho_{\rm crit}}}` for DMO simulations. In both cases, :math:`m_{\rm DM}` is the
+mean mass of the DM particles. Using this definition (rather than basing in on
+:math:`N`) makes the code robust to zoom-in scenarios where the entire volume is
+not filled with particles.
 
 For non-cosmological applications of the FOF algorithm, the choice of
 the linking length is more difficult and left to the user. The choice
diff --git a/doc/RTD/source/FriendsOfFriends/on_the_fly_fof.rst b/doc/RTD/source/FriendsOfFriends/on_the_fly_fof.rst
index 9961cca88366e398bb05dace29633a7293a6ee77..06eca6769bcbaf993dcbdd50e565e2c25d79d4c7 100644
--- a/doc/RTD/source/FriendsOfFriends/on_the_fly_fof.rst
+++ b/doc/RTD/source/FriendsOfFriends/on_the_fly_fof.rst
@@ -10,8 +10,9 @@ The main purpose of the on-the-fly FOF is to identify haloes during a
 cosmological simulation in order to seed some of them with black holes
 based on physical considerations.
 
-**In this mode, no group catalogue is written to the disk. The resulting list
-of haloes is only used internally by SWIFT.**
+.. warning::
+   In this mode, no group catalogue is written to the disk. The resulting list
+   of haloes is only used internally by SWIFT.
 
 Note that a catalogue can nevertheless be written after every seeding call by
 setting the optional parameter ``dump_catalogue_when_seeding``.
diff --git a/doc/RTD/source/FriendsOfFriends/parameter_description.rst b/doc/RTD/source/FriendsOfFriends/parameter_description.rst
index d9820a74d2bea768904b5fdeb35aacd01e04efd1..0e2afa97ceb7806d2851fe68ed4fc8e9d22ae082 100644
--- a/doc/RTD/source/FriendsOfFriends/parameter_description.rst
+++ b/doc/RTD/source/FriendsOfFriends/parameter_description.rst
@@ -20,6 +20,12 @@ absolute value using the parameter ``absolute_linking_length``. This is
 expressed in internal units. This value will be ignored (and the ratio of
 the mean inter-particle separation will be used) when set to ``-1``.
 
+The categories of particles are specified using the ``linking_types`` and
+``attaching_types`` arrays. They are of the length of the number of particle
+types in SWIFT (currently 7) and specify for each type using ``1`` or ``0``
+whether or not the given particle type is in this category. Types not present
+in either category are ignored entirely.
+
 The second important parameter is the minimal size of groups to retain in
 the catalogues. This is given in terms of number of particles (of all types)
 via the parameter ``min_group_size``. When analysing simulations, to
@@ -98,10 +104,12 @@ A full FOF section of the YAML parameter file looks like:
        time_first:                      0.2         # Time of first FoF black hole seeding calls.
        delta_time:                      1.005       # Time between consecutive FoF black hole seeding calls.
        min_group_size:                  256         # The minimum no. of particles required for a group.
+       linking_types:                   [0, 1, 0, 0, 0, 0, 0]  # Which particle types to consider for linking    (here only DM)
+       attaching_types:                 [1, 0, 0, 0, 1, 1, 0]  # Which particle types to consider for attaching  (here gas, stars, and BHs)
        linking_length_ratio:            0.2         # Linking length in units of the main inter-particle separation.
+       seed_black_holes_enabled:        0           # Do not seed black holes when running FOF
        black_hole_seed_halo_mass_Msun:  1.5e10      # Minimal halo mass in which to seed a black hole (in solar masses).
        dump_catalogue_when_seeding:     0           # (Optional) Write a FOF catalogue when seeding black holes. Defaults to 0 if unspecified.
        absolute_linking_length:         -1.         # (Optional) Absolute linking length (in internal units).
        group_id_default:                2147483647  # (Optional) Sets the group ID of particles in groups below the minimum size.
        group_id_offset:                 1           # (Optional) Sets the offset of group ID labelling. Defaults to 1 if unspecified.
-       seed_black_holes_enabled:        0           # Do not seed black holes when running FOF
diff --git a/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst b/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst
index d5b341409b1553dcfb4062765d83f7ba14e18570..b802ac5f412e17006719101bc45e0472fd67a8d0 100644
--- a/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst
+++ b/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst
@@ -11,17 +11,17 @@ compiled by configuring the code with the option
 ``--enable-stand-alone-fof``. The ``fof`` and ``fof_mpi`` executables
 will then be generated alongside the regular SWIFT ones.
 
-The executable takes a parameter file as an argument. It will then
-read the snapshot specified in the parameter file and extract all
-the dark matter particles by default. FOF is then run on these
-particles and a catalogue of groups is written to disk. Additional
-particle types can be read and processed by the stand-alone FOF
-code by adding any of the following runtime parameters to the
-command line:
+The executable takes a parameter file as an argument. It will then read the
+snapshot specified in the parameter file (specified as an initial condition
+file) and extract all the dark matter particles by default. FOF is then run on
+these particles and a catalogue of groups is written to disk. Additional
+particle types can be read and processed by the stand-alone FOF code by adding
+any of the following runtime parameters to the command line:
 
  * ``--hydro``: Read and process the gas particles,
  * ``--stars``: Read and process the star particles,
  * ``--black-holes``: Read and process the black hole particles,
+ * ``--sinks``: Read and process the sink particles,
  * ``--cosmology``: Consider cosmological terms.
 
 Running with cosmology is necessary when using a linking length based
@@ -34,3 +34,13 @@ internal units). The FOF code will also write a snapshot with an
 additional field for each particle. This contains the ``GroupID`` of
 each particle and can be used to find all the particles in a given
 halo and to link them to the information stored in the catalogue.
+
+The particle fields written to the snapshot can be modified using the 
+:ref:`Output_selection_label` options.
+
+.. warning::
+
+   Note that since not all particle properties are read in stand-alone
+   mode, not all particle properties will be written to the snapshot generated
+   by the stand-alone FOF.
+
diff --git a/doc/RTD/source/GettingStarted/compiling_code.rst b/doc/RTD/source/GettingStarted/compiling_code.rst
index 26a3b940888ba9103180e920ab0f623ad97bf3cc..bc08e7118fe7a318e78066b427412f9f37af92e0 100644
--- a/doc/RTD/source/GettingStarted/compiling_code.rst
+++ b/doc/RTD/source/GettingStarted/compiling_code.rst
@@ -31,10 +31,10 @@ To compile SWIFT, you will need the following libraries:
 HDF5
 ~~~~
 
-Version 1.8.x or higher is required. Input and output files are stored as HDF5
+Version 1.10.x or higher is required. Input and output files are stored as HDF5
 and are compatible with the existing GADGET-2 specification. Please consider
 using a build of parallel-HDF5, as SWIFT can leverage this when writing and
-reading snapshots. We recommend using HDF5 > 1.10.x as this is *vastly superior*
+reading snapshots. We recommend using HDF5 >= 1.12.x as this is *vastly superior*
 in parallel.
 
 HDF5 is widely available through system package managers.
@@ -105,6 +105,16 @@ GRACKLE
 GRACKLE cooling is implemented in SWIFT. If you wish to take advantage of it, you 
 will need it installed. It can be found `here <https://github.com/grackle-project/grackle>`_.
 
+.. warning::
+    (State 2023) Grackle is experiencing current development, and the API is subject
+    to changes in the future. For convenience, a frozen version is hosted as a fork
+    on github here: https://github.com/mladenivkovic/grackle-swift .
+    The version available there will be tried and tested and ensured to work with
+    SWIFT.
+
+    Additionally, that repository hosts files necessary to install that specific 
+    version of grackle with spack.
+
 
 HEALPix C library
 ~~~~~~~~~~~~~~~~~~~
diff --git a/doc/RTD/source/GettingStarted/index.rst b/doc/RTD/source/GettingStarted/index.rst
index 2086bcfb4af0ac1b7bbc24c34caa85fa1ebec498..1cf55dbac3759ba151df90186ec80ef98639590b 100644
--- a/doc/RTD/source/GettingStarted/index.rst
+++ b/doc/RTD/source/GettingStarted/index.rst
@@ -9,7 +9,7 @@ to get up and running with some examples, and then build your own initial condit
 for running.
 
 Also, you might want to consult our onboarding guide (available at
-http://www.swiftsim.com/onboarding.pdf) if you would like something to print out
+https://swift.strw.leidenuniv.nl/onboarding.pdf) if you would like something to print out
 and keep on your desk.
 
 .. toctree::
diff --git a/doc/RTD/source/HydroSchemes/adaptive_softening.rst b/doc/RTD/source/HydroSchemes/adaptive_softening.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f24cbdc87b8d6111987f4a51c34df455915cd31a
--- /dev/null
+++ b/doc/RTD/source/HydroSchemes/adaptive_softening.rst
@@ -0,0 +1,26 @@
+.. Adding Hydro Schemes
+   Matthieu Schaller, 23/02/2024
+
+.. _adaptive_softening:
+   
+Adaptive Softening
+==================
+
+.. toctree::
+   :maxdepth: 2
+   :hidden:
+   :caption: Contents:
+
+We implement the method of Price & Monaghan (2007). This add correction terms to
+the SPH equations of motions to account for the change in energy and momentum
+created by the change in gravitationl potential generated by the change of
+softening.  The softening length is tied to the gas' smoothing length and is
+thus adapting with the changes in density field.  To use adaptive softening, use
+	     
+.. code-block:: bash
+
+    ./configure --with-adaptive-softening=yes
+
+The adaptive softening scheme is implemented for all the SPH schemes above but
+only for the Wendland-C2 kernel as it is the kernel used for the gravity
+softening throughout the code.
diff --git a/doc/RTD/source/HydroSchemes/index.rst b/doc/RTD/source/HydroSchemes/index.rst
index 49635f2596dc61eab362089a6dab1e3ff16120d4..3b511e03c0d0e92c6b663050d4c85df87860316f 100644
--- a/doc/RTD/source/HydroSchemes/index.rst
+++ b/doc/RTD/source/HydroSchemes/index.rst
@@ -39,6 +39,8 @@ In case the case of a 2 loop scheme, SWIFT removes the gradient loop and the ext
    sphenix_sph
    gasoline_sph
    phantom_sph
+   adaptive_softening
    gizmo
+   shadowswift
    adding_your_own
 
diff --git a/doc/RTD/source/HydroSchemes/shadowswift.rst b/doc/RTD/source/HydroSchemes/shadowswift.rst
new file mode 100644
index 0000000000000000000000000000000000000000..40409f725ed8ac9aa63a8bb90c3d8b825f07f238
--- /dev/null
+++ b/doc/RTD/source/HydroSchemes/shadowswift.rst
@@ -0,0 +1,48 @@
+.. ShadowSWIFT (Moving mesh hydrodynamics)
+   Yolan Uyttenhove September 2023
+
+ShadowSWIFT (moving mesh hydrodynamics)
+=======================================
+
+.. warning::
+    The moving mesh hydrodynamics solver is currently in the process of being merged into master and will **NOT**
+    work on the master branch. To use it, compile the code using the ``moving_mesh`` branch.
+
+This is an implementation of the moving-mesh finite-volume method for hydrodynamics in SWIFT.
+To use this scheme, a Riemann solver is also needed. Configure SWIFT as follows:
+
+.. code-block:: bash
+
+    ./configure --with-hydro="shadowswift" --with-riemann-solver="hllc"
+
+
+Current status
+~~~~~~~~~~~~~~
+
+Due to the completely different task structure compared to SPH hydrodynamics, currently only a subset of the features of
+SWIFT is supported in this scheme.
+
+-   Hydrodynamics is fully supported in 1D, 2D and 3D and over MPI.
+
+-   Both self-gravity and external potentials are supported.
+
+-   Cosmological time-integration is supported.
+
+-   Cooling and chemistry are supported, with the exception of the ``GEAR_diffusion`` chemistry scheme. Metals are
+    properly according to mass fluxes.
+
+-   Choice between periodic, reflective, open, inflow and vacuum boundary conditions (for non-periodic boundary
+    conditions, the desired variant must be selected in ``const.h``). Additionally, reflective boundary conditions
+    are applied to SWIFT's boundary particles. Configure with ``--with-boundary-particles=<N>`` to use this (e.g. to
+    simulate walls).
+
+
+Caveats
+~~~~~~~
+These are currently the main limitations of the ShadowSWIFT hydro scheme:
+
+-   Unlike SPH the cells of the moving mesh must form a partition of the entire simulation volume. This means that there
+    cannot be empty SWIFT cells and vacuum must be explicitly represented by zero (or negligible) mass particles.
+-   Most other subgrid physics, most notably, star formation and stellar feedback are not supported yet.
+-   No MHD schemes are supported.
+-   No radiative-transfer schemes are supported.
diff --git a/doc/RTD/source/NewOption/index.rst b/doc/RTD/source/NewOption/index.rst
index 08f1ff04efa9508145c1f7e04d72d2f40fe22f0d..c05acd6f1d053118766482f89fae72f8271df899 100644
--- a/doc/RTD/source/NewOption/index.rst
+++ b/doc/RTD/source/NewOption/index.rst
@@ -34,3 +34,8 @@ In order to add a new scheme, you will need to:
    ``nobase_noinst_HEADERS``, add your new header files.
 
 6. Update the documentation.  Add your equations/documentation to ``doc/RTD``.
+
+.. toctree::
+   :caption: Table of Contents
+
+   sink_adding_new_scheme
diff --git a/doc/RTD/source/NewOption/sink_adding_new_scheme.rst b/doc/RTD/source/NewOption/sink_adding_new_scheme.rst
new file mode 100644
index 0000000000000000000000000000000000000000..023446e1fdab844ade130d53eedac08c19170cd1
--- /dev/null
+++ b/doc/RTD/source/NewOption/sink_adding_new_scheme.rst
@@ -0,0 +1,62 @@
+.. Adding new schemes
+   Darwin Roduit, 16 Ocotber 2024
+
+.. _new_option_sink:
+
+How to add your sink scheme
+-------------------------------
+
+Here, we provide comprehensive information to guide you in adding your sink scheme into Swift. To better understand how to add new schemes within Swift, read the general information provided on :ref:`new_option` page. 
+
+The default sink scheme is empty and gives you an idea of the minimum required fields and functions for the code to compile. The GEAR sink module has the base functions plus some extra ones for its operations. It can provide you with a working example. However, it can only work with the GEAR feedback module since it relies on IMF properties that are only located there. 
+
+As discussed in the GEAR sink :ref:`sink_GEAR_model_summary`, the physics relies on the following tasks: sink formation, gas and sink particle flagging, gas swallowing, sink swallowing and star formation. You do not need to care about the tasks, only the core functions within the sink module. However, you may need to locate where the code calls these functions. The file ``src/runner_others.c`` contains the ``runner_do_star_formation_sink()`` and ``runner_do_sink_formation()``. These functions are responsible for generating stars out of sinks and sinks from gas particles. The other general task-related functions are in ``src/runner_sinks.c``.
+
+The following presents the most essential functions you need to implement. This will give you an idea of the workload. 
+
+
+Sink formation
+~~~~~~~~~~~~~~
+
+Before forming a sink, the code loops over all gas and sink particles to gather data about its neighbourhood. This is performed in ``sink_prepare_part_sink_formation_gas_criteria()`` and ``sink_prepare_part_sink_formation_sink_criteria()`` functions. For instance, in GEAR, we compute the total potential energy, thermal energy, etc. 
+
+Then, to decide if we can turn a gas particle into a sink particle, the function ``sink_is_forming()`` is called. Before forming a sink particle, there is a call to ``sink_should_convert_to_sink()``. This function determines whether the gas particle must transform into a sink. Both functions return either 0 or 1.
+
+
+Gas-sink density interactions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first interaction task to be run for the sinks is the density task. This task updates the smoothing length for the sink particle, unless a fixed cutoff radius is being used (coming soon). It can also calculate the contributions made by neigbouring gas particles to the density, sound speed, velocity etc. at the location of the sink. Code for these interactions should be added to ``sink_iact.h/runner_iact_nonsym_sinks_gas_density()``.
+
+Once the contributions of all neigbouring gas particles have been calculated, the density calculation is completed by the sink density ghost task. You can set what this task does with the functions ``sink_end_density()`` and ``sink_prepare_swallow()`` in ``sink.h``.
+
+The ``sink_end_density()`` function completes the calculation of the smoothing length (coming soon), and this is where you can finish density-based calculations by e.g. dividing mass-weighted contributions to the velocity field by the total density in the kernel. For examples of this, see the equivalent task for the black hole particles.
+
+The ``sink_prepare_swallow()`` task is where you can calculate density-based quantities that you might need to use in swallowing interactions later. For example, a Bondi-Hoyle accretion prescription should calculate an accretion rate and target mass to be accreted here.
+
+
+Gas and sink flagging: finding whom to eat
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before accreting the gas/sink particles, the sink needs to look for eligible particles. The gas swallow interactions are performed within ``runner_iact_nonsym_sinks_gas_swallow()`` and the sink swallow in ``runner_iact_nonsym_sinks_sink_swallow()``.
+
+
+Gas and sink swallowing: updating the sink properties
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When the sink swallows gas particles, it updates its internal properties based on the gas particles' properties. The ``sink_swallow_part()`` function takes care of this.
+
+Similarly, when the sink swallows sink particles, it updates its properties from the to-be-swallowed sink particles. The ``sink_swallow_sink()`` performs the update.
+
+There is no more than that. The code properly removes the swallowed particles. 
+
+Star formation
+~~~~~~~~~~~~~~
+
+The most important function is ``sink_spawn_star()``. It controls whether the code should continue creating stars and must return 0 or 1.
+
+The ``sink_copy_properties_to_star()`` does what it says. This function is also responsible for adequately initialising the stars' properties. In GEAR, we give new positions and velocities within this function. 
+
+The following three functions allow you to update your sink particles (e.g. their masses) before, during and after the star formation loop: ``sink_update_sink_properties_before_star_formation()``, ``sink_update_sink_properties_during_star_formation()`` and ``sink_update_sink_properties_after_star_formation()``.
+
+These functions are located in ``sink/Default/sink.h``
diff --git a/doc/RTD/source/ParameterFiles/lossy_filters.rst b/doc/RTD/source/ParameterFiles/lossy_filters.rst
index 61915e990d190db75dce5d69f1b88e7a2e9fa5f0..1d259893cbe4d1f790f1ed61b990569de7a6b35f 100644
--- a/doc/RTD/source/ParameterFiles/lossy_filters.rst
+++ b/doc/RTD/source/ParameterFiles/lossy_filters.rst
@@ -15,11 +15,12 @@ instead of, the lossless gzip compression filter.
 **These compression filters are lossy, meaning that they modify the
 data written to disk**
 
-*The filters will reduce the accuracy of the data stored. No check is
-made inside SWIFT to verify that the applied filters make sense. Poor
-choices can lead to all the values of a given array reduced to 0, Inf,
-or to have lost too much accuracy to be useful. The onus is entirely
-on the user to choose wisely how they want to compress their data.*
+.. warning::
+   The filters will reduce the accuracy of the data stored. No check is
+   made inside SWIFT to verify that the applied filters make sense. Poor
+   choices can lead to all the values of a given array reduced to 0, Inf,
+   or to have lost too much accuracy to be useful. The onus is entirely
+   on the user to choose wisely how they want to compress their data.
 
 The filters are not applied when using parallel-hdf5.
 
@@ -27,6 +28,14 @@ The name of any filter applied is carried by each individual field in
 the snapshot using the meta-data attribute ``Lossy compression
 filter``.
 
+.. warning::
+   Starting with HDF5 version 1.14.4, filters which compress the data
+   by more than 2x are flagged as problematic (see their
+   `doc <https://docs.hdfgroup.org/hdf5/v1_14/group___f_a_p_l.html#gafa8e677af3200e155e9208522f8e05c0>`_
+   ).  SWIFT can nevertheless write files with them by setting the
+   appropriate file-level flags. However, some tools (such as
+   ``h5py``) may *not* be able to read these fields.
+
 The available filters are listed below.
 
 N-bit filters for long long integers
@@ -171,6 +180,8 @@ Same for a ``double`` (64 bits) output:
 +=================+==============+==============+=============+===================================================+===================+
 | No filter       | 52           | 11           | 15.9 digits | :math:`[2.2\times 10^{-308}, 1.8\times 10^{308}]` | ---               |
 +-----------------+--------------+--------------+-------------+---------------------------------------------------+-------------------+
+| ``DMantissa21`` | 21           | 11           | 6.62 digits | :math:`[2.2\times 10^{-308}, 1.8\times 10^{308}]` | 1.93x             |
++-----------------+--------------+--------------+-------------+---------------------------------------------------+-------------------+
 | ``DMantissa13`` | 13           | 11           | 4.21 digits | :math:`[2.2\times 10^{-308}, 1.8\times 10^{308}]` | 2.56x             |
 +-----------------+--------------+--------------+-------------+---------------------------------------------------+-------------------+
 | ``DMantissa9``  | 9            | 11           | 3.01 digits | :math:`[2.2\times 10^{-308}, 1.8\times 10^{308}]` | 3.05x             |
diff --git a/doc/RTD/source/ParameterFiles/output_selection.rst b/doc/RTD/source/ParameterFiles/output_selection.rst
index fb4e3a2a3375092ab7a475e20b8d46b709004997..7da0babfcb18007f3535eaf5f4ff7616f5156b81 100644
--- a/doc/RTD/source/ParameterFiles/output_selection.rst
+++ b/doc/RTD/source/ParameterFiles/output_selection.rst
@@ -74,6 +74,9 @@ CGS. Entries in the file look like:
     SmoothingLengths_Gas: on  # Co-moving smoothing lengths (FWHM of the kernel) of the particles : a U_L  [ cm ]
     ...
 
+This can also be used to set the outputs produced by the 
+:ref:`fof_stand_alone_label`.
+
 For cosmological simulations, users can optionally add the ``--cosmology`` flag
 to generate the field names appropriate for such a run.
 
diff --git a/doc/RTD/source/ParameterFiles/parameter_description.rst b/doc/RTD/source/ParameterFiles/parameter_description.rst
index d8a070462bccabcf5c71ebe1f24cdaab843c323d..5827427469aa2766ae3d87a5d98d3fb36b1aa97f 100644
--- a/doc/RTD/source/ParameterFiles/parameter_description.rst
+++ b/doc/RTD/source/ParameterFiles/parameter_description.rst
@@ -466,7 +466,10 @@ can be either drawn randomly by setting the parameter ``generate_random_ids``
 newly generated IDs do not clash with any other pre-existing particle. If this
 option is set to :math:`0` (the default setting) then the new IDs are created in
 increasing order from the maximal pre-existing value in the simulation, hence
-preventing any clash.
+preventing any clash. Finally, if the option
+``particle_splitting_log_extra_splits`` is set, the code will log all the splits
+that go beyond the maximal allowed (typically 64) in a file so that the split tree
+for these particles can still be reconstructed.
 
 The final set of parameters in this section determine the initial and minimum
 temperatures of the particles.
@@ -503,7 +506,7 @@ The full section to start a typical cosmological run would be:
      minimal_temperature:                100   # U_T
      H_mass_fraction:                    0.755
      H_ionization_temperature:           1e4   # U_T
-     particle_splitting:                 1 
+     particle_splitting:                 1
      particle_splitting_mass_threshold:  5e-3  # U_M
 
 .. _Parameters_Stars:
@@ -514,7 +517,7 @@ Stars
 The ``Stars`` section is used to set parameters that describe the Stars
 calculations when doing feedback or enrichment. Note that if stars only act
 gravitationally (i.e. SWIFT is run *without* ``--feedback``) no parameters
-in this section are used. 
+in this section are used.
 
 The first four parameters are related to the neighbour search:
 
@@ -576,6 +579,26 @@ specified, SWIFT will start and use the birth times specified in the
 ICs. If no values are given in the ICs, the stars' birth times will be
 zeroed, which can cause issues depending on the type of run performed.
 
+.. _Parameters_Sinks:
+
+Sinks
+-----
+
+Currently, there are two models for the sink particles, the Default model and the GEAR one. Their parameters are described below. To choose a model, configure the code with ``--with-sink=<model>``, where ``<model>`` can be ``none`` or ``GEAR``. To run with sink particles, add the option ``--sinks``.
+Below you will find the description of the ``none`` which is the default model. For ``GEAR`` model, please refer to :ref:`sink_GEAR_model`.
+
+By default, the code is configured with ``--with-sink=none``. Then, the ``DefaultSink`` section is used to set parameters that describe the sinks in this model. The unique parameter is the sink accretion radius (also called cut-off radius): ``cut_off_radius``.
+
+Note that this model does not create sink particles or accrete gas.
+
+The full section is:
+
+.. code:: YAML
+
+  DefaultSink:
+    cut_off_radius:        1e-3       # Cut off radius of the sink particles (in internal units). This parameter should be adapted with the resolution..
+
+
 .. _Parameters_time_integration:
 
 Time Integration
@@ -623,7 +646,7 @@ the start and end times or scale factors from the parameter file.
   ``max_dt_RMS_factor`` (default: ``0.25``)
 * Whether or not only the gas particle masses should be considered for
   the baryon component of the calculation: ``dt_RMS_use_gas_only`` (default: ``0``)
-  
+
 These values rarely need altering. The second parameter is only
 meaningful if a subgrid model produces star (or other) particles with
 masses substantially smaller than the gas masses. See the theory
@@ -648,7 +671,7 @@ Whilst for a cosmological run, one would need:
     dt_min:              1e-10
     max_dt_RMS_factor:   0.25     # Default optional value
     dt_RMS_use_gas_only: 0        # Default optional value
-    
+
 .. _Parameters_ICs:
 
 Initial Conditions
@@ -693,7 +716,7 @@ Finally, SWIFT also offers these options:
 * Whether to replicate the box along each axis: ``replicate`` (default: ``1``).
 * Whether to re-map the IDs to the range ``[0, N]`` and hence discard
   the original IDs from the IC file: ``remap_ids`` (default: ``0``).
-  
+
 The shift is expressed in internal units and will be written to the header of
 the snapshots. The option to replicate the box is especially useful for
 weak-scaling tests. When set to an integer >1, the box size is multiplied by
@@ -858,24 +881,42 @@ that can be used as if it was a non-distributed snapshot. In this case, the
 HDF5 library itself can figure out which file is needed when manipulating the
 snapshot.
 
-On Lustre filesystems [#f4]_ it is important to properly stripe files to achieve
-a good writing speed. If the parameter ``lustre_OST_count`` is set to the number
-of OSTs present on the system, then SWIFT will set the `stripe count` of each
-distributed file to `1` and set each file's `stripe index` to the MPI rank
-generating it modulo the OST count [#f5]_. If the parameter is not set then the
-files will be created with the default system policy (or whatever was set for
-the directory where the files are written). This parameter has no effect on
-non-Lustre file systems and no effect if distributed snapshots are not used.
-
-* The number of Lustre OSTs to distribute the single-striped distributed
-  snapshot files over: ``lustre_OST_count`` (default: ``0``)
-
+On Lustre filesystems [#f4]_ it is important to properly stripe files to
+achieve a good writing and reading speed. If the parameter
+``lustre_OST_checks`` is set and the lustre API is available SWIFT will
+determine the number of OSTs available and rank these by free space, it will
+then set the `stripe count` of each file to `1` and choose an OST
+`offset` so each rank writes to a different OST, unless there are more ranks
+than OSTs in which case the assignment wraps. In this way OSTs should be
+filled evenly and written to using an optimal access pattern.
+
+If the parameter is not set then the files will be created with the default
+system policy (or whatever was set for the directory where the files are
+written). This parameter has no effect on non-Lustre file systems.
+
+Other parameters are also provided to handle the cases when individual OSTs do
+not have sufficient free space to write a file: ``lustre_OST_free`` and
+when OSTs are closed for administrative reasons: ``lustre_OST_test``, in which
+case they cannot be written. This is important as the `offset` assignment in
+this case is not used by lustre which picks the next writable OST, so in our
+scheme such OSTs will be used more times than we intended.
+
+* Use the lustre API to assign a stripe and offset to the distributed snapshot
+  files:
+  ``lustre_OST_checks`` (default: ``0``)
+
+* Do not use OSTs that do not have a certain amount of free space in MiB.
+  Zero disables and -1 activates a guess based on the size of the process:
+  ``lustre_OST_free`` (default: ``0``)
+
+* Check OSTs can be written to and remove those from consideration:
+  ``lustre_OST_test`` (default: ``0``)
 
 Users can optionally ask to randomly sub-sample the particles in the snapshots.
 This is specified for each particle type individually:
 
-* Whether to switch on sub-sampling: ``subsample``   
-* Whether to switch on sub-sampling: ``subsample_fraction`` 
+* Whether to switch on sub-sampling: ``subsample``
+* Whether to switch on sub-sampling: ``subsample_fraction``
 
 These are arrays of 7 elements defaulting to seven 0s if left unspecified. Each
 entry corresponds to the particle type used in the initial conditions and
@@ -885,7 +926,7 @@ indicating the fraction of particles to keep in the outputs.  Note that the
 selection of particles is selected randomly for each individual
 snapshot. Particles can hence not be traced back from output to output when this
 is switched on.
-  
+
 Users can optionally specify the level of compression used by the HDF5 library
 using the parameter:
 
@@ -1103,7 +1144,7 @@ output field will be enabled. If this is 0 lossy compression is not applied.
 * Whether to use lossless compression in the particle outputs: ``particles_gzip_level``
 
 If this is non-zero the HDF5 deflate filter will be applied to lightcone particle output with
-the compression level set to the specified value. 
+the compression level set to the specified value.
 
 * HEALPix map resolution: ``nside``
 
@@ -1144,7 +1185,7 @@ filter names. Set the filter name to ``on`` to disable compression.
 * Whether to use lossless compression in the HEALPix map outputs: ``maps_gzip_level``
 
 If this is non-zero the HDF5 deflate filter will be applied to the lightcone map output with
-the compression level set to the specified value. 
+the compression level set to the specified value.
 
 The following shows a full set of light cone parameters for the case where we're making two
 light cones which only differ in the location of the observer:
@@ -1158,7 +1199,7 @@ light cones which only differ in the location of the observer:
     buffer_chunk_size:      100000
     max_particles_buffered: 1000000
     hdf5_chunk_size:        10000
- 
+
     # Redshift ranges for particle types
     z_range_for_Gas:           [0.0, 0.05]
     z_range_for_DM:            [0.0, 0.05]
@@ -1166,7 +1207,7 @@ light cones which only differ in the location of the observer:
     z_range_for_Stars:         [0.0, 0.05]
     z_range_for_BH:            [0.0, 0.05]
     z_range_for_Neutrino:      [0.0, 0.05]
-    
+
     # Healpix map parameters
     nside:                512
     radius_file:          ./shell_radii.txt
@@ -1191,7 +1232,7 @@ light cones which only differ in the location of the observer:
     enabled:  1
     basename: lightcone1
     observer_position: [74.2, 10.80, 53.59]
-  
+
 
 An example of the radius file::
 
@@ -1217,11 +1258,11 @@ Equation of State (EoS)
 
 The ``EoS`` section contains options for the equations of state.
 Multiple EoS can be used for :ref:`planetary`,
-see :ref:`planetary_eos` for more information. 
+see :ref:`planetary_eos` for more information.
 
 To enable one or multiple EoS, the corresponding ``planetary_use_*:``
 flag(s) must be set to ``1`` in the parameter file for a simulation,
-along with the path to any table files, which are set by the 
+along with the path to any table files, which are set by the
 ``planetary_*_table_file:`` parameters.
 
 For the (non-planetary) isothermal EoS, the ``isothermal_internal_energy:``
@@ -1230,7 +1271,9 @@ parameter sets the thermal energy per unit mass.
 .. code:: YAML
 
    EoS:
-     isothermal_internal_energy: 20.26784  # Thermal energy per unit mass for the case of isothermal equation of state (in internal units).
+     isothermal_internal_energy: 20.26784      # Thermal energy per unit mass for the case of isothermal equation of state (in internal units).
+     barotropic_vacuum_sound_speed: 2e4        # Vacuum sound speed in the case of the barotropic equation of state (in internal units).
+     barotropic_core_density:       1e-13      # Core density in the case of the barotropic equation of state (in internal units).
      # Select which planetary EoS material(s) to enable for use.
      planetary_use_idg_def:    0               # Default ideal gas, material ID 0
      planetary_use_Til_iron:       1           # Tillotson iron, material ID 100
@@ -1278,6 +1321,7 @@ The options are:
  * The number of grid foldings to use: ``num_folds``.
  * The factor by which to fold at each iteration: ``fold_factor`` (default: 4)
  * The order of the window function: ``window_order`` (default: 3)
+ * Whether or not to correct the placement of the centre of the k-bins for small k values: ``shift_centre_small_k_bins`` (default: 1)
 
 The window order sets the way the particle properties get assigned to the mesh.
 Order 1 corresponds to the nearest-grid-point (NGP), order 2 to cloud-in-cell
@@ -1317,6 +1361,20 @@ are mutually exclusive. The particles selected in each half are different in
 each output. Note that neutrino PS can only be computed when neutrinos are
 simulated using particles.
 
+SWIFT uses bins of integer :math:`k`, with bins :math:`[0.5,1.5]`, :math:`[1.5,2.5]` etc.  The
+representative :math:`k` values used to be assigned to the bin centres (so k=1, 2, etc), which are
+then transformed to physical :math:`k` by a factor :math:`kL/(2*pi)`. For the first few bins, only
+few modes contribute to each bin. It is then advantageous to move the "centre" of the bin to the
+actual location correponding to the mean of the contributing modes. The :math:`k` label of the bin
+is thus shifted by a small amount. The way to calculate these shifts is to consider a 3D cube of
+:math:`(kx,ky,kz)` cells and check which cells fall inside a spherical shell with boundaries
+:math:`(i+0.5,i+1.5)`, then calculate the average :math:`k=\sqrt{kx^2+ky^2+kz^2}`. So for
+:math:`i=0` there cells :math:`k=1` and 12 cells :math:`k=\sqrt(2)`, so the weighted k becomes
+:math:`(6 * 1 + 12 * \sqrt(2)) / 18 = 1.2761424`. Note that only the first 7 (22) bins require a
+correction larger than 1 (0.1) percent. We apply a correction to the first 128 terms. This
+correction is activated when ``shift_centre_small_k_bins`` is switched on (that's the default
+behaviour).
+
 An example of a valid power-spectrum section of the parameter file looks like:
 
 .. code:: YAML
@@ -1390,17 +1448,35 @@ the MPI-rank. SWIFT writes one file per MPI rank. If the ``save`` option has
 been activated, the previous set of restart files will be named
 ``basename_000000.rst.prev``.
 
-On Lustre filesystems [#f4]_ it is important to properly stripe files to achieve
-a good writing and reading speed. If the parameter ``lustre_OST_count`` is set
-to the number of OSTs present on the system, then SWIFT will set the `stripe
-count` of each restart file to `1` and set each file's `stripe index` to the MPI
-rank generating it modulo the OST count [#f5]_. If the parameter is not set then
-the files will be created with the default system policy (or whatever was set
-for the directory where the files are written). This parameter has no effect on
-non-Lustre file systems.
+On Lustre filesystems [#f4]_ it is important to properly stripe files to
+achieve a good writing and reading speed. If the parameter
+``lustre_OST_checks`` is set and the lustre API is available SWIFT will
+determine the number of OSTs available and rank these by free space, it will
+then set the `stripe count` of each restart file to `1` and choose an OST
+`offset` so each rank writes to a different OST, unless there are more ranks
+than OSTs in which case the assignment wraps. In this way OSTs should be
+filled evenly and written to using an optimal access pattern.
 
-* The number of Lustre OSTs to distribute the single-striped restart files over:
-  ``lustre_OST_count`` (default: ``0``)
+If the parameter is not set then the files will be created with the default
+system policy (or whatever was set for the directory where the files are
+written). This parameter has no effect on non-Lustre file systems.
+
+Other parameters are also provided to handle the cases when individual OSTs do
+not have sufficient free space to write a restart file: ``lustre_OST_free`` and
+when OSTs are closed for administrative reasons: ``lustre_OST_test``, in which
+case they cannot be written. This is important as the `offset` assignment in
+this case is not used by lustre which picks the next writable OST, so in our
+scheme such OSTs will be used more times than we intended.
+
+* Use the lustre API to assign a stripe and offset to restart files:
+  ``lustre_OST_checks`` (default: ``0``)
+
+* Do not use OSTs that do not have a certain amount of free space in MiB.
+  Zero disables and -1 activates a guess based on the size of the process:
+  ``lustre_OST_free`` (default: ``0``)
+
+* Check OSTs can be written to and remove those from consideration:
+  ``lustre_OST_test`` (default: ``0``)
 
 SWIFT can also be stopped by creating an empty file called ``stop`` in the
 directory where the restart files are written (i.e. the directory speicified by
@@ -1808,11 +1884,11 @@ necessary and one would use:
     invoke_stf:        1                              # We want VELOCIraptor to be called when snapshots are dumped.
     # ...
     # Rest of the snapshots properties
-	  
+
   StructureFinding:
     config_file_name:  my_stf_configuration_file.cfg  # See the VELOCIraptor manual for the content of this file.
     basename:          ./haloes/                      # Write the catalogs in this sub-directory
-     
+
 If one additionally want to call VELOCIraptor at times not linked with
 snapshots, the additional parameters need to be supplied.
 
@@ -1892,7 +1968,7 @@ Fermi-Dirac momenta will be generated if ``generate_ics`` is used. The
 ``use_delta_f``. Finally, a random seed for the Fermi-Dirac momenta can
 be set with ``neutrino_seed``.
 
-For mode details on the neutrino implementation, refer to :ref:`Neutrinos`. 
+For mode details on the neutrino implementation, refer to :ref:`Neutrinos`.
 A complete specification of the model looks like
 
 .. code:: YAML
@@ -1904,7 +1980,7 @@ A complete specification of the model looks like
 
 
 ------------------------
-    
+
 .. [#f1] The thorough reader (or overly keen SWIFT tester) would find  that the speed of light is :math:`c=1.8026\times10^{12}\,\rm{fur}\,\rm{ftn}^{-1}`, Newton's constant becomes :math:`G_N=4.896735\times10^{-4}~\rm{fur}^3\,\rm{fir}^{-1}\,\rm{ftn}^{-2}` and Planck's constant turns into :math:`h=4.851453\times 10^{-34}~\rm{fur}^2\,\rm{fir}\,\rm{ftn}^{-1}`.
 
 
diff --git a/doc/RTD/source/Planetary/equations_of_state.rst b/doc/RTD/source/Planetary/equations_of_state.rst
index cb6d5dea60c90a5cbedb4b45d86e38805b843a77..37b175c53737288374fe1a4afec07a280c3bbd53 100644
--- a/doc/RTD/source/Planetary/equations_of_state.rst
+++ b/doc/RTD/source/Planetary/equations_of_state.rst
@@ -13,6 +13,8 @@ Every SPH particle then requires and carries the additional ``MaterialID`` flag
 from the initial conditions file. This flag indicates the particle's material
 and which EoS it should use.
 
+If you have another EoS that you would like us to add, then just let us know!
+
 It is important to check that the EoS you use are appropriate
 for the conditions in the simulation that you run.
 Please follow the original sources of these EoS for more information and
@@ -24,15 +26,18 @@ So far, we have implemented several Tillotson, ANEOS, SESAME,
 and Hubbard \& MacFarlane (1980) materials, with more on the way.
 Custom materials in SESAME-style tables can also be provided.
 The material's ID is set by a somewhat arbitrary base type ID
-(multiplied by 100) plus an individual value:
+(multiplied by 100) plus an individual value, matching our code for making
+planetary initial conditions, `WoMa  <https://github.com/srbonilla/WoMa>`_:
 
 + Ideal gas: ``0``
-    + Default (\\(\\gamma\\) set using ``--with-adiabatic-index``, default 5/3): ``0``
+    + Default (Set :math:`\gamma` using ``--with-adiabatic-index``, default 5/3): ``0``
 + Tillotson (Melosh, 2007): ``1``
     + Iron: ``100``
     + Granite: ``101``
     + Water: ``102``
     + Basalt: ``103``
+    + Ice: ``104``
+    + Custom user-provided parameters: ``190``, ``191``, ..., ``199``
 + Hubbard \& MacFarlane (1980): ``2``
     + Hydrogen-helium atmosphere: ``200``
     + Ice H20-CH4-NH3 mix: ``201``
@@ -42,6 +47,10 @@ The material's ID is set by a somewhat arbitrary base type ID
     + Basalt (7530): ``301``
     + Water (7154): ``302``
     + Senft \& Stewart (2008) water: ``303``
+    + AQUA, Haldemann, J. et al. (2020) water: ``304``
+    + Chabrier, G. et al. (2019) Hydrogen: ``305``
+    + Chabrier, G. et al. (2019) Helium: ``306``
+    + Chabrier & Debras (2021) H/He mixture Y=0.245 (Jupiter): ``307``
 + ANEOS (in SESAME-style tables): ``4``
     + Forsterite (Stewart et al. 2019): ``400``
     + Iron (Stewart, zenodo.org/record/3866507): ``401``
@@ -53,7 +62,7 @@ The data files for the tabulated EoS can be downloaded using
 the ``examples/Planetary/EoSTables/get_eos_tables.sh`` script.
 
 To enable one or multiple EoS, the corresponding ``planetary_use_*:``
-flag(s) must be set to ``1`` in the parameter file for a simulation,
+flag(s) must be set from ``0`` to ``1`` in the parameter file for a simulation,
 along with the path to any table files, which are set by the
 ``planetary_*_table_file:`` parameters,
 as detailed in :ref:`Parameters_eos` and ``examples/parameter_example.yml``.
@@ -61,18 +70,18 @@ as detailed in :ref:`Parameters_eos` and ``examples/parameter_example.yml``.
 Unlike the EoS for an ideal or isothermal gas, these more complicated materials
 do not always include transformations between the internal energy,
 temperature, and entropy. At the moment, we have implemented
-\\(P(\\rho, u)\\) and \\(c_s(\\rho, u)\\) (and more in some cases),
+:math:`P(\rho, u)` and :math:`c_s(\rho, u)` (and more in some cases),
 which is sufficient for the :ref:`planetary_sph` hydro scheme,
 but some materials may thus currently be incompatible with
 e.g. entropy-based schemes.
 
 The Tillotson sound speed was derived using
-\\(c_s^2 = \\left. ( \\partial P / \\partial \\rho ) \\right|_S \\)
+:math:`c_s^2 = \left. ( \partial P / \partial \rho ) \right|_S`
 as described in
 `Kegerreis et al. (2019)  <https://doi.org/10.1093/mnras/stz1606>`_.
 Note that there is a typo in the sign of
-\\(du = T dS - P dV = T dS + (P / \\rho^2) d\\rho \\) in the appendix;
-the correct version was used in the actual derivation.
+:math:`du = T dS - P dV = T dS + (P / \rho^2) d\rho` in the appendix,
+but the correct version was used in the actual derivation.
 
 The ideal gas uses the same equations detailed in :ref:`equation_of_state`.
 
diff --git a/doc/RTD/source/Planetary/index.rst b/doc/RTD/source/Planetary/index.rst
index b851bc51144702511c31aea9f89c58e3e15ea3fa..15e2daedd9d53e22ac645b2e56b29f555b6af7cf 100644
--- a/doc/RTD/source/Planetary/index.rst
+++ b/doc/RTD/source/Planetary/index.rst
@@ -7,21 +7,23 @@ Planetary Simulations
 =====================
 
 SWIFT is also designed for running planetary simulations
-such as of giant impacts, as introduced in Kegerreis+2019,
-and any other types of simulations with more complicated equations of state
-and/or multiple materials, etc.
+such as of giant impacts, as introduced in
+`Kegerreis et al. (2019)  <https://doi.org/10.1093/mnras/stz1606>`_,
+and any other non-planetary simulations with more complicated equations
+of state and/or multiple materials, etc.
 
 Active development is ongoing of more features and examples,
 so please let us know if you are interested in using SWIFT
 or have any implementation requests.
 
-You can find an example simulation in ``swiftsim/examples/Planetary/``
-under ``EarthImpact/``, as well as some hydro tests for comparison with other
-schemes. The tabulated equation of state files can be downloaded using
+You can find an example of creating initial conditions and running an impact
+simulation in ``swiftsim/examples/Planetary/`` under ``DemoImpactInitCond/``
+and ``DemoImpact/``. Check out their `README.md` files for more info.
+The tabulated equation of state files can be downloaded there using
 ``EoSTables/get_eos_tables.sh``.
 
-Planetary simulations are currently intended to be run with
-SWIFT configured to use the planetary hydrodynamics scheme and equations of state:
+To run planetary or similar simulations, SWIFT should be configured to use
+the planetary hydrodynamics scheme and equations of state:
 ``--with-hydro=planetary`` and ``--with-equation-of-state=planetary``.
 These allow for multiple materials to be used,
 chosen from the several available equations of state.
diff --git a/doc/RTD/source/RadiativeTransfer/RT_general.rst b/doc/RTD/source/RadiativeTransfer/RT_general.rst
new file mode 100644
index 0000000000000000000000000000000000000000..46c610863755c04c534c87cddd1982ab1249230b
--- /dev/null
+++ b/doc/RTD/source/RadiativeTransfer/RT_general.rst
@@ -0,0 +1,246 @@
+.. Radiative Transfer Scheme Requirements
+    Mladen Ivkovic 05.2021
+
+.. _rt_general:
+
+
+.. warning::
+    The radiative transfer schemes are still in development and are not useable
+    at this moment. This page is currently a placeholder to document new
+    features and requirements as the code grows.
+
+
+
+
+Radiative Transfer in SWIFT
+---------------------------
+
+We currently support two methods for radiative transfer in SWIFT:
+:ref:`GEAR-RT <rt_GEAR>` and :ref:`SPHM1RT <rt_SPHM1>`. They are both
+moment-based methods. For documentation on each of these methods, please
+refer to their respective pages.
+
+This page contains some general documentation on running SWIFT with RT, which
+is valid irrespective of the exact method used.
+
+
+
+
+
+Requirements
+~~~~~~~~~~~~
+
+
+To be able to run with radiative transfer, you'll need to run swift with the
+following flags:
+
+.. code::
+
+    swift --radiation --hydro --feedback --stars --self-gravity [and/or] --external-gravity
+
+
+
+Some notes on these runtime flags:
+
+
+- The radiation data is coupled to the gas data, so you can't run without
+  ``--hydro``. (Also, the whole point of these schemes is the interaction between
+  gas and radiation, so why would you want to?)
+
+- Currently the only source of radiation are stars, so you need ``--stars``.
+  If you want to run without radiative sources, still run the code with
+  ``--stars``, even if you don't have any stars in your initial conditions.
+
+- Running with ``--stars`` requires some form of gravity, be it self-gravity or
+  external gravity. Since we need stars, we inherit this requirement. If you want
+  no gravity, run with ``--external-gravity`` and set the external potential to
+  zero.
+
+- We need ``--feedback`` in order to have meaningful smoothing lengths for
+  stars. However, you don't need any specific feedback model; It'll work even if
+  you configured ``--with-feedback=none``.
+
+
+
+
+
+.. _rt_subcycling:
+
+RT Sub-Cycling
+~~~~~~~~~~~~~~
+
+SWIFT allows to sub-cycle the solution of radiative transfer steps (both
+photon propagation and thermochemistry) with respect to the hydrodynamics
+time steps. Basically you can tell SWIFT to run up to X radiative transfer
+steps during a single hydrodynamics step for all particles in the simulation.
+The aim is to not waste time doing unnecessary hydrodynamics updates, which
+typically allow for much higher time steps compared to radiation due to the
+propagation speed of the respective advected quantity.
+
+You will need to provide an upper limit on how many RT sub-cycles per hydro
+step you want to allow. That is governed by the
+
+.. code:: yaml
+
+   TimeIntegration:
+       max_nr_rt_subcycles: 128         # maximal number of RT sub-cycles per hydro step
+
+parameter, which is mandatory for any RT runs. To turn off sub-cycling and
+couple the radiative transfer and the hydrodynamics time steps one-to-one,
+set this parameter to either 0 or 1.
+
+Due to the discretization of individual particle time steps in time bins
+with a factor of 2 difference in time step size from a lower to a higher
+time bin, the ``max_nr_rt_subcycles`` parameter itself is required to be
+a power of 2 as well.
+
+Note that this parameter will set an upper limit to the number of sub-cycles
+per hydro step. If the ratio of hydro-to-RT time step is greater than what
+``max_nr_rt_subcycles`` allows for, then the hydro time step will be reduced
+to fit the maximal threshold. If it is smaller, the particle will simply do
+fewer sub-cycles.
+
+
+
+
+
+
+
+Reading the output of time-step information
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+When running with :ref:`sub-cycling enabled <rt_subcycling>`, additional time
+step information is written.
+
+Firstly, whenever you run with a SWIFT compiled with any RT method, it will also
+create a file called ``rtsubcycles.txt``, which  contains analogous data to the
+``timesteps.txt`` file. However, if you run without sub-cycling (i.e. with
+``TimeIntegration:max_nr_rt_subcycles`` set to ``0`` or ``1``), **then the file
+will remain empty** (save for the header information). That's because its contents
+would be redundant - all the data will be identical to what will be written in
+``timesteps.txt``.
+
+
+
+Secondly, when running with RT and sub-cycling enabled, the output to ``STDOUT``
+contains additional information. More concretely, it changes from something like this:
+
+
+.. code-block::
+
+    [00003.0] engine_dump_snapshot: Dumping snapshot at t=0.000000e+00
+    [00003.1] engine_print_stats: Saving statistics at t=0.000000e+00
+    #   Step           Time Scale-factor     Redshift      Time-step Time-bins      Updates    g-Updates    s-Updates sink-Updates    b-Updates  Wall-clock time [ms]  Props    Dead time [ms]
+           0   0.000000e+00    1.0000000    0.0000000   0.000000e+00    1   56        18000        31000        13000            0            0              2610.609    281           101.971
+           1   1.220703e-05    1.0000000    0.0000000   1.220703e-05   43   43           11           11            0            0            0                61.686      1             1.324
+           2   2.441406e-05    1.0000000    0.0000000   1.220703e-05   43   44        12685        12685            0            0            0              1043.433      0            35.461
+           3   3.662109e-05    1.0000000    0.0000000   1.220703e-05   43   43           11           11            0            0            0                51.340      1             1.628
+           4   4.882813e-05    1.0000000    0.0000000   1.220703e-05   43   45        18000        18000            0            0            0              1342.531      0            36.831
+           5   6.103516e-05    1.0000000    0.0000000   1.220703e-05   43   43           11           11            0            0            0                48.412      1             1.325
+           6   7.324219e-05    1.0000000    0.0000000   1.220703e-05   43   44        12685        12685            0            0            0              1037.307      0            34.718
+           7   8.544922e-05    1.0000000    0.0000000   1.220703e-05   43   43           11           11            0            0            0                47.791      1             1.362
+           8   9.765625e-05    1.0000000    0.0000000   1.220703e-05   43   46        18000        18004            4            0            0              1410.851      0            35.005
+           9   1.098633e-04    1.0000000    0.0000000   1.220703e-05   43   43           11           11            0            0            0                48.322      1             1.327
+          10   1.220703e-04    1.0000000    0.0000000   1.220703e-05   43   44        12685        12685            0            0            0              1109.944      0            33.691
+
+
+To something like this:
+
+
+.. code-block::
+
+     [rt-sc] 0    0.000000e+00    1.000000    0.000000  1.220703e-05    1   56        18000            -            -            -            -                     -      -                 -
+     [rt-sc] 1    1.220703e-05    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.683      -             0.170
+     [rt-sc] 2    2.441406e-05    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               100.543      -             5.070
+     [rt-sc] 3    3.662109e-05    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.762      -             0.208
+     [rt-sc] 4    4.882813e-05    1.000000    0.000000  1.220703e-05   43   45        18000            -            -            -            -               124.011      -             6.396
+     [rt-sc] 5    6.103516e-05    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.831      -             0.254
+     [rt-sc] 6    7.324219e-05    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               107.172      -             5.572
+     [rt-sc] 7    8.544922e-05    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.759      -             0.227
+    [00003.0] engine_dump_snapshot: Dumping snapshot at t=0.000000e+00
+    [00003.1] engine_print_stats: Saving statistics at t=0.000000e+00
+    #   Step           Time Scale-factor     Redshift      Time-step Time-bins      Updates    g-Updates    s-Updates sink-Updates    b-Updates  Wall-clock time [ms]  Props    Dead time [ms]
+           0   0.000000e+00    1.0000000    0.0000000   0.000000e+00    1   56        18000        31000        13000            0            0              2941.254    281           120.261
+     [rt-sc] 0    9.765625e-05    1.000000    0.000000  1.220703e-05   43   46        18000            -            -            -            -                     -      -                 -
+     [rt-sc] 1    1.098633e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.990      -             0.417
+     [rt-sc] 2    1.220703e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               104.155      -             5.744
+     [rt-sc] 3    1.342773e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.765      -             0.176
+     [rt-sc] 4    1.464844e-04    1.000000    0.000000  1.220703e-05   43   45        18000            -            -            -            -               125.237      -             5.605
+     [rt-sc] 5    1.586914e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.856      -             0.282
+     [rt-sc] 6    1.708984e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               112.171      -             5.251
+     [rt-sc] 7    1.831055e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.861      -             0.241
+           1   9.765625e-05    1.0000000    0.0000000   9.765625e-05   46   46            4            8            4            0            0               546.225      1            24.648
+     [rt-sc] 0    1.953125e-04    1.000000    0.000000  1.220703e-05   43   47        18000            -            -            -            -                     -      -                 -
+     [rt-sc] 1    2.075195e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.842      -             0.212
+     [rt-sc] 2    2.197266e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               126.674      -             6.295
+     [rt-sc] 3    2.319336e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.797      -             0.289
+     [rt-sc] 4    2.441406e-04    1.000000    0.000000  1.220703e-05   43   45        18000            -            -            -            -               142.086      -             5.511
+     [rt-sc] 5    2.563477e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.919      -             0.196
+     [rt-sc] 6    2.685547e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               131.550      -             5.896
+     [rt-sc] 7    2.807617e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.809      -             0.186
+           2   1.953125e-04    1.0000000    0.0000000   9.765625e-05   46   47           27           43           16            0            0               558.226      0            27.711
+     [rt-sc] 0    2.929688e-04    1.000000    0.000000  1.220703e-05   43   46        18000            -            -            -            -                     -      -                 -
+     [rt-sc] 1    3.051758e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.738      -             0.207
+     [rt-sc] 2    3.173828e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               122.572      -             5.170
+     [rt-sc] 3    3.295898e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 1.063      -             0.345
+     [rt-sc] 4    3.417969e-04    1.000000    0.000000  1.220703e-05   43   45        18000            -            -            -            -               147.110      -             5.409
+     [rt-sc] 5    3.540039e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 1.091      -             0.350
+     [rt-sc] 6    3.662109e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               134.273      -             6.561
+     [rt-sc] 7    3.784180e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.825      -             0.298
+           3   2.929688e-04    1.0000000    0.0000000   9.765625e-05   46   46            4            8            4            0            0               557.164      0            24.760
+
+
+Here's what's going on here:
+
+- All lines beginning with the prefix ``[rt-sc]`` are time step data of RT
+  sub-cycling steps, i.e. of sub-cycles.
+- The sub-cycling index follows the prefix ``[rt-sc]``. For example, a line
+  beginning with ``[rt-sc] 2`` is the sub-cycle with index ``2`` of some time step.
+- The "sub-cycle" with index ``0`` is the one performed alongside all other tasks
+  (e.g. hydro, gravity, stellar feedback, etc.) All other sub-cycle indices
+  indicate actual sub-cycles, i.e. actual intermediate steps where only radiative
+  transfer is being solved (or put differently: where only RT tasks are being launched).
+- The sub-cycling lines are written to ``STDOUT`` *before* the line of the full
+  time step data. More precisely, in the above example, time step ``1`` with all its
+  RT sub-cycles is written to ``STDOUT`` as this block:
+
+.. code-block::
+
+    #   Step           Time Scale-factor     Redshift      Time-step Time-bins      Updates    g-Updates    s-Updates sink-Updates    b-Updates  Wall-clock time [ms]  Props    Dead time [ms]
+
+    [ ... some lines omitted ... ]
+
+     [rt-sc] 0    9.765625e-05    1.000000    0.000000  1.220703e-05   43   46        18000            -            -            -            -                     -      -                 -
+     [rt-sc] 1    1.098633e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.990      -             0.417
+     [rt-sc] 2    1.220703e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               104.155      -             5.744
+     [rt-sc] 3    1.342773e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.765      -             0.176
+     [rt-sc] 4    1.464844e-04    1.000000    0.000000  1.220703e-05   43   45        18000            -            -            -            -               125.237      -             5.605
+     [rt-sc] 5    1.586914e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.856      -             0.282
+     [rt-sc] 6    1.708984e-04    1.000000    0.000000  1.220703e-05   43   44        12685            -            -            -            -               112.171      -             5.251
+     [rt-sc] 7    1.831055e-04    1.000000    0.000000  1.220703e-05   43   43           11            -            -            -            -                 0.861      -             0.241
+           1   9.765625e-05    1.0000000    0.0000000   9.765625e-05   46   46            4            8            4            0            0               546.225      1            24.648
+
+- Let's have a closer look at the written data:
+
+  - In step ``1``, 4 hydro particles, 8 gravity particles, and 4 star particles
+    were updated. (You can see that in the last line.)
+  - The integration of the full step was performed over a time step with
+    size ``9.765625e-05``. **That is valid for all physics except radiative
+    transfer.** The RT was integrated 8 times with a time step size of ``1.220703e-05``.
+    You can see this in the sub-cycling output lines.
+  - Each RT sub-cycling line also tells you the minimal and maximal time bin
+    size that was worked on, as well as how many hydro particles underwent RT
+    updates.
+  - RT sub-cycles only ever update radiative transfer. There will never be any
+    gravity, star, sink, or black hole particle updates in it.
+  - Since the sub-cycle with index ``0`` is performed alongside all other physics
+    during the main step, the isolated wall-clock time and dead time fields are
+    not available for it.
+  - The wall-clock time and dead time fields in the full step line (the one starting
+    *without* ``[rt-sc]``) include the data of the sub-cycles for this step as well.
+
+
+
+
+
diff --git a/doc/RTD/source/RadiativeTransfer/RT_subcycling.rst b/doc/RTD/source/RadiativeTransfer/RT_subcycling.rst
deleted file mode 100644
index c8e3f546b2b57030d91e2fde0a8604be4bd8534d..0000000000000000000000000000000000000000
--- a/doc/RTD/source/RadiativeTransfer/RT_subcycling.rst
+++ /dev/null
@@ -1,45 +0,0 @@
-.. RT Subcycling
-    Mladen Ivkovic 07.2022
-
-.. _rt_subcycling:
-   
-RT Subcycling
--------------
-
-.. warning::
-    The radiative transfer schemes are still in development and are not useable
-    at this moment. This page is currently a placeholder to document new
-    features and requirements as the code grows.
-
-
-SWIFT allows to sub-cycle the solution of radiative transfer steps (both 
-photon propagation and thermochemistry) with respect to the hydrodynamics
-time steps. Basically you can tell SWIFT to run up to X radiative transfer
-steps during a single hydrodynamics step for all particles in the simulation.
-The aim is to not waste time doing unnecessary hydrodynamics updates, which
-typically allow for much higher time steps compared to radiation due to the
-propagation speed of the respective advected quantity.
-
-You will need to provide an upper limit on how many RT subcycles per hydro
-step you want to allow. That is governed by the
-
-.. code:: yaml
-
-   TimeIntegration:
-       max_nr_rt_subcycles: 128         # maximal number of RT subcycles per hydro step
-
-parameter, which is mandatory for any RT runs. To turn off subcycling and 
-couple the radiative transfer and the hydrodynamics time steps one-to-one,
-set this parameter to either 0 or 1.
-
-Due to the discretization of individual particle time steps in time bins
-with a factor of 2 difference in time step size from a lower to a higher
-time bin, the ``max_nr_rt_subcycles`` parameter itself is required to be
-a power of 2 as well.
-
-Note that this parameter will set an upper limit to the number of subcycles
-per hydro step. If the ratio of hydro-to-RT time step is greater than what
-``max_nr_rt_subcycles`` allows for, then the hydro time step will be reduced
-to fit the maximal threshold. If it is smaller, the particle will simply do 
-fewer subcycles.
-
diff --git a/doc/RTD/source/RadiativeTransfer/index.rst b/doc/RTD/source/RadiativeTransfer/index.rst
index 2f465b735f34b9bb63c5bde0a006fba4eb1441e8..e08bb9db6efd4f58d43a7e704e414a6b8ecaa25e 100644
--- a/doc/RTD/source/RadiativeTransfer/index.rst
+++ b/doc/RTD/source/RadiativeTransfer/index.rst
@@ -21,9 +21,8 @@ schemes.
    :maxdepth: 2
    :caption: Contents:
 
-   requirements
+   RT_general
    GEAR_RT
    SPHM1_RT
-   RT_subcycling
    RT_notes_for_developers
    additional_tools
diff --git a/doc/RTD/source/RadiativeTransfer/requirements.rst b/doc/RTD/source/RadiativeTransfer/requirements.rst
deleted file mode 100644
index e8da78436e4fd8872be3517d5a16d1140d472223..0000000000000000000000000000000000000000
--- a/doc/RTD/source/RadiativeTransfer/requirements.rst
+++ /dev/null
@@ -1,42 +0,0 @@
-.. Radiative Transfer Scheme Requirements
-    Mladen Ivkovic 05.2021
-
-.. _rt_requirements:
-   
-Requirements
-------------
-
-.. warning::
-    The radiative transfer schemes are still in development and are not useable
-    at this moment. This page is currently a placeholder to document new
-    features and requirements as the code grows.
-
-
-To be able to run with radiative transfer, you'll need to run swift with the
-following flags:
-
-.. code::
-
-    swift --radiation --hydro --feedback --stars --self-gravity [and/or] --external-gravity
-
-
-Some notes:
-
-- The radiation data is coupled to the gas data, so you can't run without 
-  ``--hydro``. (Also the whole point of these schemes is the interaction between
-  gas and radiation, so why would you want to?)
-
-- Currently the only source of radiation are stars, so you need ``--stars``. 
-  If you want to run without radiative sources, still run the code with
-  ``--stars``, even if you don't have any stars in your initial conditions.
-
-- Running with ``--stars`` requires some form of gravity, be it self-gravity or
-  external gravity. Since we need stars, we inherit this requirement. If you want
-  no gravity, run with ``--external-gravity`` and set the external potential to
-  zero.
-
-- We need ``--feedback`` in order to have meaningful smoothing lengths for
-  stars. However, you don't need any specific feedback model; It'll work even if
-  you configured ``--with-feedback=none``.
-
-
diff --git a/doc/RTD/source/Snapshots/index.rst b/doc/RTD/source/Snapshots/index.rst
index 19abf8b19ddd8fc08d3c6798f7d134d254c02c0c..03dc411dd2a9f05afd3ee8b78b5115a5b50f1942 100644
--- a/doc/RTD/source/Snapshots/index.rst
+++ b/doc/RTD/source/Snapshots/index.rst
@@ -249,6 +249,12 @@ obtained by running SWIFT with the ``-o`` runtime option (See
 :ref:`Output_selection_label` for details). Each field contains a short
 description attribute giving a brief summary of what the quantity represents.
 
+Note that the HDF5 names of some fields differ from the GADGET-2 format for
+initial condition files (see :ref:`Initial_Conditions_label`) that mixes
+singular and plural names, which in snapshot files are all plural by default 
+(e.g. ``InternalEnergies`` in snapshots versus ``InternalEnergy`` in initial
+conditions).
+
 All the individual arrays created by SWIFT have had the Fletcher 32 check-sum
 filter applied by the HDF5 library when writing them. This means that any
 eventual data corruption on the disks will be detected and reported by the
@@ -267,7 +273,9 @@ part designed for users to directly read and in part for machine
 reading of the information. Each field contains the exponent of the
 scale-factor, reduced Hubble constant [#f2]_ and each of the 5 base units
 that is required to convert the field values to physical CGS
-units. These fields are:
+units. The base assumption is that all fields are written in the
+co-moving frame (see below for exceptions).
+These fields are:
 
 +----------------------+---------------------------------------+
 | Meta-data field name | Description                           |
@@ -324,6 +332,12 @@ case of the densities and assuming the usual system of units
 In the case of a non-cosmological simulation, these two expressions
 are identical since :math:`a=1`.
 
+In some special cases, the fields cannot be meaningfully expressed as
+co-moving quantities. In these exceptional circumstances, we set the
+value of the attribute ``Value stored as physical`` to ``1``. And we
+additionally set the attribute ``Property can be converted to
+comoving`` to ``0``.
+
 Particle splitting metadata
 ---------------------------
 
@@ -357,12 +371,16 @@ the combination of ``ProgenitorID`` and this binary tree corresponds to a
 fully traceable, unique, identifier for every particle in the simulation volume.
 
 Note that we can only track 64 splitting events for a given particle, and after
-this the binary tree is meaningless. In practice, however, such a high number
-of splitting events is extremely unlikely to occur.
-
-An example is provided in ``examples/SubgridTests/ParticleSplitting``, with
-a figure showing how one particle is split (eventually) into 16 descendants
-that makes use of this metadata.
+this the binary tree is meaningless. In practice, however, such a high number of
+splitting events is extremely unlikely to occur. The logging of extra splits can
+optionally be activated. When particles reach 64 splits, their tree information
+is reset but the status prior to the reset is stored in a log file allowing for
+the reconstruction of the full history even in the cases where the maximum is
+reached.
+
+An example is provided in ``examples/SubgridTests/ParticleSplitting``, with a
+figure showing how one particle is split (eventually) into 16 descendants that
+makes use of this metadata.
    
 Quick access to particles via hash-tables
 -----------------------------------------
diff --git a/doc/RTD/source/SubgridModels/AGNSpinJets/index.rst b/doc/RTD/source/SubgridModels/AGNSpinJets/index.rst
index d7c83ca5117f8be7a663a7e34eaf6167e0101a11..b1f2e17e8fe56004f3cae241c16590297757572c 100644
--- a/doc/RTD/source/SubgridModels/AGNSpinJets/index.rst
+++ b/doc/RTD/source/SubgridModels/AGNSpinJets/index.rst
@@ -44,3 +44,4 @@ A full list of all relevant parameters of the model is in ``params.rst``. We als
   numerics
   params
   output
+  variable_heating_temperatures
diff --git a/doc/RTD/source/SubgridModels/AGNSpinJets/numerics.rst b/doc/RTD/source/SubgridModels/AGNSpinJets/numerics.rst
index 6d37b8aec2510b37a40629f4eb12ff61609e586f..0e9b93956125c989c80648228b511017937330f1 100644
--- a/doc/RTD/source/SubgridModels/AGNSpinJets/numerics.rst
+++ b/doc/RTD/source/SubgridModels/AGNSpinJets/numerics.rst
@@ -10,22 +10,41 @@ In order to launch jets, we introduce a jet reservoir that functions identically
 
 These kicks are handed out to particles in a symmetric way with respect to the spin vector of the BH. :math:`N_\mathrm{j}/2` particles are kicked from the 'upper' hemisphere relative to the spin vector, and the other half from the lower hemisphere. The particles to be kicked can be any in the smoothing kernel. We include four different choices: the particles kicked are: 1) the closest to the BH, 2) the farthest from the BH, 3) the ones of minimal density and 4) the ones closest to the spin axis, in terms of angular distance. Note that these sortings are done for each hemisphere seperately. 
 
-The particles chosen are always given velocities based on the same algorithm, regardless of their positions in the kernel. We perform the actual kicks in the following way. Velocity kicks are chosen at random from a cone around the current spin vector with a (half-)opening angle of :math:`\theta_\mathrm{j}`. In particular, we first choose the kick vector around the z-axis as :math:`\vec{v}_\mathrm{kick}=(\sin\theta\cos\phi,\hspace{0.3mm}\sin\theta\sin\phi,\hspace{0.3mm}\cos \theta)`. Here, :math:`\cos\theta` is chosen uniformly from the interval :math:`[\cos\theta_\mathrm{j},1]`, and :math:`\sin\theta=\sqrt{1-\cos\theta^2}`. :math:`\phi` is chosen uniformly from :math:`[0,2\pi]`. This random vector, now representing a random kick within a cone around the z-axis, is rotated into the frame of the spin vector so that the cone is pointing in the right direction. For particles being kicked from the 'negative' side of the BH hemisphere, the final kick vector is simply multiplied by :math:`-1`.
+The particles chosen are always given velocities based on the same algorithm, regardless of their positions in the kernel. We perform the actual kicks in the following way. Velocity kicks are chosen at random from a cone around the current spin vector with a (half-)opening angle of :math:`\theta_\mathrm{j}`. In particular, we first choose the kick vector around the z-axis as :math:`\hat{v}_\mathrm{kick}=(\sin\theta\cos\phi,\hspace{0.3mm}\sin\theta\sin\phi,\hspace{0.3mm}\cos \theta)`. Here, :math:`\cos\theta` is chosen uniformly from the interval :math:`[\cos\theta_\mathrm{j},1]`, and :math:`\sin\theta=\sqrt{1-\cos\theta^2}`. :math:`\phi` is chosen uniformly from :math:`[0,2\pi]`. This random vector, now representing a random kick within a cone around the z-axis, is rotated into the frame of the spin vector so that the cone is pointing in the right direction. For particles being kicked from the 'negative' side of the BH hemisphere, the final kick vector is simply multiplied by :math:`-1`.
 
-We then add the kick vector to the particle's current velocity. We do this in a way that conserves energy, so that the magnitude of the final velocity is computed from
+We increase the particle's velocity in the chosen kick direction by an amount :math:`\vert \Delta \vec{v} \vert` such that its energy increases by :math:`(1/2)m_\mathrm{gas}v_\mathrm{jet}^2`. For this reason, :math:`\vert \Delta \vec{v} \vert< v_\mathrm{jet}` generally holds. We calculate :math:`\vert \Delta \vec{v} \vert` from the equation
 
 .. math::
-    \frac{1}{2}m_\mathrm{gas}\vec{v}_\mathrm{fin}^2=\frac{1}{2}m_\mathrm{gas}\vec{v}_\mathrm{0}^2 + \frac{1}{2}m_\mathrm{gas}\vec{v}_\mathrm{kick}^2,
+    (\vec{v}_0+\Delta\vec{v})^2 = \vert \vec{v}_0\vert ^2 + v_\mathrm{j}^2,
     
-while its direction is computed from conservation of momentum:
+which follows from conservation of energy. This vector equation can be solved to yield the necessary magnitude of the velocity increase that should be applied (in the chosen kick direction)
 
 .. math::
-    m_\mathrm{gas}\vec{v}_\mathrm{fin}=m_\mathrm{gas}\vec{v}_\mathrm{0} + m_\mathrm{gas}\vec{v}_\mathrm{kick}.
+    \vert \Delta\vec{v}\vert = \sqrt{v_\mathrm{j}^2 + v_\mathrm{0,j}^2} - v_\mathrm{0,j},
+    
+where :math:`v_\mathrm{0,j} = \sin \theta\vert \vec{v}_0\vert` is the magnitude of the initial velocity projected onto the chosen kick direction, with :math:`\sin \theta` the angle between the direction of the initial velocity and the chosen kick direction.
 
 Black hole time steps
 ---------------------
 
-Black holes will generally have time steps based on their gravitational interactions, but also based on their current accretion rate and the expected time interval until which the thermal feedback reservoir (representing radiative feedback) will have grown enough to heat 1 particle. We introduce a similar time step, but based on when the jet reservoir grows enough to kick :math:`N_\mathrm{j}` particles. We then take the minimum of those two. 
+Given the changes to the BH model in the form of AGN jets and BH spin evolution, a few additional time-step criteria need to be implemented. The minimum of these time-steps is taken to actually evolve the BH, alongside the other time-steps already used for the BH in the code. We introduce a jet-related time-step that is given by:
+
+.. math::
+    \Delta t_\mathrm{jet}=\frac{\Delta E_\mathrm{jet}}{P_\mathrm{jet}}.
+
+This time-step ensures that the BH is woken up by the time it needs to 'hand out' a pair of kicks. In the above equation, :math:`P_\mathrm{jet}` is the current, instantenous jet power, while :math:`\Delta E_\mathrm{jet}=2\times m_\mathrm{ngb}v_\mathrm{jet}^2` is the energy to be handed out to a pair of particles, with :math:`m_\mathrm{ngb}` the average gas particle mass in the BH's kernel, and :math:`v_\mathrm{jet}` the target jet velocity.
+
+We also introduce two time-steps related to the angular momentum of the BH. The first of these ensures that the magnitude of spin does not change too much over a single time-step, and it is given by
+
+.. math::
+    \Delta t_\mathrm{a}=0.1\frac{\vert a\vert M_\mathrm{BH}}{s \dot{M}_\mathrm{BH,0}},
+
+where :math:`s` is the spinup/spindown function. The numerical factor :math:`0.1` quantifies how finely we want to evolve spin; it ensures that the value of spin changes no more than :math:`10` per cent (relative to the current value) over the next time-step.
+
+We also introduce a time-step related to the redirection of the spin vector. Since the spin vector may be redirected very quickly relative to its magnitude (due to LT torques), this criterion is separate to the one mentioned above. This time-step is given
+
+.. math::
+    \Delta t_\mathrm{a}=0.1\frac{M_\mathrm{warp}J_\mathrm{BH}}{\dot{M}_\mathrm{BH,0}J_\mathrm{warp}\sin\theta},
 
-On top of that, we add a time step that makes sure the BH spin doesn't get evolved too much over one time step. Its magnitude is actually not a problem, since the growth of the spin magnitude is always tied with the growth of mass. However, the direction is more problematic (especially in the thin disk case, where alignment can occur very quickly, with little mass or spin growth, due to large warp radii). For this reason, we make sure that the amount of warp angular momentum interacting with the BH over the next time-step, :math:`\Delta_J=(J_\mathrm{warp}/M_\mathrm{warp})\Delta M`, is a small fraction of the current BH angular momentum :math:`J_\mathrm{BH}` (e.g. :math:`0.01`).
+where :math:`\theta` is the angle between the current BH spin vector and the angular momentum of gas in the accretion disc on large scales. The numerical prefactor is again present to ensure a fine enough evolution of the spin vector direction. In particular, in the case that the spin vector and the gas angular momentum are perpendicular (:math:`\sin\theta=1`), this criterion will lead to a change of no more than :math:`\approx5\degree` in the spin vector direction per time-step.
 
diff --git a/doc/RTD/source/SubgridModels/AGNSpinJets/params.rst b/doc/RTD/source/SubgridModels/AGNSpinJets/params.rst
index c276229ffce96bc222c6403a1e810eb3bddd2822..5e541c79317079a136976059fcdcf760fee5bde7 100644
--- a/doc/RTD/source/SubgridModels/AGNSpinJets/params.rst
+++ b/doc/RTD/source/SubgridModels/AGNSpinJets/params.rst
@@ -46,38 +46,40 @@ Below we give an example of parameter choices applicable for e.g. a 50 Mpc box.
         AGN_use_deterministic_feedback:     1          # Deterministic (1) or stochastic (0) AGN feedback model
         AGN_feedback_model:                 Isotropic  # AGN feedback model (Isotropic or MinimumDistance)
         minimum_timestep_yr:                1000.0     # Minimum time-step of black-hole particles
-        max_eddington_fraction:             1.        # Maximal allowed accretion rate in units of the Eddington rate.
+        max_eddington_fraction:             1.         # Maximal allowed accretion rate in units of the Eddington rate.
         include_jets:                       1          # Global switch whether to include jet feedback [1] or not [0].
         turn_off_radiative_feedback:        0          # Global switch whether to turn off radiative (thermal) feedback [1] or not [0]. This should only be used if 'include_jets' is set to 1, since we want feedback in some form or another.
-        alpha_acc:                          0.1        # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between thin and thick disk, as dot(m) = 0.2 * alpha^2.
-        seed_spin:                          0.01        # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
-        AGN_jet_velocity_model:             BlackHoleMass          # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
-        v_jet_km_p_s:                       10000.     # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
-        v_jet_cs_ratio:                     10.        # This sets the jet velocity to v_jet_cs_ratio times the sound speed of the hot gas of the parent halo the black hole is in. This is used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
-        v_jet_BH_mass_scaling_reference_mass_Msun: 3.4e3 # The reference mass used in the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
-        v_jet_BH_mass_scaling_slope:        0.65       # The slope of the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
-        v_jet_mass_loading:                 400.       # The constant mass loading to use if 'AGN_jet_velocity_model' is MassLoading.
-        v_jet_xi:                           0.707       # The numerical multiplier by which the jet velocity formula is scaled, if 'AGN_jet_velocity_model' is 'Local' or 'SoundSpeed'. The appropriate values (to exactly obtain the formulas as derived) are 0.63 and 0.707 for the two, respectively.
-        v_jet_min_km_p_s:                   500        # The minimal jet velocity. This is used if  'AGN_jet_velocity_model' is 'BlackHoleMass', 'MassLoading', 'Local' or 'SoundSpeed'.
+        alpha_acc:                          0.2        # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between the thin and thick disk, as dot(m) = 0.2 * alpha^2.
+        mdot_crit_ADAF:                     0.01       # The transition normalized accretion rate (Eddington ratio) at which the disc goes from thick (low accretion rates) to thin (high accretion rates). The feedback also changes from kinetic jets to thermal isotropic, respectively.
+        seed_spin:                          0.01       # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
+        AGN_jet_velocity_model:             Constant   # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
+        v_jet_km_p_s:                       3160.      # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
+        v_jet_BH_mass_scaling_reference_mass_Msun: 1e9 # The reference mass used in the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
+        v_jet_BH_mass_scaling_slope:        0.5        # The slope of the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
+        v_jet_min_km_p_s:                   500        # The minimal jet velocity. This is used if 'AGN_jet_velocity_model' is 'BlackHoleMass', 'MassLoading' or 'Local'.
+        v_jet_max_km_p_s:                   1e4        # The maximal jet velocity. This is used if 'AGN_jet_velocity_model' is 'BlackHoleMass', 'MassLoading' or 'Local'.
         opening_angle_in_degrees:           7.5        # The half-opening angle of the jet in degrees. Should use values < 15 unless for tests.
         N_jet:                              2          # Target number of particles to kick as part of a single jet feedback event. Should be a multiple of 2 to ensure approximate momentum conservation (we always kick particles in pairs, one from each 'side' of the BH, relative to the spin vector).
-        AGN_jet_feedback_model:             MinimumDistance   # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
+        AGN_jet_feedback_model:             MinimumDistance # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
         eps_f_jet:                          1.         # Coupling efficiency for jet feedback. No reason to expect this to be less than 1.
-        fix_jet_efficiency:                 0          # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0]. If used, jets will be launched exclusively along the z axis. Should be set to 1 only for tests.
+        fix_jet_efficiency:                 0          # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0].
         jet_efficiency:                     0.1        # The constant jet efficiency used if 'fix_jet_efficiency' is set to 1.
+        fix_jet_direction:                  0          # Global switch whether to fix the jet direction to be along the z-axis, instead of along the spin vector.
+        accretion_efficiency_mode:          Variable   # How the accretion efficiencies are calculated for the thick accretion disc. If 'Constant', the value of 'accretion_efficiency_thick' will be used. If 'Variable', the accretion efficiency will scale with Eddington ratio.
+        accretion_efficiency_thick:         0.01       # The accretion efficiency (suppression factor of the accretion rate) to use in the thick disc (ADAF), to represent the effects of subgrid ADIOS winds that take away most of the mass flowing through the accretion disc.
+        accretion_efficiency_slim:          1          # The constant accretion efficiency to use in the slim disc, at super-Eddington rates.
+        ADIOS_s:                            0.5        # The exponent of the scaling between accretion efficiency and transition radius of the accretion disc, used if 'accretion_efficiency_mode' is 'Variable'.
+        ADIOS_R_in:                         1e4        # The normalisation (the value) of the transition radius of the accretion disc at the critical Eddington ratio (0.01), used if 'accretion_efficiency_mode' is 'Variable'.
         fix_radiative_efficiency:           0          # Global switch whether to fix the radiative efficiency to a particular value [1], or use a spin-dependant formula [0]. 
         radiative_efficiency:               0.1        # The constant jet efficiency used if 'fix_radiative_efficiency' is set to 1. Otherwise, this value is used to define the Eddington accretion rate.
-        TD_region:                          B          # How to treat the subgrid accretion disk if it is thin, according to the Shakura & Sunyaev (1973) model. If set to B, region b will be used. If set to C, region c will be used. 
+        TD_region:                          B          # How to treat the subgrid accretion disk if it is thin, according to the Shakura & Sunyaev (1973) model. If set to B, region b will be used. If set to C, region c will be used.
         include_GRMHD_spindown:             1          # Whether to include high jet spindown rates from GRMHD simulations [1], or use an analytical formula that assumes extraction of energy from the rotational mass/energy of the BH.
-        include_ADIOS_suppression:          0          # Whether to suppress the accretion rate in the fully thick disc regime [1] (Eddington rate below 0.2alpha^2) by the amount expected to be taken away by isotropic kinetic disk winds.
-        ADIOS_R_in:                         30.        # If include_ADIOS_accr_suppression is set to 1, this parameter controls the inner radius within which winds are not important.
-        ADIOS_s:                            0.4        # Slope of the accretion rate - radius relationship if include_ADIOS_accr_suppression is set to 1.
-        turn_off_secondary_feedback:        1          # If set to 1, there will be only radiative (thermal) feedback in the thin disk mode, and only jets in the thick disk mode.
-        jet_h_r_slope:                      1.         # The slope of the dependence of jet efficiency on aspect ratio of the subgrid accretion disk, H/R. Default value is 1, and another reasonable value is 0 (same jet efficiency for all disks). Reality could be anything in between. This parameter is only used if turn_off_secondary_feedback is set to 0.
         delta_ADAF:                         0.2        # Electron heating parameter, which controls the strength of radiative feedback in thick disks. Should be between 0.1 and 0.5. This parameter is only used if turn_off_secondary_feedback is set to 0.
-        include_slim_disk:                  0          # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
-        TD_SD_eps_r_threshold:              0.75       # Parameter controlling the transition from thin to slim disk. Accretion disk will be slim if radiative efficiency satisfies eps_slim < TD_SD_eps_r_threshold * eps_thin. This parameter is only used if include_slim_disk is set to 1.
+        include_slim_disk:                  1          # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
+        use_jets_in_thin_disc:              1          # Whether to use jets alongside radiation in the thin disc at moderate Eddington ratios.
+        use_ADIOS_winds:                    1          # Whether to include ADIOS winds in the thick disc as thermal isotropic feedback (same channel as thin disc quasar feedback, but with a different efficiency). 
+        slim_disc_wind_factor:              1          # The relative efficiency of slim disc winds at super-Eddington rates. If '1', full winds will be used, while '0' will lead to no winds. Any value in between those can also be used. The wind is implemented in the thermal isotropic feedback channel.
 
-Most of these parameters should work well generally, and should not be changed except for tests. We will discuss only some of the more important ones. You can choose whether to have only the thick and thin disk (low and high BH accretion rates, respectively), or you can also include the slim disk at super-Eddington rates with ``include_slim_disk``. You can control what type of feedback you (do not) want with ``include_jets`` and ``turn_off_radiative_feedback``. If you choose to turn off jets, everything will be modeled as a thin disk (regardless of accretion rate), since jets go hand-in-hand with the thick and the slim disk. Similarly, if you turn off radiation, everything will be treated as a thick disk.
+Most of these parameters should work well generally, and should not be changed except for tests. We will discuss only some of the more important ones. You can choose whether to have only the thick and thin disk (low and high BH accretion rates, respectively, separated by a value of ``mdot_crit_ADAF``), or you can also include the slim disk at super-Eddington rates with ``include_slim_disk``. You can control what type of feedback you (do not) want with ``include_jets`` and ``turn_off_radiative_feedback``. If you choose to turn off jets, everything will be modeled as a thin disk (regardless of accretion rate), since jets go hand-in-hand with the thick and the slim disk. Similarly, if you turn off radiation, everything will be treated as a thick disk.
 
-If you set ``use_var_v_jet:   0``, you will need to change ``v_jet``, the kicking velocity of particles, depending on what system you are simulating. You should typically choose values at least 10 times larger than the sound speed of the hot gas in your most massive haloes (e.g. 1500 km/s for a MW-type galaxy and 10 000 km/s for a :math:`10^{14}` :math:`\mathrm{M}_\odot` halo). If, on the other hand, you set ``use_var_v_jet:   1``, the launching velocities will vary on their own depending on the typical sound speed (virial velocity) of the hot gas in the haloes. You then need to set ``v_jet_cs_ratio`` to values :math:`\gg1` (10-30 works well) in order to have significant shocking.
+Turning on ``use_jets_in_thin_disc`` or ``use_ADIOS_winds`` will cause jets to also be used in the thin disk and winds (thermal isotropic feedback) in the thick disk. Similarly, ``use_ADIOS_winds`` will lead to winds in the slim disk, with the value of the parameter being used to rescale the formula that is implemented (a value of 1 leading to maximal winds).
diff --git a/doc/RTD/source/SubgridModels/AGNSpinJets/plots.py b/doc/RTD/source/SubgridModels/AGNSpinJets/plots.py
index 6c4045d07be9a771ce00a7c3021517ddf044973e..de0d7c853d2d215586b46319ac294a5869452cfb 100644
--- a/doc/RTD/source/SubgridModels/AGNSpinJets/plots.py
+++ b/doc/RTD/source/SubgridModels/AGNSpinJets/plots.py
@@ -188,6 +188,39 @@ def L_adv(x, alpha):
     ) / eta(x, alpha)
 
 
+def jet_eff(f_Edd, a):
+    horizon_ang_vel = abs(a) / (2.0 * (1.0 + np.sqrt(1 - a * a)))
+    phi = -20.2 * a ** 3 - 14.9 * a ** 2 + 34.0 * a + 52.6
+    phi = phi * (f_Edd / 1.88) ** 1.29 / (1 + (f_Edd / 1.88) ** 1.29)
+    return (
+        0.05
+        / (4.0 * np.pi)
+        * phi ** 2
+        * horizon_ang_vel ** 2
+        * (1.0 + 1.38 * horizon_ang_vel ** 2 - 9.2 * horizon_ang_vel ** 4)
+    )
+
+
+def s_HD(f_Edd, a):
+    xi = f_Edd * 0.017
+    s_min = 0.86 - 1.94 * a
+    L_ISCO = 0.385 * (1.0 + 2.0 * np.sqrt(3.0 * r_isco(a) - 2.0))
+    s_thin = L_ISCO - 2.0 * a * (1.0 - eps_NT(a))
+    return (s_thin + s_min * xi) / (1 + xi)
+
+
+def s(f_Edd, a):
+    horizon_ang_vel = abs(a) / (2.0 * (1.0 + np.sqrt(1 - a * a)))
+    k_EM = 0.23 * np.ones(np.size(a))
+    k_EM[a > 0] = np.minimum(0.1 + 0.5 * a[a > 0], 0.35 * np.ones(np.size(a[a > 0])))
+
+    s_EM = (
+        -1 * a / abs(a) * jet_eff(f_Edd, a) * (1.0 / (k_EM * horizon_ang_vel) - 2.0 * a)
+    )
+
+    return s_HD(f_Edd, a) + s_EM
+
+
 a = np.arange(-1, 1, 0.0001)
 mdotcrit1 = m_dot_crit1(a, 0.5)
 mdotcrit2 = m_dot_crit2(a, 0.5)
@@ -196,6 +229,7 @@ m_a_75 = [find_root(x, 0.75) for x in a]
 m_a_50 = [find_root(x, 0.5) for x in a]
 
 import matplotlib
+import pylab
 
 matplotlib.use("Agg")
 import matplotlib.pyplot as plt
@@ -204,17 +238,17 @@ import matplotlib.gridspec as gridspec
 fig = plt.figure(figsize=(8, 6))
 
 plt.style.use("classic")
-plt.fill_between(a, [0.0001 for x in a], [0.028 for x in a], color="blue", alpha=0.2)
-plt.fill_between(a, [0.028 for x in a], mdotcrit1, color="red", alpha=0.2)
-plt.fill_between(a, mdotcrit1, [375 for x in a], color="orange", alpha=0.2)
-plt.ylabel("$\dot{m}$", fontsize=24, usetex=True)
+plt.fill_between(a, [0.0001 for x in a], [0.01 for x in a], color="blue", alpha=0.2)
+plt.fill_between(a, [0.01 for x in a], [1 for x in a], color="red", alpha=0.2)
+plt.fill_between(a, [1 for x in a], [375 for x in a], color="orange", alpha=0.2)
+plt.ylabel("$f_\mathrm{Edd}$", fontsize=24, usetex=True)
 plt.xlabel("$a$", fontsize=24, usetex=True)
 plt.tick_params(axis="y", right=True, direction="in")
 plt.yscale("log")
-plt.axis([-1, 1, 0.001, 100])
-plt.text(-0.22, 0.004, "Thick disc", fontsize=20)
-plt.text(-0.2, 0.33, "Thin disc", fontsize=20)
-plt.text(-0.2, 18, "Slim disc", fontsize=20)
+plt.axis([-1, 1, 0.0001, 100])
+plt.text(-0.22, 0.0008, "Thick disk", fontsize=20)
+plt.text(-0.2, 0.08, "Thin disk", fontsize=20)
+plt.text(-0.2, 8, "Slim disk", fontsize=20)
 plt.minorticks_on()
 plt.tick_params(
     axis="x",
@@ -260,6 +294,7 @@ plt.tick_params(
 plt.savefig("modes.png", bbox_inches="tight")
 plt.close()
 
+a = np.arange(-1, 1, 0.0001)
 phi = -20.2 * a ** 3 - 14.9 * a ** 2 + 34.0 * a + 52.6
 horizon_ang_vel = a / (2 * (1 + np.sqrt(1 - a ** 2)))
 jet_factor = (
@@ -271,14 +306,14 @@ jet_factor = (
     * horizon_ang_vel ** 2
     * (1.0 + 1.38 * horizon_ang_vel ** 2 - 9.2 * horizon_ang_vel ** 4)
 )
-Z1_j = np.array(
+Z_1 = np.array(
     [
         1 + (1 - x ** 2) ** 0.333 * ((1 + abs(x)) ** 0.333 + (1 - abs(x)) ** 0.333)
         for x in a
     ]
 )
-Z2_j = np.array(np.sqrt(3 * a ** 2 + Z1_j ** 2))
-r_iso = 3 + Z2_j - np.sign(np.array(a)) * np.sqrt((3 - Z1_j) * (3 + Z1_j + 2 * Z2_j))
+Z_2 = np.array(np.sqrt(3 * a ** 2 + Z_1 ** 2))
+r_iso = 3 + Z_2 - np.sign(np.array(a)) * np.sqrt((3 - Z_1) * (3 + Z_1 + 2 * Z_2))
 eps_TD = 1 - np.sqrt(1 - 2 / (3 * r_iso))
 eps_ADAF1 = 0.144 * (6 / r_iso) * eps_TD * min(1, 0.028 / 0.0044)
 eps_ADAF2 = 0.144 * (6 / r_iso) * eps_TD * min(1, 0.001 / 0.0044)
@@ -286,6 +321,7 @@ Jet_ADAF = jet_factor * 0.3
 Jet_SD = 0.22 * jet_factor
 Jet_TD1 = 10 ** -3 * 0.1 ** (-0.1) * 100 ** 0.2 * 10 ** (2 * 0.1) * jet_factor
 Jet_TD2 = 10 ** -3 * 0.1 ** (-0.1) * 10 ** (-1 * 0.1) * jet_factor
+
 eps_SD1 = (
     1
     / 10
@@ -317,109 +353,82 @@ mdot_bh_TD1 = (1 - Jet_TD1 / 4.447) * (1 - eps_TD - Jet_TD1)
 mdot_bh_TD2 = (1 - Jet_TD2 / 4.447) * (1 - eps_TD - Jet_TD2)
 
 
-fig = plt.figure(figsize=(18, 4))
-fig.subplots_adjust(wspace=0, hspace=0, top=1, bottom=0)
-gs = gridspec.GridSpec(1, 3, width_ratios=[1, 1, 1])
+def omega(spin):
+    return spin / (2 * (1 + np.sqrt(1 - spin ** 2)))
+
+
+fig = plt.figure(figsize=(13, 4))
+fig.subplots_adjust(top=1, bottom=0, wspace=0.25)
+gs = gridspec.GridSpec(1, 2, width_ratios=[1, 1])
 plt.style.use("classic")
 
 plt.subplot(gs[0])
 plt.plot(
     a,
-    eps_ADAF2,
+    100 * 0.005 * (1 + 3 * (phi / 50) ** 2 * (horizon_ang_vel / 0.2) ** 2),
     linewidth=2,
-    label="$\epsilon_\mathrm{rad}(\dot{m}<0.0044)$",
-    color="red",
+    label="$\epsilon_\mathrm{wind,thick}$",
+    color="blue",
 )
 plt.plot(
     a,
-    eps_ADAF1,
+    100 * 0.1 * (1 - np.sqrt(1 - 2 / (3 * r_iso))),
     linewidth=2,
-    label="$\epsilon_\mathrm{rad}(\dot{m}=0.028)$",
+    label="$\epsilon_\mathrm{f}\epsilon_\mathrm{rad,NT}$ $\mathrm{(thin}$ $\mathrm{disc})$",
     color="red",
-    linestyle="--",
 )
-plt.plot(a, 0.97 * Jet_ADAF, linewidth=2, label="$\epsilon_\mathrm{jet}$", color="blue")
-plt.fill_between(a, eps_ADAF1, eps_ADAF2, color="red", alpha=0.2)
-plt.ylabel("$\epsilon_\mathrm{feedback}$", fontsize=24, usetex=True)
-plt.xlabel("$a$", fontsize=24, usetex=True)
-plt.tick_params(axis="y", right=True, direction="in")
-plt.legend(loc="upper left", prop={"size": 13})
-plt.xticks([-1, -0.5, 0, 0.5, 1], [-1, -0.5, 0, 0.5, 1])
-plt.yticks(
-    [0.0001, 0.001, 0.01, 0.1, 1, 10, 100],
-    ["", 10 ** (-3), 10 ** (-2), 10 ** (-1), 10 ** (-0), 10 ** (1), 10 ** (2)],
-)
-plt.minorticks_on()
-plt.axis([-1, 1, 0.0001, 10])
-plt.yscale("log")
-plt.tick_params(
-    axis="x",
-    direction="in",
-    bottom=True,
-    top=True,
-    length=8,
-    width=1.2,
-    which="major",
-    labelsize=16,
-)
-plt.tick_params(
-    axis="y",
-    direction="in",
-    left=True,
-    right=True,
-    length=8,
-    width=1.2,
-    which="major",
-    labelsize=16,
-)
-plt.tick_params(
-    axis="x",
-    direction="in",
-    bottom=True,
-    top=True,
-    length=4,
-    width=0.9,
-    which="minor",
-    labelsize=16,
-)
-plt.tick_params(
-    axis="y",
-    direction="in",
-    left=True,
-    right=True,
-    length=4,
-    width=0.9,
-    which="minor",
-    labelsize=16,
+plt.plot(
+    a,
+    100
+    * 0.0635
+    * (1 + ((1 / 1.88) ** 1.29 / (1 + (1 / 1.88) ** 1.29) * phi / 50) ** 2)
+    * np.maximum((1 - 8 * omega(a) ** 2 + 1 * omega(a)), np.zeros(np.size(a))),
+    linestyle=":",
+    linewidth=1.5,
+    label="$\epsilon_\mathrm{wind,slim},$ $f_\mathrm{Edd}=1$",
+    color="orange",
 )
-plt.title("Thick disc", fontsize=16)
-
-plt.subplot(gs[1])
-plt.plot(a, 0.97 * eps_TD, linewidth=2, label="$\epsilon_\mathrm{rad}$", color="red")
 plt.plot(
     a,
-    Jet_TD2,
-    linewidth=2,
-    label="$\epsilon_\mathrm{jet}(\dot{m}=0.028,M_\mathrm{BH}=10^9 \mathrm{M}_\odot)$",
-    color="blue",
+    100
+    * 0.0635
+    * (1 + ((10 / 1.88) ** 1.29 / (1 + (10 / 1.88) ** 1.29) * phi / 50) ** 2)
+    * np.maximum((1 - 8 * omega(a) ** 2 + 1 * omega(a)), np.zeros(np.size(a))),
+    linestyle="-.",
+    linewidth=1.5,
+    label="$\epsilon_\mathrm{wind,slim},$ $f_\mathrm{Edd}=10$",
+    color="orange",
 )
 plt.plot(
     a,
-    Jet_TD1,
-    linewidth=2,
-    label="$\epsilon_\mathrm{jet}(\dot{m}=1,M_\mathrm{BH}=10^6 \mathrm{M}_\odot)$",
-    color="blue",
+    100
+    * 0.0635
+    * (1 + ((100 / 1.88) ** 1.29 / (1 + (100 / 1.88) ** 1.29) * phi / 50) ** 2)
+    * np.maximum((1 - 8 * omega(a) ** 2 + 1 * omega(a)), np.zeros(np.size(a))),
     linestyle="--",
+    linewidth=1.5,
+    label="$\epsilon_\mathrm{wind,slim},$ $f_\mathrm{Edd}=100$",
+    color="orange",
 )
-plt.fill_between(a, Jet_TD1, Jet_TD2, color="blue", alpha=0.2)
+plt.plot(
+    a,
+    100
+    * 0.0635
+    * (1 + ((1000 / 1.88) ** 1.29 / (1 + (1000 / 1.88) ** 1.29) * phi / 50) ** 2)
+    * np.maximum((1 - 8 * omega(a) ** 2 + 1 * omega(a)), np.zeros(np.size(a))),
+    linestyle="-",
+    linewidth=1.5,
+    label="$\epsilon_\mathrm{wind,slim},$ $f_\mathrm{Edd}=1000$",
+    color="orange",
+)
+
+plt.fill_between(a, eps_ADAF1, eps_ADAF2, color="red", alpha=0.2)
+plt.ylabel("$\epsilon_\mathrm{wind}$ $[\%]$", fontsize=24, usetex=True)
 plt.xlabel("$a$", fontsize=24, usetex=True)
 plt.tick_params(axis="y", right=True, direction="in")
-plt.yscale("log")
-plt.legend(loc="upper left", prop={"size": 13})
-plt.xticks([-1, -0.5, 0, 0.5, 1], ["", -0.5, 0, 0.5, 1])
-plt.axis([-1, 1, 0.0001, 10])
-plt.yticks([0.001, 0.01, 0.1, 1, 10], ["", "", "", "", ""])
+pylab.legend(loc="upper left", prop={"size": 12}, ncol=2)
 plt.minorticks_on()
+plt.axis([-1, 1, 0, 25])
 plt.tick_params(
     axis="x",
     direction="in",
@@ -460,29 +469,58 @@ plt.tick_params(
     which="minor",
     labelsize=16,
 )
-plt.title("Thin disc", fontsize=16)
+plt.title("Wind efficiency", fontsize=16)
 
-plt.subplot(gs[2])
+plt.subplot(gs[1])
 plt.plot(
-    a, eps_SD1, linewidth=2, label="$\epsilon_\mathrm{rad}(\dot{m}=1)$", color="red"
+    a, 100 * Jet_ADAF, linewidth=2, label="$\epsilon_\mathrm{jet,thick}$", color="blue"
 )
 plt.plot(
     a,
-    eps_SD2,
-    linewidth=2,
-    label="$\epsilon_\mathrm{rad}(\dot{m}=50)$",
+    100 * Jet_ADAF * ((0.01 / 1.88) ** 1.29 / (1 + (0.01 / 1.88) ** 1.29)) ** 2,
+    linewidth=1.5,
+    linestyle=":",
+    label="$\epsilon_\mathrm{jet,thin},$ $f_\mathrm{Edd}=0.01$",
     color="red",
+)
+plt.plot(
+    a,
+    100 * Jet_ADAF * ((0.1 / 1.88) ** 1.29 / (1 + (0.1 / 1.88) ** 1.29)) ** 2,
+    linewidth=1.5,
+    linestyle="-.",
+    label="$\epsilon_\mathrm{jet,thin},$ $f_\mathrm{Edd}=0.1$",
+    color="red",
+)
+plt.plot(
+    a,
+    100 * Jet_ADAF * ((1 / 1.88) ** 1.29 / (1 + (1 / 1.88) ** 1.29)) ** 2,
+    linewidth=1.5,
     linestyle="--",
+    label="$\epsilon_\mathrm{jet,thin},$ $f_\mathrm{Edd}=1$",
+    color="red",
+)
+plt.plot(
+    a,
+    100 * Jet_ADAF * ((10 / 1.88) ** 1.29 / (1 + (10 / 1.88) ** 1.29)) ** 2,
+    linewidth=1.5,
+    linestyle="--",
+    label="$\epsilon_\mathrm{jet,slim},$ $f_\mathrm{Edd}=10$",
+    color="orange",
+)
+plt.plot(
+    a,
+    100 * Jet_ADAF * ((100 / 1.88) ** 1.29 / (1 + (100 / 1.88) ** 1.29)) ** 2 - 2,
+    linewidth=1.5,
+    linestyle="-",
+    label="$\epsilon_\mathrm{jet,slim},$ $f_\mathrm{Edd}=100$",
+    color="orange",
 )
-plt.plot(a, Jet_SD, linewidth=2, label="$\epsilon_\mathrm{jet}$", color="blue")
-plt.fill_between(a, eps_SD1, eps_SD2, color="red", alpha=0.2)
+
+plt.ylabel("$\epsilon_\mathrm{jet}$ $[\%]$", fontsize=24, usetex=True)
 plt.xlabel("$a$", fontsize=24, usetex=True)
 plt.tick_params(axis="y", right=True, direction="in")
-plt.yscale("log")
-plt.legend(loc="upper left", prop={"size": 13})
-plt.xticks([-1, -0.5, 0, 0.5, 1], ["", -0.5, 0, 0.5, 1])
-plt.axis([-1, 1, 0.0001, 10])
-plt.yticks([0.001, 0.01, 0.1, 1, 10], ["", "", "", "", ""])
+pylab.legend(loc="upper left", prop={"size": 15})
+plt.axis([-1, 1, 0, 200])
 plt.minorticks_on()
 plt.tick_params(
     axis="x",
@@ -524,7 +562,7 @@ plt.tick_params(
     which="minor",
     labelsize=16,
 )
-plt.title("Slim disc", fontsize=16)
+plt.title("Jet efficiency", fontsize=16)
 
 plt.savefig("efficiencies.png", bbox_inches="tight")
 
@@ -670,125 +708,75 @@ da_SD_Benson = (
 )
 
 
-fig = plt.figure(figsize=(18, 4))
-fig.subplots_adjust(wspace=0, hspace=0, top=1, bottom=0)
-gs = gridspec.GridSpec(1, 3, width_ratios=[1, 1, 1])
+fig = plt.figure(figsize=(7, 5))
 plt.style.use("classic")
 
-plt.subplot(gs[0])
-plt.plot(a, da_TD_acc_only, linewidth=2, label="Accretion only", color="black")
-plt.plot(a, da_TD_Benson, linewidth=1.5, label="Jet spindown included", color="blue")
-plt.plot(a, [0 for x in a], linewidth=1.5, color="black", linestyle="--")
-plt.ylabel("$\mathrm{d}a/\mathrm{d}\ln M_\mathrm{BH,0}$", fontsize=24, usetex=True)
-plt.xlabel("$a$", fontsize=24, usetex=True)
-plt.tick_params(axis="y", right=True, direction="in")
-plt.legend(loc="lower left", prop={"size": 15})
-plt.minorticks_on()
-plt.axis([-1, 1, -4, 7])
-plt.tick_params(
-    axis="x",
-    direction="in",
-    bottom=True,
-    top=True,
-    length=8,
-    width=1.2,
-    which="major",
-    labelsize=16,
+z1 = np.array(
+    [
+        1 + (1 - x ** 2) ** 0.333 * ((1 + abs(x)) ** 0.333 + (1 - abs(x)) ** 0.333)
+        for x in a
+    ]
 )
-plt.tick_params(
-    axis="y",
-    direction="in",
-    left=True,
-    right=True,
-    length=8,
-    width=1.2,
-    which="major",
-    labelsize=16,
+z2 = np.array(np.sqrt(3 * a ** 2 + z1 ** 2))
+r_iso = 3 + z2 - np.sign(np.array(a)) * np.sqrt((3 - z1) * (3 + z1 + 2 * z2))
+da_TD_acc_only = 2 / 3 * 1 / np.sqrt(3) * (
+    1 + 2 * np.sqrt(3 * r_iso - 2)
+) - 2 * a * np.sqrt(1 - 2 / (3 * r_iso))
+da_ADAF_Narayan = (
+    0.45 - 12.53 * a - 7.8 * a ** 2 + 9.44 * a ** 3 + 5.71 * a ** 4 - 4.03 * a ** 5
 )
-plt.tick_params(
-    axis="x",
-    direction="in",
-    bottom=True,
-    top=True,
-    length=4,
-    width=0.9,
-    which="minor",
-    labelsize=16,
+
+plt.plot(a, da_ADAF_Narayan, linewidth=2, label="Thick disk", color="blue")
+plt.plot(
+    a,
+    s(0.01, a),
+    linewidth=2,
+    label="Thin disk, $f_\mathrm{Edd}=0.01$",
+    linestyle=":",
+    color="red",
 )
-plt.tick_params(
-    axis="y",
-    direction="in",
-    left=True,
-    right=True,
-    length=4,
-    width=0.9,
-    which="minor",
-    labelsize=16,
+plt.plot(
+    a,
+    s(0.1, a),
+    linewidth=2,
+    label="Thin disk, $f_\mathrm{Edd}=0.1$",
+    linestyle="-.",
+    color="red",
 )
-plt.title("Thin disc", fontsize=16)
-
-plt.subplot(gs[1])
-plt.plot(a, da_ADAF_acc_only, linewidth=2, label="Accretion only", color="black")
-plt.plot(a, da_ADAF_Benson, linewidth=1.5, label="Jet spindown included", color="blue")
-plt.plot(a, [0 for x in a], linewidth=1.5, color="black", linestyle="--")
-plt.xlabel("$a$", fontsize=24, usetex=True)
-plt.tick_params(axis="y", right=True, direction="in")
-plt.xticks([-1.0, -0.5, 0.0, 0.5, 1.0], ["", -0.5, 0.0, 0.5, 1.0])
-plt.yticks([-8, -6, -4, -2, 0, 2, 4, 6, 8], ["", "", "", "", "", "", "", "", ""])
-plt.minorticks_on()
-plt.axis([-1, 1, -4, 7])
-plt.tick_params(
-    axis="x",
-    direction="in",
-    bottom=True,
-    top=True,
-    length=8,
-    width=1.2,
-    which="major",
-    labelsize=16,
+plt.plot(
+    a,
+    s(1, a),
+    linewidth=2,
+    label="Thin disk, $f_\mathrm{Edd}=1$",
+    linestyle="--",
+    color="red",
 )
-plt.tick_params(
-    axis="y",
-    direction="in",
-    left=True,
-    right=True,
-    length=8,
-    width=1.2,
-    which="major",
-    labelsize=16,
+plt.plot(
+    a,
+    s(10, a),
+    linewidth=2,
+    label="Slim disk, $f_\mathrm{Edd}=10$",
+    linestyle="--",
+    color="orange",
 )
-plt.tick_params(
-    axis="x",
-    direction="in",
-    bottom=True,
-    top=True,
-    length=4,
-    width=0.9,
-    which="minor",
-    labelsize=16,
+plt.plot(
+    a,
+    s(100, a),
+    linewidth=2,
+    label="Slim disk, $f_\mathrm{Edd}=100$",
+    linestyle="-",
+    color="orange",
 )
-plt.tick_params(
-    axis="y",
-    direction="in",
-    left=True,
-    right=True,
-    length=4,
-    width=0.9,
-    which="minor",
-    labelsize=16,
+plt.plot(a, [0 for x in a], linewidth=1.0, color="black", linestyle="--")
+plt.plot([-0.0001, 0.0001], [-200, 200], linewidth=1.0, color="black", linestyle="--")
+plt.ylabel(
+    "$\mathrm{d}a/(\mathrm{d} M_\mathrm{BH,0}/M_\mathrm{BH})$", fontsize=24, usetex=True
 )
-plt.title("Thick disc", fontsize=16)
-
-plt.subplot(gs[2])
-plt.plot(a, da_SD_acc_only, linewidth=2, label="Accretion only", color="black")
-plt.plot(a, da_SD_Benson, linewidth=1.5, label="Jet spindown included", color="blue")
-plt.plot(a, [0 for x in a], linewidth=1.5, color="black", linestyle="--")
 plt.xlabel("$a$", fontsize=24, usetex=True)
 plt.tick_params(axis="y", right=True, direction="in")
-plt.xticks([-1.0, -0.5, 0.0, 0.5, 1.0], ["", -0.5, 0.0, 0.5, 1.0])
-plt.yticks([-8, -6, -4, -2, 0, 2, 4, 6, 8], ["", "", "", "", "", "", "", "", ""])
+pylab.legend(loc="lower left", prop={"size": 13})
 plt.minorticks_on()
-plt.axis([-1, 1, -4, 7])
+plt.axis([-1, 1, -10, 10])
 plt.tick_params(
     axis="x",
     direction="in",
@@ -829,6 +817,5 @@ plt.tick_params(
     which="minor",
     labelsize=16,
 )
-plt.title("Slim disc", fontsize=16)
 
 plt.savefig("spinup.png", bbox_inches="tight")
diff --git a/doc/RTD/source/SubgridModels/AGNSpinJets/theory.rst b/doc/RTD/source/SubgridModels/AGNSpinJets/theory.rst
index b61564aef0da52895d7b04a060a9eb25e8e6a58b..c40d9c23f5def283d0cf523195ed3b4cc2257516 100644
--- a/doc/RTD/source/SubgridModels/AGNSpinJets/theory.rst
+++ b/doc/RTD/source/SubgridModels/AGNSpinJets/theory.rst
@@ -11,7 +11,7 @@ Here we provide a comprehensive summary of the model. In order to more easily vi
 
 Any model for realistic AGN jets must include black hole spin since jet powers depend steeply on spin, and because including spin provides a well-defined direction for the jets to be launched in. The spin (angular momentum) of BHs is best represented through the dimensionlesss spin parameter :math:`a=J_\mathrm{BH}c/M_\mathrm{BH}^2 G`, where :math:`J_\mathrm{BH}` is its angular momentum. For theoretical reasons, its magnitude cannot grow above 1. It can be positive, representing prograde accretion, or negative, representing retrograde accretion.
 
-Jet powers, in addition to depending on spin, also depend on which accretion state the black hole is in. We refer to these states by the shape of the accretion disk that surrounds the BH. We include three accretion states: the thick (or advection-dominated accretion flow; ADAF), thin (standard) and slim disk. Our main reference points for these disks are the following papers: `Shakura & Sunyaev (1973) <https://ui.adsabs.harvard.edu/abs/1973A%26A....24..337S/abstract>`_, `Narayan & Yi (1994) <https://ui.adsabs.harvard.edu/abs/1994ApJ...428L..13N/abstract>`_ and `Wang & Zhou. (1999) <https://ui.adsabs.harvard.edu/abs/1999ApJ...516..420W/abstract>`_, respectively.
+Jet powers, in addition to depending on spin, also depend on which accretion state the black hole is in. We refer to these states by the shape of the accretion disk that surrounds the BH. We include three accretion states: the thick (or advection-dominated accretion flow; ADAF), thin (standard) and slim disk (super-Eddington accretion). Our main reference points for these disks are the following papers: `Shakura & Sunyaev (1973) <https://ui.adsabs.harvard.edu/abs/1973A%26A....24..337S/abstract>`_, `Narayan & Yi (1994) <https://ui.adsabs.harvard.edu/abs/1994ApJ...428L..13N/abstract>`_ and `Wang & Zhou. (1999) <https://ui.adsabs.harvard.edu/abs/1999ApJ...516..420W/abstract>`_, respectively.
 
 The thick disk appears at low accretion rates, has very strong jets and is inefficient at spinning up the black hole. The thin disk, appearing at intermediate accretion rates, typically has weak jets, strong radiation and efficiently spins up the black hole. The slim disk, corresponding to super-Eddington accretion, has features of both: in terms of geometry, orbits and angular momentum, it is similar to the thick disk. It is optically thin, leading to strong radiation. However, it also has strong jets. We assume that each subgrid accretion disk launches jets and radiates at the same time, regardless of the type it is. However, we use expressions for the jet and radiative efficiencies that depend on the type of the disk, and which are physically motivated.
 
@@ -24,42 +24,63 @@ Transitions from one accretion state to another
     :figclass: align-center
     :alt: Accretion regimes
 
-    The type of accretion disk surrounding the BHs depending on their accretion rates and spins. The transition between the thick and thin disk is calculated assuming the viscosity parameter :math:`\alpha=0.2`, while the transition from thin to slim disk is assumed to occur when the latter is :math:`F=0.5` times as radiatively efficienct as the former.
+    The type of accretion disk surrounding the BHs depending on their accretion rates and spins.
 
-The state of the subgrid accretion disk depends mostly on the Eddington fraction, i.e. the (dimensionless) accretion rate of the BH in units of the Eddington accretion rate, which we denote as :math:`\dot{m}`. We assume that the subgrid accretion disk is thick for :math:`\dot{m}<0.03`, based on observations (`Russell et al. 2013 <https://ui.adsabs.harvard.edu/abs/2013MNRAS.432..530R/abstract>`_). This also allows us to constrain the value of one of the main parameters in any accretion model: the viscosity parameter :math:`\alpha` (which is related to the kinematic viscosity :math:`\nu`and sound speed :math:`c_\mathrm{s}` through :math:`\nu=\alpha c_\mathrm{s}H`, with :math:`c_\mathrm{s}` the sound speed and :math:`H` the disk half-thickness). Numerical calculations suggest that thick disks are present for :math:`\dot{m}<0.4\alpha^2` (`Yuan & Narayan 2014 <https://ui.adsabs.harvard.edu/abs/2014ARA%26A..52..529Y/abstract>`_), and this agrees with observations if :math:`\alpha=0.25-0.3`. These values agree very well with more direct observational estimates, which suggest :math:`\alpha=0.2-0.3` (`Martin et al. 2019 <https://ui.adsabs.harvard.edu/abs/2014ARA%26A..52..529Y/abstract>`_).
+The state of the subgrid accretion disk depends mostly on the Eddington fraction, i.e. the (dimensionless) accretion rate of the BH in units of the Eddington accretion rate, which we denote as :math:`f_\mathrm{Edd}`. We assume that the subgrid accretion disk is thick for :math:`f_\mathrm{Edd}<f_\mathrm{Edd,crit,thick}`, where :math:`f_\mathrm{Edd,crit,thick}\approx0.01-0.03` (based on observations; `Russell et al. 2013 <https://ui.adsabs.harvard.edu/abs/2013MNRAS.432..530R/abstract>`_) is a free parameter in the model. The accretion disc is assumed to be slim for :math:`f_\mathrm{Edd}>1`.
 
-The transition from the thin to the slim disk should occur around :math:`\dot{m}\approx 1`. However, the exact physics of this transition is not well understood. There is likely some spin dependence of the critical accretion rate, due to different radiative physics depending on spin. One of the main properties of slim disks is that they are less radiatively efficient than thin disks (`Sadowski et al. 2014 <https://ui.adsabs.harvard.edu/abs/2014MNRAS.439..503S/abstract>`_). We thus assume that the transition occurs when the radiative efficiency of a slim disk, :math:`\epsilon_\mathrm{r,SD}`, falls below some fraction of the radiative efficiency of a thin disk, :math:`\epsilon_\mathrm{r,TD}`. We quantify this as :math:`\epsilon_\mathrm{r,SD}<F\epsilon_\mathrm{r,SD}`, with :math:`F\approx 0.5` a free parameter. We give the expressions for both of the efficiencies below.
+Accretion efficiencies
+-----------------------------------------------
 
-Jet efficiencies
-----------------
+Our model requires the usage of accretion efficiencies, minimally in the thick disk regime. These accretion efficiencies arise due to winds that take away most of the accreting mass as it falls towards the BH. We supress the large-scale accretion rate (e.g. the Bondi rate), :math:`\dot{M}_\mathrm{BH,0}:` such that the net accretion rate is equal to
 
-The jet efficiency is related to the jet power through :math:`\epsilon_\mathrm{j}=P_\mathrm{j}/\dot{M}_\mathrm{BH,0}c^2`, where :math:`\dot{M}_\mathrm{BH,0}` is the accretion rate measured in the simulation, e.g. the Bondi rate). We use the formula for the jet efficiency based on general-relativistic, magneto-hydrodynamical (GRMHD) simulations by `Tchekhovskoy et al. (2010) <https://ui.adsabs.harvard.edu/abs/2010ApJ...711...50T/abstract>`_:
+.. math::
+    \dot{M}_\mathrm{BH} = (1 - \epsilon_\mathrm{rad} - \epsilon_\mathrm{wind} - \epsilon_\mathrm{jet})\epsilon_\mathrm{acc}\dot{M}_\mathrm{BH,0},
+
+where the terms in parenthesis are feedback efficiencies (discussed below), defined as :math:`\epsilon_i=P_i/\dot{M}_\mathrm{BH}c^2`, while :math:`\epsilon_\mathrm{acc}` is the accretion efficiency.
+
+In the thick and slim disk, we allow options where the accretion efficiencies are free parameters in the model, to be tuned somehow (in practice, for the thick disc efficiency, to the local AGN bolometric luminosity function). We also allow a more complex scaling for the thick disk, based on GRMHD simulations (e.g. `Cho et al. 2024 <https://arxiv.org/abs/2405.13887>`_). These simulations show that the accretion efficiency in thick disks can be calculated as
 
 .. math::
-    \epsilon_\mathrm{j}=\frac{\kappa}{4\pi}\bigg(\frac{H/R}{0.3}\bigg)^\eta \phi_\mathrm{BH}^2\Omega_\mathrm{BH}^2\big(1+1.38\Omega_\mathrm{BH}^2-9.2\Omega_\mathrm{BH}^4\big),
+    \epsilon_\mathrm{acc} = \bigg(\frac{R_0}{R_\mathrm{thick}}\bigg)^s,
+    
+where :math:`R_0\approx5-10` (in units of :math:`R_\mathrm{G}`), :math:`R_\mathrm{thick}` is the radius of the hot accretion flow and :math:`s=0.5` in recent such simulations. The radius :math:`R_\mathrm{thick}` is the same as the Bondi radius if the accretion rate is very low. However, at high accretion rates (but still below :math:`f_\mathrm{Edd}\approx0.01`), the accretion disk may be thin at large distances and thick at smaller ones, with the transition occuring at some radius :math:`R_\mathrm{tr}`. In this case, the winds (and mass loading) operate only at smaller radii. Given these considerations, we write
 
-where :math:`\kappa\approx0.05` is a numerical factor which depends on the initial geometry of the magnetic field, :math:`\phi_\mathrm{BH}` is the dimensionless magnetic flux threading the horizon (see original paper for precise definition), and :math:`\Omega_\mathrm{BH}=a/2r_\mathrm{H}` is the (dimensionless) angular velocity of the black hole event horizon. Here, :math:`r_\mathrm{H}=1+\sqrt{1-a^2}` is the radius of the horizon in units of the gravitational radius :math:`R_\mathrm{G}=M_\mathrm{BH}G/c^2`. The formula above, for the jet efficiency, agrees very well with the results from higher-resolution simulations performed by `Narayan et al. (2021) <https://ui.adsabs.harvard.edu/abs/2010ApJ...711...50T/abstract>`_, who provide the following fit for the magnetic flux as a function of spin:
+.. math::
+    R_\mathrm{thick} = \min(R_\mathrm{B},R_\mathrm{tr}),
+    
+and we choose to parametrize :math:`R_\mathrm{tr}` based on original calculations by `Narayan & Yi (1995) <https://ui.adsabs.harvard.edu/abs/1995ApJ...452..710N/abstract>`_, who found the transition radius as the radius where half of the energy gets radiated away, and half advected inwards. Their formula can be written in the form
 
 .. math::
-    \phi_\mathrm{BH}(a)=-20.2a^3-14.9a^2+34a+52.6.
+    R_\mathrm{tr} = R_1 \bigg(\frac{0.01}{f_\mathrm{Edd}}\bigg)^2,
     
-The `Tchekhovskoy et al. (2010) <https://ui.adsabs.harvard.edu/abs/2010ApJ...711...50T/abstract>`_ jet efficiency depends very steeply on spin (:math:`\epsilon_\mathrm{j}\propto a^2` for small spin and :math:`\epsilon_\mathrm{j}\propto a^6` near :math:`a=1`). It can reach values above 100 per cent for large spins, and is also different (weaker) for negative spins.
+where :math:`R_1` is some normalisation radius that depends strongly on the assumed value of the accretion disk viscosity parameter :math:`\alpha`. We instead leave :math:`R_1` as a free parameter in our formulation. Note that at moderate Eddington ratios, where the Bondi radius is not the limiting radius (i.e. where a thin disk component exists outside the thick disk), we may write the accretion efficiency as:
 
-The dependence of the jet efficiency on the type of accretion disk comes through the factor that depends on the aspect ratio :math:`H/R`, since accretion disks differ in this quantity. Theoretical, self-similar models of thick disks suggest :math:`H/R=0.5` (`Narayan & Yi 1995b <https://ui.adsabs.harvard.edu/abs/1995ApJ...452..710N/abstract>`_), but we instead take :math:`H/R=0.3`, more in line with simulations. For slim disks, which have received less attention in simulations, we assume the value :math:`H/R=1/(2\sqrt{5})\approx 0.2` (based on the self-similar model by `Wang & Zhou. 1999 <https://ui.adsabs.harvard.edu/abs/1999ApJ...516..420W/abstract>`_).
+.. math::
+    \epsilon_\mathrm{acc} = \sqrt{\frac{R_0}{R_1}}\bigg(\frac{f_\mathrm{Edd}}{0.01}\bigg),
+    
+where we have assumed :math:`s=0.5`.
+
+Jet efficiencies
+----------------
 
-Thin disks are, not surprisingly, much thinner. The value of :math:`H/R` in this regime is not a constant, but rather depends on the BH mass and accretion rate, slightly on radius and also on the viscosity parameter :math:`\alpha`. Thin disks have three different regions in the `Shakura & Sunyaev (1973) <https://ui.adsabs.harvard.edu/abs/1973A%26A....24..337S/abstract>`_ model. For simplicity, we model the whole disk as being represented with only one region. In region a), the innermost one, radiation dominates over gas pressure. It is typically very small or doesn't exist at all, so we disregard it as a possibility. In regions b) and c), gas pressure dominates over radiation pressure. In b), electrons dominate in the opacity, while in c), free-free absorption dominates. We leave both regions as a possibility, and leave the choice as a free parameter in the model (not likely to lead to large differences in galaxy/BH evolution). The expressions for the aspect ratio in these regions are
+The jet efficiency is related to the jet power through :math:`\epsilon_\mathrm{j}=P_\mathrm{j}/\dot{M}_\mathrm{BH,0}c^2`, where :math:`\dot{M}_\mathrm{BH,0}` is the accretion rate measured in the simulation, e.g. the Bondi rate). We use the formula for the jet efficiency based on general-relativistic, magneto-hydrodynamical (GRMHD) simulations by `Tchekhovskoy et al. (2010) <https://ui.adsabs.harvard.edu/abs/2010ApJ...711...50T/abstract>`_:
 
 .. math::
-    \bigg(\frac{H}{R}\bigg)_\mathrm{TD,b} = 1.25\times10^{-3} \alpha^{-1/10}\dot{m}^{1/5}\bigg(\frac{M_\mathrm{BH}}{10^8\hspace{0.5mm}\mathrm{M}_\odot}\bigg)^{-1/10}\bigg(\frac{R}{2R_\mathrm{G}}\bigg)^{1/20}
+    \epsilon_\mathrm{j}=\frac{\kappa}{4\pi} \phi_\mathrm{BH}^2\Omega_\mathrm{BH}^2\big(1+1.38\Omega_\mathrm{BH}^2-9.2\Omega_\mathrm{BH}^4\big),
 
-in region b) and
+where :math:`\kappa\approx0.05` is a numerical factor which depends on the initial geometry of the magnetic field, :math:`\phi_\mathrm{BH}` is the dimensionless magnetic flux threading the horizon (see original paper for precise definition), and :math:`\Omega_\mathrm{BH}=a/2r_\mathrm{H}` is the (dimensionless) angular velocity of the black hole event horizon. Here, :math:`r_\mathrm{H}=1+\sqrt{1-a^2}` is the radius of the horizon in units of the gravitational radius :math:`R_\mathrm{G}=M_\mathrm{BH}G/c^2`. The formula above, for the jet efficiency, agrees very well with the results from higher-resolution simulations performed by `Narayan et al. (2021) <https://ui.adsabs.harvard.edu/abs/2010ApJ...711...50T/abstract>`_, who provide the following fit for the magnetic flux as a function of spin:
 
 .. math::
-    \bigg(\frac{H}{R}\bigg)_\mathrm{TD,c} = 1.15\times10^{-3} \alpha^{-1/10}\dot{m}^{3/20}\bigg(\frac{M_\mathrm{BH}}{10^8\hspace{0.5mm}\mathrm{M}_\odot}\bigg)^{-1/10}\bigg(\frac{R}{2R_\mathrm{G}}\bigg)^{1/8}
+    \phi_\mathrm{BH,MAD}(a)=-20.2a^3-14.9a^2+34a+52.6.
+    
+The `Tchekhovskoy et al. (2010) <https://ui.adsabs.harvard.edu/abs/2010ApJ...711...50T/abstract>`_ jet efficiency depends very steeply on spin (:math:`\epsilon_\mathrm{j}\propto a^2` for small spin and :math:`\epsilon_\mathrm{j}\propto a^6` near :math:`a=1`). It can reach values above 100 per cent for large spins, and is also different (weaker) for negative spins.
 
-in region c). 
+The dependence of the jet efficiency on the type of accretion disk is encoded in the fact that thick disks are thought to be in a magnetically-arred state (so-called MAD. see `Narayan et al. 2003 <https://ui.adsabs.harvard.edu/abs/2003PASJ...55L..69N/abstract>`_), while thin disks are likely not, because they do not feature strong advection. The slim disk, on the other hand, is thought to be similar to the thick disk in terms of advection, and thus probably in terms of jet powers. Recent simulations by `Ricarte et al. (2023) <https://ui.adsabs.harvard.edu/abs/2023ApJ...954L..22R/abstract>`_ have found an increase of :math:`\phi_\mathrm{BH}` in the thin and slim disk regime as the Eddington ratio increases, and they parametrise this increase as
 
-The jet efficiency also depends on the slope :math:`\eta`. Classical jet theory (`Meier 2001 <https://ui.adsabs.harvard.edu/abs/2001Sci...291...84M/abstract>`_) suggests that jet powers depend on the aspect ratio linearly, so :math:`\eta=1`. This is also in line with some simulations finding a reduction in jet efficiencies with the aspect ratio (e.g. `Tchekhovskoy et al. 2014 <https://ui.adsabs.harvard.edu/abs/2014MNRAS.437.2744T/abstract>`_). In this scenario, jets launched from thin disks are of order :math:`\approx100` times less powerful than those launched from thick disks. On the other hand, some simulations of thin disks have found jet efficiencies similar to thick disk ones (e.g. `Liska et al. 2019 <https://ui.adsabs.harvard.edu/abs/2019MNRAS.487..550L/abstract>`_), which is supported by observations of blazars (`Ghisellini et al. 2014 <https://ui.adsabs.harvard.edu/abs/2014Natur.515..376G/abstract>`_). In this picture, thin jets are approximately as efficient as thick disk ones, which can in our case be implemented as :math:`\eta=0`. The reality is likely to be somwhere in between. Note that the choice of :math:`\eta` likely has a strong impact on the evolution of galaxies and BHs; our default choice is the classical picture in which :math:`\eta=1`.
+.. math::
+    \phi_\mathrm{BH,thin,slim} = \frac{(f_\mathrm{Edd}/1.88)^{1.29}}{1+(f_\mathrm{Edd}/1.88)^{1.29}}\phi_\mathrm{BH,MAD}.
+
+The magnetic flux eventually saturates (at very high :math:`f_\mathrm{Edd}`) at the same value as that reached in the thick disc; :math:`\phi_\mathrm{BH,MAD}`.
 
 .. figure:: efficiencies.png
     :width: 1200px
@@ -69,8 +90,8 @@ The jet efficiency also depends on the slope :math:`\eta`. Classical jet theory
 
     Feedback efficiencies (jet - blue, radiation - red) for all three accretion disk types. Shaded regions represent likely ranges of efficiencies (where the efficiencies depend on mass and/or accretion rate). The thin disk jet efficiencies were computed assuming the slope of the efficiency vs. aspect ratio relation is :math:`\eta=1`, and the aspect ratios were computed for region b) of the Shakura & Sunyaev solution. Radiative efficiencies in the thick disk were computed assuming the electron heating parameter :math:`\delta=0.2`.
 
-Radiative efficiencies
-----------------------
+Radiative/wind efficiencies
+---------------------------
 
 In the EAGLE and COLIBRE models, all subgrid accretion disks are effectively thin, and the BH is always assumed to be in this regime. In our model, the radiative efficiency (defined in an analagous way to the jet efficiency, but using the luminosity) is no longer fixed at a value of order :math:`10` per cent. Instead, we use spin-dependant formulas that vary with the type of disk. In the thin disk, the radiative efficiency :math:`\epsilon_\mathrm{r,TD}` is related to the binding energy at the innermost stable circular orbit (ISCO) and is given by
 
@@ -79,7 +100,7 @@ In the EAGLE and COLIBRE models, all subgrid accretion disks are effectively thi
     
 Here, :math:`r_\mathrm{ISCO}` is the radius of the ISCO in gravitational radii (see e.g. appendix B of `Fiacconi et al. 2018 <https://ui.adsabs.harvard.edu/abs/2018MNRAS.477.3807F/abstract>`_ for an expression giving the spin dependence). The radiative efficiency of the thin disk grows slowly from its minimum value of :math:`\approx4` per cent for :math:`a=-1` to :math:`\approx5.5` per cent for :math:`a=0`. For positive spins it grows more steeply; it is :math:`10` per cent by :math:`a=0.65`. Beyond that the dependence steepens even further, with values of :math:`20`, :math:`30` and :math:`40` per cent reached at :math:`a=0.95`, :math:`a=0.997` and :math:`a=1`, respectively.
 
-In the thick disk regime, radiative efficiencies are lower by a factor :math:`\approx100` than jet efficiencies. The formulas we use are based on results by `Mahadevan (1997) <https://ui.adsabs.harvard.edu/abs/1997ApJ...477..585M/abstract>`_, who studied cooling processes of electrons (which dominate in the radiation) in the context of the original thick disc solution. They found two different regimes: for :math:`\dot{m}<\dot{m}_\mathrm{crit,visc}`, viscous heating dominates the heating of electrons, whereas for :math:`\dot{m}_\mathrm{crit,visc}<\dot{m}<\dot{m}_\mathrm{crit,ADAF}`, it is dominated by ion-electron heating. Here, :math:`\dot{m}_\mathrm{crit,visc}` is the transitional value between the two thick disc (ADAF) regimes, and :math:`\dot{m}_\mathrm{crit,ADAF}=0.4\alpha^2` is the transitional accretion rate which separates thin and thick discs. The radiative efficiency in the viscous heating regime is given by
+In the thick disk regime, radiative efficiencies are lower by a factor :math:`\approx100` than jet efficiencies. The formulas we use are based on results by `Mahadevan (1997) <https://ui.adsabs.harvard.edu/abs/1997ApJ...477..585M/abstract>`_, who studied cooling processes of electrons (which dominate in the radiation) in the context of the original thick disc solution. They found two different regimes: for :math:`f_\mathrm{Edd}<f_\mathrm{Edd,crit,visc}`, viscous heating dominates the heating of electrons, whereas for :math:`f_\mathrm{Edd,crit,visc}<f_\mathrm{Edd}<f_\mathrm{Edd,crit,ADAF}`, it is dominated by ion-electron heating. Here, :math:`f_\mathrm{Edd,crit,visc}` is the transitional value between the two thick disc (ADAF) regimes, and :math:`f_\mathrm{Edd,crit,ADAF}=0.4\alpha^2` is the transitional accretion rate which separates thin and thick discs. The radiative efficiency in the viscous heating regime is given by
 
 .. math::
     \epsilon_\mathrm{r,ADAF}=0.0002\epsilon_\mathrm{r,TD}\bigg(\frac{\delta_\mathrm{ADAF}}{0.0005}\bigg)\bigg(\frac{1-\beta}{0.5}\bigg)\bigg(\frac{6}{r_\mathrm{ISCO}}\bigg),
@@ -87,30 +108,32 @@ In the thick disk regime, radiative efficiencies are lower by a factor :math:`\a
 while in the ion-heating regime it is given by
 
 .. math::
-    \epsilon_\mathrm{r,ADAF}=0.2\epsilon_\mathrm{r,TD}\bigg(\frac{\dot{m}}{\alpha^2}\bigg)\bigg(\frac{\beta}{0.5}\bigg)\bigg(\frac{6}{r_\mathrm{ISCO}}\bigg).
+    \epsilon_\mathrm{r,ADAF}=0.2\epsilon_\mathrm{r,TD}\bigg(\frac{f_\mathrm{Edd}}{\alpha^2}\bigg)\bigg(\frac{\beta}{0.5}\bigg)\bigg(\frac{6}{r_\mathrm{ISCO}}\bigg).
     
 Here, :math:`\beta` is the ratio of gas pressure and total pressure (which includes the magnetic pressure). `Yuan & Narayan (2014) <https://ui.adsabs.harvard.edu/abs/2014ARA%26A..52..529Y/abstract>`_ define a somewhat different parameter, :math:`\beta_\mathrm{ADAF}`, as the ratio of gas pressure and magnetic pressure. The two parameters are related by :math:`\beta=\beta_\mathrm{ADAF}/(1+\beta_\mathrm{ADAF})`. :math:`\beta_\mathrm{ADAF}` is not an independent parameter; many simulations have found that :math:`\alpha\beta_\mathrm{ADAF}\approx0.5` (e.g. `Begelman et al. 2021 <https://ui.adsabs.harvard.edu/abs/2022MNRAS.511.2040B/abstract>`_, see also `Yuan & Narayan 2014 <https://ui.adsabs.harvard.edu/abs/2014ARA%26A..52..529Y/abstract>`_ for a review), which we adopt. :math:`\delta_\mathrm{ADAF}` represents the fraction of viscous energy transferred to the electrons, and is constrained in theoretical studies between 0.1 and 0.5 (`Yuan & Narayan 2014 <https://ui.adsabs.harvard.edu/abs/2014ARA%26A..52..529Y/abstract>`_, `Sharma et al. 2007 <https://ui.adsabs.harvard.edu/abs/2007ApJ...667..714S/abstract>`_). Observations imply a value close to 0.2 (`Yuan et al. 2003 <https://ui.adsabs.harvard.edu/abs/2003ApJ...598..301Y/abstract>`_, `Liu & Wu 2013 <https://ui.adsabs.harvard.edu/abs/2013ApJ...764...17L/abstract>`_). The critical accretion rate between the two thick disc regimes can be found by ensuring that both formulas presented above yield the same radiative efficiency (at that accretion rate). This gives an accretion rate equal to
 
 .. math::
-    \dot{m}_\mathrm{crit,visc}=0.0002\bigg(\frac{\delta_\mathrm{ADAF}}{0.0005}\bigg)\bigg(\frac{1-\beta}{\beta}\bigg)\alpha^2.
+    f_\mathrm{Edd,crit,visc}=0.0002\bigg(\frac{\delta_\mathrm{ADAF}}{0.0005}\bigg)\bigg(\frac{1-\beta}{\beta}\bigg)\alpha^2.
     
 For slim disks we take the radiative efficiency based on GRMHD simulations of super-Eddington accretion (for various BH spins) performed by `Sadowski et al. (2014) <https://ui.adsabs.harvard.edu/abs/2014MNRAS.439..503S/abstract>`_. `Madau et al. (2014) <https://ui.adsabs.harvard.edu/abs/2014ApJ...784L..38M/abstract>`_ found the following fitting function which represents the `Sadowski et al. (2014) <https://ui.adsabs.harvard.edu/abs/2014MNRAS.439..503S/abstract>`_ results:
 
 .. math::
-    \epsilon_\mathrm{r,SD}=\frac{0.1}{\dot{m}}A(a)\bigg( \frac{0.985}{1.6/\dot{m}+B(a)}+\frac{0.015}{1.6/\dot{m}+C(a)}\bigg),
+    \epsilon_\mathrm{r,SD}=\frac{0.1}{f_\mathrm{Edd}}A(a)\bigg( \frac{0.985}{1.6/f_\mathrm{Edd}+B(a)}+\frac{0.015}{1.6/f_\mathrm{Edd}+C(a)}\bigg),
     
-where the three spin-dependant functions are given by :math:`A(a)=(0.9663-0.9292a)^{-0.5639}`, :math:`B(a)=(4.627-4.445a)^{-0.5524}` and :math:`C(a)=(827.3-718.1a)^{-0.7060}`. The radiative efficiency of slim disks, based on this formula, matches the thin disk radiative efficiency (given at the beginning of the section) at low accretion rates. At high accretion rates (:math:`\dot{m}\gtrapprox1`, but depending on spin), the radiative efficiency drops. These two formulas are used to decide when a disk transitions from thin to slim.
+where the three spin-dependant functions are given by :math:`A(a)=(0.9663-0.9292a)^{-0.5639}`, :math:`B(a)=(4.627-4.445a)^{-0.5524}` and :math:`C(a)=(827.3-718.1a)^{-0.7060}`. The radiative efficiency of slim disks, based on this formula, matches the thin disk radiative efficiency (given at the beginning of the section) at low accretion rates. At high accretion rates (:math:`f_\mathrm{Edd}\gtrapprox1`, but depending on spin), the radiative efficiency drops.
 
-Evolution of the black hole spin magnitude
-------------------------------------------
+The thin disc radiative efficiency is used to source feedback in the simulations. In the thin disk regime, a fraction :math:`\epsilon_\mathrm{f}\approx0.1` of all of the radiation released by black holes couples to the gas in the form of thermal energy. In the thick and slim disk, we do not use radiation to source feedback. We do, however, assume that winds launched from the accretion disk are present in these two states. In the thick disk, winds are thought to be launched on account of a combination of gas pressure and MHD effects. We use the formula from `Sadowski et al. (2013) <https://ui.adsabs.harvard.edu/abs/2013MNRAS.436.3856S/abstract>`_:
 
-.. figure:: spec_ang_mom.png
-    :width: 600px
-    :align: center
-    :figclass: align-center
-    :alt: Angular momenta
+.. math::
+    \epsilon_\mathrm{wind,thick} = 0.005\bigg[1+0.3\bigg(\frac{\phi_\mathrm{BH,MAD}}{50}\bigg)\bigg(\frac{\Omega_\mathrm{H}}{0.2}\bigg) \bigg].
+	
+For the slim disk, we again use results from `Ricarte et al. (2023) <https://ui.adsabs.harvard.edu/abs/2023ApJ...954L..22R/abstract>`_, as we did for the jet efficiency. We use their total MHD efficiency and subtract from that the analytical jet efficiency as given by the formula we use as a function of spin and magnetic flux. We then found a simple fitting function to the remaining efficiency, representing the wind:
 
-    Dimensionless pecific angular momentum of the thin disk at the innermost stable circular orbit (ISCO, solid red line), compared with the specific angular momentum at the inner radius (the event horizon) for advection-dominated flows (the thick and slim disk) for a few values of the viscosity parameter :math:`\alpha`. The dashed red line shows that the latter can be approximated as :math:`45` per cent of the former.
+.. math::
+    \epsilon_\mathrm{wind,slim} = 0.065\bigg[1+\bigg(\frac{\phi_\mathrm{BH,thin,slim}}{50}\bigg)^2\bigg] \big(1+\Omega_\mathrm{H}-8\Omega_\mathrm{H}^2\big).
+
+Evolution of the black hole spin magnitude
+------------------------------------------
 
 The BH spin (or angular momentum) is, naturally, a vector. However, due to Lense-thirring torques (we discuss these in more detail below), the accretion disk is always either aligned or counteraligned with the rotational axis of the black hole. This means that almost all relevant quantities, such as the efficiencies discussed above, can be expressed as depending only on the magnitude of spin, but also allowing for a negative sign to account for counteraligned disks (retrograde accretion). This is also true for the evolution of the magnitude of spin.
 
@@ -119,16 +142,34 @@ In the absence of jet spindown, the evolution of angular momentum is given simpl
 .. math::
     \frac{\mathrm{d}a}{\mathrm{d}\ln M_\mathrm{BH,0}}=\ell_\mathrm{in}-2a e_\mathrm{in},
     
-where :math:`\ell_\mathrm{in}` is the specific angular momentum in units where :math:`G` and :math:`c` are equal to unity, and :math:`\mathrm{d}\ln M_\mathrm{BH,0}=\mathrm{d}M_\mathrm{BH,0}/M_\mathrm{BH}` is the logarithmic change in mass, not including losses due to radiation (`Fanidakis et al. 2011 <https://ui.adsabs.harvard.edu/abs/2011MNRAS.410...53F/abstract>`_). The specific binding energy can be related to the radiative efficiency through :math:`e_\mathrm{in}=1-\epsilon_\mathrm{r}` for all three accretion states (for the thick disc, the radiative efficiency is negligible for this application). 
+where :math:`\ell_\mathrm{in}` is the specific angular momentum in units where :math:`G` and :math:`c` are equal to unity, and :math:`\mathrm{d}\ln M_\mathrm{BH,0}=\mathrm{d}M_\mathrm{BH,0}/M_\mathrm{BH}` is the logarithmic change in mass, not including losses due to radiation (`Fanidakis et al. 2011 <https://ui.adsabs.harvard.edu/abs/2011MNRAS.410...53F/abstract>`_). The specific binding energy can be related to the radiative efficiency through :math:`e_\mathrm{in}=1-\epsilon_\mathrm{r}` for all three accretion states (for the thick disc, the radiative efficiency is negligible for this application). All of the above quantities are evaluated at some inner radius beyond which gas orbits are unstable.
 
-For the thin disc, the inner radius :math:`R_\mathrm{in}` can be taken to be the radius of the ISCO, since orbits should quickly degrade within it. We thus take :math:`\ell_\mathrm{in}` as the specific angular momentum at the ISCO for the thin disc (the expression for which is given in e.g. Appendix B of `Fiacconi et al. 2018 <https://ui.adsabs.harvard.edu/abs/2018MNRAS.477.3807F/abstract>`_). For the thin disk, the spinup function (the equation shown above) is always positive, meaning that the BH will always be spun up to positive values. This means that the BH will be spun down if spin is negative, or spun up to an equilibrium value of :math:`a_\mathrm{eq}=1` if spin is positive. For advection-dominated disks (the thick and slim disk), we assume that :math:`\ell_\mathrm{in}` is :math:`45` per cent of the ISCO value, based on numerical GR calculations by `Popham & Gammie (1998) <https://ui.adsabs.harvard.edu/abs/1998ApJ...504..419P/abstract>`_. We base this assumption on fits of the `Popham & Gammie (1998) <https://ui.adsabs.harvard.edu/abs/1998ApJ...504..419P/abstract>`_ results done by `Benson & Babul (2009) <https://ui.adsabs.harvard.edu/abs/2009MNRAS.397.1302B/abstract>`_. We independently compared these fits to the ISCO values and found :math:`\ell_\mathrm{in}\approx0.45\ell_\mathrm{ISCO}` with no more than :math:`10` per cent error for all values of spin and relevant values of :math:`\alpha=0.1-0.3`.
+To be consistent with what we assumed for feedback efficiencies, we take results for the spinup/spindown function directly from GRMHD simulations. For the thick disc, we use the formula from `Narayan et al. (2021) <https://ui.adsabs.harvard.edu/abs/2010ApJ...711...50T/abstract>`_:
 
-For the thick and slim disk, these lower specific angular momenta lead to a non-zero equilibrium spin value :math:`a_\mathrm{eq}<1`. If :math:`a>a_\mathrm{eq}`, the BH will be spun down due to frame-dragging and viscosity; the frame-dragging rotationally accelerates any accreting gas (on account of the BH angular momentum), while viscosity carries away some of that angular momentum. Including jets into the model leads to further spindown. The jet spindown term (to be added to the spinup equation above) can be derived as 
+.. math::
+    \bigg(\frac{\mathrm{d}a}{\mathrm{d}M_\mathrm{BH,0}/M_\mathrm{BH}}\bigg)_\mathrm{thick}=0.45 - 12.53a - 7.8a^2 +9.44a^3 + 5.71a^4 -4.03a^5.
+  
+For the slim and thin disc, we use results from `Ricarte et al. (2023) <https://ui.adsabs.harvard.edu/abs/2023ApJ...954L..22R/abstract>`_, who find a fitting formula that smoothly interpolates between the thin disc regime without significant jet feedback (for :math:`f_\mathrm{Edd}` not close to super-Eddington values), and that where jet feedback essentially matches the thick disc (and so jet spindown should also be similar). Their formula takes the form
 
 .. math::
-    \bigg(\frac{\mathrm{d}a}{\mathrm{d}\ln M_\mathrm{BH,0}}\bigg)_\mathrm{j}=-\epsilon_\mathrm{j}(a)\frac{\sqrt{1-a^2}}{a}\bigg[\Big(\sqrt{1-a^2}+1 \Big)^2+a^2 \bigg]
+    \bigg(\frac{\mathrm{d}a}{\mathrm{d}M_\mathrm{BH,0}/M_\mathrm{BH}}\bigg)_\mathrm{thin/slim}=s_\mathrm{HD} - s_\mathrm{EM},
     
-(see `Benson & Babul 2009 <https://ui.adsabs.harvard.edu/abs/2009MNRAS.397.1302B/abstract>`_ for a derivation, which we have independently verified). Including jet spindown leads to even lower equilibrium spin values; e.g. for the thick disk this is only :math:`a_\mathrm{eq}\approx0.25`.
+where the first term is a pure hydrodynamical term, while the second is an electromagnetic term. The first term is given by
+
+.. math::
+    s_\mathrm{HD}=\frac{s_\mathrm{thin}+s_\mathrm{min}\xi}{1+\xi},
+
+where :math:`\xi=0.017f_\mathrm{Edd}`, :math:`s_\mathrm{min}=0.86-1.94a` and :math:`s_\mathrm{thin}=\ell_\mathrm{ISCO}-2a e_\mathrm{ISCO}` is the spinup/spindown function of the 'pure' thin disc (with no outflows and outside the MAD regime), in which :math:`\ell_\mathrm{ISCO}` and :math:`e_\mathrm{ISCO}` are the (dimensionless) specific angular momentum and binding energy, respectively, at the ISCO. The EM term is given by
+
+.. math::
+    s_\mathrm{EM}=\mathrm{sgn}(a)\epsilon_\mathrm{EM}\bigg(\frac{1}{k\Omega_\mathrm{H}}-2a\bigg),
+    
+where :math:`\epsilon_\mathrm{EM}` is the total (jet+wind) EM efficiency, and :math:`k` is given by 
+
+.. math::
+    k=\min(0.35,0.1+0.5a)
+    
+for positive spins :math:`a>0` and by :math:`k=0.23` for negative spins :math:`a<0`.
 
 .. figure:: spinup.png
     :width: 1200px
@@ -136,7 +177,7 @@ For the thick and slim disk, these lower specific angular momenta lead to a non-
     :figclass: align-center
     :alt: Spinup/spindown function
 
-    Spinup/spindown function (the rate of black hole spin evolution) as a function of spin for all three accretion disk types. Black lines show evolution with only accretion included, while blue lines show the total including jet spindown. These plots show that the thin disk is always spun up to to :math:`a_\mathrm{eq}=1`, even with jets (due to low jet efficiencies). The advection-dominated disks (thick and slim disk) are spun up to positive equilibrium values :math:`a_\mathrm{eq}<1`, or spun down to such an equilibrium value if :math:`a>a_\mathrm{eq}`. This is due to extraction of rotational energy from the BH by frame dragging and transport of the angular momentum to large distances through viscous forces. Including jet spindown pushes these equilibrium spins to even smaller values.
+    Spinup/spindown function (the dimensionless rate of black hole spin evolution) as a function of spin for all three accretion disk types. For the thin and slim disk, we show several curves for different choices of the Eddington ratio.
 
 Evolution of the black hole spin direction
 ------------------------------------------
@@ -149,7 +190,7 @@ In all cases, Lense-Thirring torques are effective only within some radius :math
 
 In terms of the evolution of the spin direction, the main assumption of our model is as follows (see `King et al. 2005 <https://ui.adsabs.harvard.edu/abs/2005MNRAS.363...49K/abstract>`_ for the original argument, and additional discussions in e.g. `Fanidakis et al. 2011 <https://ui.adsabs.harvard.edu/abs/2011MNRAS.410...53F/abstract>`_, `Fiacconi et al. 2018 <https://ui.adsabs.harvard.edu/abs/2018MNRAS.477.3807F/abstract>`_ and `Griffin et al. 2019a <https://ui.adsabs.harvard.edu/abs/2019MNRAS.487..198G/abstract>`_). All matter that flows through an accretion disk is aligned or counteraligned with the BH spin vector in the accretion process. Due to conservation of angular momentum, the spin vector itself also has to adjust to keep the total angular momentum conserved. In the process of consuming one warp mass :math:`M_\mathrm{warp}`, the direction of the BH spin vector is aligned to match the direction of the total angular momentum of the system comprising the BH and the disk out to the warp radius. The direction of the BH spin vector can then be determined from :math:`\vec{J}_\mathrm{warp}=\vec{J}_\mathrm{BH}+J_\mathrm{warp}\hat{J}_\mathrm{d}`, where :math:`\vec{J}_\mathrm{BH}` is the old BH angular momentum vector, and :math:`\hat{J}_\mathrm{d}` is the direction of the large-scale accretion disk (which we assume matches the direction of the angular momentum of the gas in the BH smoothing kernel).
 
-In practice, the BH will consume parcels of mass that differ from :math:`M_\mathrm{warp}`. We assume that any such parcel of mass :math:`\Delta M` (e.g. the mass to be consumed within a single time step) can be split up onto :math:`n=\Delta M / M_\mathrm{warp}` individual increments of accretion, so the total angular momentum of the system within that time step is :math:`\vec{J}_\mathrm{warp}=\vec{J}_\mathrm{BH}+n J_\mathrm{warp}\hat{J}_\mathrm{d}`, i.e. :math:`n` warp angular momenta are consumed, with an angular momentum of :math:`\Delta \vec{J}=n J_\mathrm{warp}\hat{J}_\mathrm{d}=(J_\mathrm{warp}/M_\mathrm{warp})\Delta M `. This can also be viewed as the BH consuming material with a specific angular momentum of :math:`L_\mathrm{warp}=J_\mathrm{warp}/M_\mathrm{warp}`. Note that this picture is only valid if the BH spin vector does not change much during this process (in both magnitude and direction), which can be ensured with wisely chosen time steps.
+In practice, the BH will consume parcels of mass that differ from :math:`M_\mathrm{warp}`. We assume that any such parcel of mass :math:`\Delta M` (e.g. the mass to be consumed within a single time step) can be split up onto :math:`n=\Delta M / M_\mathrm{warp}` individual increments of accretion, so the total angular momentum of the system within that time step is :math:`\vec{J}_\mathrm{warp}=\vec{J}_\mathrm{BH}+n J_\mathrm{warp}\hat{J}_\mathrm{d}`, i.e. :math:`n` warp angular momenta are consumed, with an angular momentum of :math:`\Delta \vec{J}=n J_\mathrm{warp}\hat{J}_\mathrm{d}=(J_\mathrm{warp}/M_\mathrm{warp})\Delta M`. This can also be viewed as the BH consuming material with a specific angular momentum of :math:`L_\mathrm{warp}=J_\mathrm{warp}/M_\mathrm{warp}`. Note that this picture is only valid if the BH spin vector does not change much during this process (in both magnitude and direction), which can be ensured with wisely chosen time steps.
 
 Deciding whether accretion is prograde or retrograde
 ----------------------------------------------------
@@ -172,34 +213,34 @@ As mentioned already, Lense-Thirring torques have different effects depending on
 (`Ogilvie 1999 <https://ui.adsabs.harvard.edu/abs/1999MNRAS.304..557O/abstract>`_, see also `Lodato et al. 2010 <https://ui.adsabs.harvard.edu/abs/2010MNRAS.405.1212L/abstract>`_ for a detailed discussion). We use the relation :math:`\dot{M}=3\pi\nu_1 \Sigma` to calculate :math:`\nu_1`, and therefore :math:`\nu_2`. The warp radius will depend on which region of the thin disc we assume, with each having its own expression for :math:`\Sigma`. In region b) of the `Shakura & Sunyaev (1973) <https://ui.adsabs.harvard.edu/abs/1973A%26A....24..337S/abstract>`_ thin disk, the surface density can be expressed as
 
 .. math::
-    \Sigma_\mathrm{TD,b}=6.84 \times 10^{5} \mathrm{~g} \mathrm{~cm}^{-2} \alpha^{-4 / 5} \dot{m}^{3 / 5}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{1 / 8}\left(\frac{R}{R_{\mathrm{S}}}\right)^{-3 / 5},
+    \Sigma_\mathrm{TD,b}=6.84 \times 10^{5} \mathrm{~g} \mathrm{~cm}^{-2} \alpha^{-4 / 5} f_\mathrm{Edd}^{3 / 5}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{1 / 8}\left(\frac{R}{R_{\mathrm{S}}}\right)^{-3 / 5},
     
 while in region c) we have
 
 .. math::
-    \Sigma_\mathrm{TD,c}=3.41 \times 10^{4} \mathrm{~g} \mathrm{~cm}^{-2} \alpha^{-4 / 5} \dot{m}^{7/10}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{1 / 20}\left(\frac{R}{R_{\mathrm{S}}}\right)^{-3 / 4}.
+    \Sigma_\mathrm{TD,c}=3.41 \times 10^{4} \mathrm{~g} \mathrm{~cm}^{-2} \alpha^{-4 / 5} f_\mathrm{Edd}^{7/10}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{1 / 20}\left(\frac{R}{R_{\mathrm{S}}}\right)^{-3 / 4}.
     
 These relations lead to the following expressions for :math:`R_\mathrm{warp}`:
 
 .. math::
-    R_{\text {warp,TD,b}}=3410 R_{S} a^{5 / 8} \xi^{-5/8}\alpha^{-1 / 2} \dot{m}^{-1 / 4}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{1 / 8}
+    R_{\text {warp,TD,b}}=3410 R_{S} a^{5 / 8} \xi^{-5/8}\alpha^{-1 / 2} f_\mathrm{Edd}^{-1 / 4}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{1 / 8}
     
 (in region b) and
 
 .. math::
-    R_\mathrm{warp,TD,c}=2629R_\mathrm{S}a^{4/7}\xi^{-4/7}\alpha^{-16/35}\dot{m}^{-6/35}\bigg(\frac{M_\mathrm{BH}}{10^8\hspace{0.5mm}\mathrm{M}_\odot}  \bigg)^{4/35},
+    R_\mathrm{warp,TD,c}=2629R_\mathrm{S}a^{4/7}\xi^{-4/7}\alpha^{-16/35}f_\mathrm{Edd}^{-6/35}\bigg(\frac{M_\mathrm{BH}}{10^8\hspace{0.5mm}\mathrm{M}_\odot}  \bigg)^{4/35},
     
 (in region c), with :math:`R_\mathrm{S}=2R_\mathrm{G}` the Schwarzschild radius. These warp radii are generally of order :math:`\approx1000R_\mathrm{G}`, which can lead to fairly quick alignment of the thin disk with the large-scale angular momentum direction (quicker than any significant evolution in mass or spin magnitude, illustrating why the inclusion of the effects of Lense-Thirring torques is important).
 
 In the context of thin disks, there is a futher complication. The self-gravity of the disk may become important at large radii (see `Lodato 2007 <https://www.sif.it/riviste/sif/ncr/econtents/2007/030/07/article/0>`_ for a review). The disk will fragment in the region where the Toomre parameter is :math:`Q(R)>1`. We thus assume that the disk extends out to where :math:`Q(R_\mathrm{sg})=1`. The self-gravity radius :math:`R_\mathrm{sg}` can be calculated from this condition and the definition of the Toomre parameter :math:`Q=\Omega c_{\mathrm{s}} /(\pi G \Sigma)`, yielding
 
 .. math::
-    R_{\text {sg,TD,b}}=6460 R_{S} \alpha^{28/51} \dot{m}^{-18/51}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{-49/51}
+    R_{\text {sg,TD,b}}=6460 R_{S} \alpha^{28/51} f_\mathrm{Edd}^{-18/51}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{-49/51}
     
 in region b) and
 
 .. math::
-    R_\mathrm{sg,TD,c}=2456 R_{S} \alpha^{28/45} \dot{m}^{-22/45}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{-52/45}
+    R_\mathrm{sg,TD,c}=2456 R_{S} \alpha^{28/45} f_\mathrm{Edd}^{-22/45}\left(\frac{M_{\mathrm{BH}}}{10^{8} M_{\odot}}\right)^{-52/45}
     
 in region c). In all our calculations involving :math:`R_\mathrm{warp}` (for deciding the sign of spin and evolving the direction of angular momentum, as described in the preceeding sections), we always take the minimum of :math:`R_\mathrm{warp}` and :math:`R_\mathrm{sg}`. This is because if :math:`R_\mathrm{sg}<R_\mathrm{warp}`, the entire disk of extent :math:`R_\mathrm{sg}` will be warped.
 
@@ -212,7 +253,10 @@ The exact behaviour of the thick and slim disk (which we will collectively call
 .. math::
     R_\mathrm{warp,adv}=R_\mathrm{G}\bigg(\frac{384a}{25(H/R)^2}\bigg)^{2/5}.
     
-In our model, we assume that the inner regions of the disks are on average aligned or counteraligned with the spin vector (one can think of this as averaging over the precession, which has periods of :math:`\approx`days, over long enough time scales). For simplicity, we  also refer to the radii within which this is true as the warp radii. For both of the advection-dominated disks, these radii are only of order several :math:`R_\mathrm{G}`. Note that similar values are found if one assumes that the Bardeen-Peterson effect operates in these disks. While there are some uncertainties in the assumptions we have made, we point out that using any of these values is much more physically motivated than using thin disk equations (the warp radii of order thousands of :math:`R_\mathrm{G}`), which is what is often done (e.g. `Griffin et al. 2019a <https://ui.adsabs.harvard.edu/abs/2019MNRAS.487..198G/abstract>`_, `Dubois et al. 2012 <https://ui.adsabs.harvard.edu/abs/2014MNRAS.440.1590D/abstract>`_).
+In our model, we assume that the inner regions of the disks are on
+average aligned or counteraligned with the spin vector (one can think
+of this as averaging over the precession, which has periods of
+:math:`\approx` days, over long enough time scales). For simplicity, we  also refer to the radii within which this is true as the warp radii. For both of the advection-dominated disks, these radii are only of order several :math:`R_\mathrm{G}`. Note that similar values are found if one assumes that the Bardeen-Peterson effect operates in these disks. While there are some uncertainties in the assumptions we have made, we point out that using any of these values is much more physically motivated than using thin disk equations (the warp radii of order thousands of :math:`R_\mathrm{G}`), which is what is often done (e.g. `Griffin et al. 2019a <https://ui.adsabs.harvard.edu/abs/2019MNRAS.487..198G/abstract>`_, `Dubois et al. 2012 <https://ui.adsabs.harvard.edu/abs/2014MNRAS.440.1590D/abstract>`_).
 
 In order to determine the sign of spin and evolve the angular momentum direction, expressions for the warp mass :math:`M_\mathrm{warp}` and warp angular momentum :math:`J_\mathrm{warp}` are also needed. We calculate this using surface integrals as
 
@@ -234,9 +278,9 @@ where :math:`v_\mathrm{r}=-\alpha v_0 v_\mathrm{K}` is the radial velocity. Here
 Black hole mergers
 ------------------
 
-In the process of merging, BHs interact in a very complicated manner. Their final spin is not trivial to predict, and it can depend on a very large parameter space (including the mass ratio of the black holes and the relative orientation and magnitude of the spins). Orbital angular momentum plays a role in the merger as well. We use the fitting function found by `Rezzolla et al. (2007) <https://journals.aps.org/prd/abstract/10.1103/PhysRevD.78.044002>`_, whose results have been found to be very accurate in newer and more sophisticated studies that sweep the huge parameter space of possible merger configurations. The only flaw in these formulas is that they do not include the effects of gravitational radiation. However, the effects of this radiation is confined to a :math:`\approx10\%` level, and only if either of the spin vectors is aligned or counteraligned with the direction of the orbital angular momentum (if it is not, the fits are even more accurate).
+In the process of merging, BHs interact in a very complicated manner. Their final spin is not trivial to predict, and it can depend on a very large parameter space (including the mass ratio of the black holes and the relative orientation and magnitude of the spins). Orbital angular momentum plays a role in the merger as well. We use the fitting function found by `Rezzolla et al. (2009) <https://ui.adsabs.harvard.edu/abs/2009CQGra..26i4023R/abstract>`_, whose results have been found to be very accurate in newer and more sophisticated studies that sweep the huge parameter space of possible merger configurations. These formulas are also applicable to cosmological simulations, since they cover the scenario of inspiral from very large distances.
 
-The final spin, according to `Rezzolla et al. (2007) <https://journals.aps.org/prd/abstract/10.1103/PhysRevD.78.044002>`_ can be calculated as
+The final spin, according to `Rezzolla et al. (2009) <https://ui.adsabs.harvard.edu/abs/2009CQGra..26i4023R/abstract>`_ can be calculated as
 
 .. math::
     \mathbf{a}_\mathrm{fin} = \frac{1}{(1+q)^2}(\mathbf{a}_1+\mathbf{a}_2q^2+\mathbf{l}q),
@@ -248,4 +292,14 @@ where :math:`q=M_2/M_1` is the mass ratio (such that :math:`M_2<M_1`), :math:`\m
     \left(\frac{s_{5} \mu+t_{0}+2}{1+q^{2}}\right)\left(\left|\mathbf{a}_{1}\right| \cos \theta+\left|\mathbf{a}_{2}\right| q^{2} \cos \xi\right)+ \\
     2 \sqrt{3}+t_{2} \mu+t_{3} \mu^{2}.
 
-Here, :math:`\mu=q/(1+q)^2` is the symmetric mass ratio, and :math:`s_4 = -0.129`, :math:`s_5 = -0.384`, :math:`t_0 = -2.686`, :math:`t_2 = -3.454`, :math:`t_3 = 2.353`. The three cosines depend on the angles between the different vectors which play a role in the merger: :math:`\cos \phi=\hat{\mathbf{a}_{1}} \cdot \hat{\mathbf{a}_{\mathbf{2}}}`, :math:`\cos \theta=\hat{\mathbf{a}_{1}} \cdot \hat{\mathbf{l}}`, :math:`\cos \xi=\hat{\mathbf{a}_{2}} \cdot \hat{\mathbf{l}}`.
+Here, :math:`\mu=q/(1+q)^2` is the symmetric mass ratio, and :math:`s_4 = -0.1229`, :math:`s_5 = -0.4537`, :math:`t_0 = -2.8904`, :math:`t_2 = -3.5171`, :math:`t_3 = 2.5763`. The three cosines depend on the angles between the different vectors which play a role in the merger: :math:`\cos \phi=\hat{\mathbf{a}_{1}} \cdot \hat{\mathbf{a}_{\mathbf{2}}}`, :math:`\cos \theta=\hat{\mathbf{a}_{1}} \cdot \hat{\mathbf{l}}`, :math:`\cos \xi=\hat{\mathbf{a}_{2}} \cdot \hat{\mathbf{l}}`.
+
+Given the information available within the model, we could in principle calculate the recoil velocity of the remnant, as well as the total mass fraction lost to gravitational waves. We do not implement the former at this stage since we cannot reliably track the movement of black holes in their host galaxies. However, we do implement the latter. We use results from the same series of numerical relativity simulations as above (`Barausse et al. 2012 <https://ui.adsabs.harvard.edu/abs/2012ApJ...758...63B/abstract>`_) and write the final mass of the remnant as:
+
+.. math::
+    M_\mathrm{BH,fin} = (M_\mathrm{BH,1}+M_\mathrm{BH,2})\Big\{1 - [1 - e_\mathrm{ISCO}(\tilde{a})]\mu  - 4\mu^2[4p_0+16p_1\tilde{a}(\tilde{a}+1)+e_\mathrm{ISCO}(\tilde{a})-1]\Big\},
+
+where :math:`p_0=0.04827`, :math:`p_1=0.01707` and :math:`e_\mathrm{ISCO}(\tilde{a})` is the dimensionless specific binding energy at the innermost stable circular orbit calculated using an effective spin variable defined as 
+
+.. math::
+    \tilde{a} = \frac{|\mathbf{a_1}|\cos\theta+|\mathbf{a_2}|\cos\xi}{(1+q)^2}.
diff --git a/doc/RTD/source/SubgridModels/AGNSpinJets/variable_heating_temperatures.rst b/doc/RTD/source/SubgridModels/AGNSpinJets/variable_heating_temperatures.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6869b1d9c8de806689b1254fe653e79360777a3b
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/AGNSpinJets/variable_heating_temperatures.rst
@@ -0,0 +1,38 @@
+.. AGN spin and jet model
+   Filip Husko, 26 September 2023
+
+.. AGN_spin_jet:
+
+Variable AGN heating temperature model
+--------------------------------------
+
+As part of this AGN model we also introduce an option of using variable AGN heating temperatures, as opposed to a constant value. This can be turned on by setting ``AGN_heating_temperature_model`` to ``AGN_heating_temperature_local``. In this scheme, we use three different criteria, all of which are applied simultaneously, and all of which are numerically motivated. These are: 1) the `Dalla Vecchia & Schaye (2012) <https://ui.adsabs.harvard.edu/abs/2012MNRAS.426..140D/abstract>`_ to prevent numerical overcooling, 2) a replenishment condition to make sure that the BH smoothing kernel can be replenished at a rate similar to the rate of evacuation due to heating and 3) a crossing condition that ensures a sufficient separation of heating events in time such that the kernel is never expected to contain more than a single heated particle.
+
+We implement the `Dalla Vecchia & Schaye (2012) <https://ui.adsabs.harvard.edu/abs/2012MNRAS.426..140D/abstract>`_ condition using Eqn. (18) from the paper. We invert it to obtain the minimum heating temperature:
+
+.. math::
+    \Delta T_\mathrm{DV}= 1.8\times10^6\hspace{0.5mm}\mathrm{K}\hspace{0.5mm}\times\bigg(\frac{N_\mathrm{ngb}m_\mathrm{g}}{60\times10^6\hspace{0.5mm}\mathrm{M}_\odot} \bigg)^{1/3}\bigg(\frac{n_\mathrm{H}}{0.1\hspace{0.5mm}\mathrm{cm}^{-3}} \bigg)^{2/3},
+
+where :math:`N_\mathrm{ngb}` is the number of neighbours in the BH kernel, :math:`m_\mathrm{g}` the average neighbour mass and :math:`n_\mathrm{H}` the number density of gas in the BH kernel.
+
+We derive the second condition by comparing the time-scales of replenishment onto the kernel and evacuation out of it on account of gas heating. The evacuation time-scale is the time required to evacuate the entire kernel of gas, and it is given by :math:`t_\mathrm{evac}=m_\mathrm{g}N_\mathrm{ngb}/\dot{M}`, where :math:`\dot{M}` is the mass flux associated with feedback. It can be expressed using the feedback power :math:`P` and internal energy per unit mass :math:`e` as :math:`\dot{M}=P/e`, where :math:`e=3k_\mathrm{B}\Delta T/2\mu m_\mathrm{p}`, :math:`\Delta T` is the heating temperature, :math:`m_\mathrm{p}` the proton mass and :math:`\mu\approx0.6` the mean molecular weight for ionized gas.
+
+The replenishment time-scale is given by :math:`t_\mathrm{repl}=h/v_\mathrm{repl}`, where :math:`v_\mathrm{repl}` is an effective velocity with which gas can flow inwards to replenish the kernel. Given this formulation, we can set the two time-scales (of evacuation and replenishment) equal to each other and solve for a heating temperature that ensures timely replenishment:
+
+.. math::
+    \Delta T_\mathrm{repl} = \frac{2\mu m_\mathrm{p}}{3 k_\mathrm{B}}\frac{hP}{N_\mathrm{ngb}m_\mathrm{g}v_\mathrm{repl}}.
+
+Finally, we calculate the replenishment velocity :math:`v_\mathrm{repl}` as follows. We assume that gas can replenish the kernel under the effects of either gas turbulence or pressure. We thus express the replenishment velocity as :math:`v_\mathrm{repl} = \max(\sigma,\Tilde{c}_\mathrm{s})`, where :math:`\sigma` is the velocity dispersion of gas and :math:`\Tilde{c}_\mathrm{s}` an effective sound speed, which we calculate by assuming that there is always some ISM exherting its pressure on gas near the BH, even if all of the gas in the kernel is cold. We choose the form :math:`\Tilde{c}_\mathrm{s}=\max(c_\mathrm{s,hot},\hspace{0.5mm}10\hspace{0.5mm}\mathrm{km}\mathrm{s}^{-1})`, where :math:`c_\mathrm{s,hot}` is the kernel-weighted average sound speed of all particles that have a temperature :math:`T>10^4` K and :math:`10` km :math:`\mathrm{s}^{-1}` is the sound speed of the ISM assuming a typical temperature of :math:`T=10^4` K.
+
+The third and final condition we use is based on the time it takes a single heated particle to cross and exit the kernel before the next one is heated. The time-interval between two heating events is :math:`\Delta = m_\mathrm{g}/\dot{M}`, while the time required for a heated particle to cross the kernel is :math:`\Delta t_\mathrm{cross}= h/c_\mathrm{s,\Delta T}`, where :math:`c_\mathrm{s,\Delta T} = \sqrt{\gamma(\gamma-1)e} = \sqrt{5k_\mathrm{B}\Delta T/3\mu m_\mathrm{p}}` is the sound speed of the heated gas. Equating these two time-scales, we obtain the final heating temperature:
+
+.. math::
+    \Delta T_\mathrm{cross} = \frac{\mu m_\mathrm{p}}{k_\mathrm{B}}\bigg(\frac{2hP}{\sqrt{15}m_\mathrm{g}}\bigg)^{2/3}.
+
+The final heating temperature scheme we use can be written as:
+
+.. math::
+    \Delta T = \max[\xi \max(\Delta T_\mathrm{DV}, \Delta T_\mathrm{repl}, \Delta T_\mathrm{cross}), \Delta T_\mathrm{min}],
+
+where :math:`\Delta T_\mathrm{min}` is an additional temperature floor meant to prevent very-low temperature heating events, and :math:`\xi` is an additional free parameter that can be used to rescale heating temperatures from all three conditions to higher values, which may be necessary to correctly calibrate the simulations. Specifically, one may use the hot gas fractions to choose a value of :math:`\xi` if :math:`\Delta T_\mathrm{min}` is set to a low value of :math:`\approx10^{7.5}` K, or one may set :math:`\xi=1`, thus using the numerical conditions exactly as derived, and instead calibrate the simulations by varying :math:`\Delta T_\mathrm{min}`.
+
diff --git a/doc/RTD/source/SubgridModels/AGORA/index.rst b/doc/RTD/source/SubgridModels/AGORA/index.rst
index dc3f829e005b0586f072bd408e3acf555889e06e..a48732b5cf03a065c8ba697da0198c8a4d33c78b 100644
--- a/doc/RTD/source/SubgridModels/AGORA/index.rst
+++ b/doc/RTD/source/SubgridModels/AGORA/index.rst
@@ -36,7 +36,7 @@ Star formation
 The AGORA model uses the :ref:`GEAR model <gear_star_formation>` scheme, however with the 
 ``GEARStarFormation:star_formation_mode`` parameter set to ``agora``. Instead of requiring the gas
 density to reach the pressure floor, we simply require it to be denser than a density
-threshold defined by ``GEARStarFormation:density_threshold``.
+threshold defined by ``GEARStarFormation:density_threshold_Hpcm3``.
 
 
 Recommended parameters for the AGORA model should be:
@@ -47,10 +47,10 @@ Recommended parameters for the AGORA model should be:
   GEARStarFormation:
     star_formation_mode: agora            
     star_formation_efficiency: 0.01   
-    maximal_temperature:  1e10       
+    maximal_temperature_K:  1e10
     n_stars_per_particle: 1
     min_mass_frac: 0.5
-    density_threshold:   1.67e-23   
+    density_threshold_Hpcm3:   10
 
 
 
@@ -127,9 +127,6 @@ Recommended parameters for the AGORA model should be:
 
 
 
-
-
-
 .. _agora_pressure_floor:
 
 Pressure Floor
@@ -138,4 +135,12 @@ Pressure Floor
 The AGORA model uses precisely the same pressure floor than the :ref:`GEAR model <gear_pressure_floor>`.
 
 
+.. _agora_initial_conditions:
+
+Initial Conditions
+~~~~~~~~~~~~~~~~~~
 
+Note that if in the initial conditions, the time of formation of a stellar particle is given (``BirthTime``)
+and set to a negative value, the stellar particle will provide no feedback.
+A similar behavior will be obtained if the parameter ``overwrite_birth_time`` is set to 1 and
+``birth_time`` to -1. 
diff --git a/doc/RTD/source/SubgridModels/Basic/index.rst b/doc/RTD/source/SubgridModels/Basic/index.rst
index ea193ef9eb228969eeee5a9fdd387193a373ec4f..0a6ea4b5be1468a8637c9955bd407f44e452501a 100644
--- a/doc/RTD/source/SubgridModels/Basic/index.rst
+++ b/doc/RTD/source/SubgridModels/Basic/index.rst
@@ -5,6 +5,25 @@
 Basic model (others)
 ====================
 
+Sinks: Simple Bondi-Hoyle accretion
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
+
+The ``Basic`` sink model provides a foundation on which new sink implementations could be built. It includes a prescription for Bondi-Hoyle gas accretion, and a method for sink-sink mergers that is a slightly simplified version of the implementation used in GEAR.
+
+No other physics is implemented for this model. Sinks cannot form - to use this model, sink particles must already be present in the initial conditions. They also cannot spawn stars from the gas they accrete.
+
+Bondi-Hoyle accretion can be done in one of two ways:
+
+ * Gas particles within the sink's kernel are stochastically swallowed entirely, with a probability set by the Bondi-Hoyle rate. Specifically, the probability is set by the current difference between the sink's subgrid mass (determined by the accretion rate) and its dynamical mass (which tracks the number of particles/sinks actually swallowed). This mode is equivalent to the EAGLE black hole accretion model.
+ * Gas particles within the sink's kernel are "nibbled" down to some minimal mass, which can be specified by the user. This method is equivalent to the black hole accretion model of Bahe et al. 2022.
+
+This model has only two parameters that must be specified in your parameter ``yml`` file:
+
+ * ``BasicSink:use_nibbling``: determines whether accretion is done by "nibbling" or by swallowing outright.
+ * ``BasicSink:min_gas_mass_for_nibbling_Msun``: if using "nibbling", the minimum mass to which gas particles can be nibbled. A good default is half the original particle mass.
+
+For an even more bare-bones starting point, the ``Default`` sink model contains no physics at all, and is a totally blank canvas on which to build your sink model.
+
 
 Cooling: Analytic models
 ~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/RTD/source/SubgridModels/EAGLE/index.rst b/doc/RTD/source/SubgridModels/EAGLE/index.rst
index 25272911352a41ecb10f4144ab54153a2afeb29a..59daa875c2ca0dbdb65fa153f783a8ec20d59af3 100644
--- a/doc/RTD/source/SubgridModels/EAGLE/index.rst
+++ b/doc/RTD/source/SubgridModels/EAGLE/index.rst
@@ -537,6 +537,15 @@ Note that the star formation rates are expressed in internal units and not in
 solar masses per year as is the case in many other codes. This choice ensures
 consistency between all the fields written to the snapshots.
 
+Finally, the star formation model can also create more than one star particle
+per gas particle in a star formation event. The number of particles generated is
+controlled by a runtime parameter. All the stars generated share the same
+properties. They are slightly displaced from each others using a random vector
+of magnitude :math:`0.1h` added to the position of the gas particle. If only
+one star is formed (as is the default), no displacement is added. If N stars are
+formed per gas particle, each star is born with a mass of 1/N of the gas
+particle's mass.
+
 For a normal EAGLE run, that section of the parameter file reads:
 
 .. code:: YAML
@@ -559,7 +568,8 @@ For a normal EAGLE run, that section of the parameter file reads:
      threshold_max_density_H_p_cm3:     10.0      # Maximal density of the metal-dependant density threshold for star formation in Hydrogen atoms per cm^3.
      min_over_density:                  57.7      # Over-density above which star-formation is allowed.
      EOS_entropy_margin_dex:            0.5       # (Optional) Logarithm base 10 of the maximal entropy above the EOS at which stars can form.
-
+     num_of_stars_per_gas_particle:     1         # (Optional) The number star particles to form per gas particle converted to stars. (Defaults to 1. Must be > 0)
+     
 
 Alternatively, the code can also use a simple Schmidt law for the SF rate
 :math:`\dot{m}_* = \epsilon_{ff} \times m_g \times \frac{3 \pi} {32 G
diff --git a/doc/RTD/source/SubgridModels/GEAR/chemistry.rst b/doc/RTD/source/SubgridModels/GEAR/chemistry.rst
new file mode 100644
index 0000000000000000000000000000000000000000..aefa7a3608b9253cb2dae38ac77cf35fdad43bb9
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/chemistry.rst
@@ -0,0 +1,27 @@
+.. GEAR sub-grid model chemistry
+   Darwin Roduit, 30th March 2025
+
+.. gear_chemistry:
+
+.. _gear_chemistry:
+
+Chemistry
+=========
+
+Feedback mechanisms such as supernova feedback transfer metals to the nearby gas particles. There is no mass exchange (mass advection) in SPH methods, as in grid-based or moving-mesh hydrodynamics solvers. As a result, the metals received by the gas particles are locked in these particles. Grid or moving-mesh codes have mass advection and often implement passive scalar advection of metals to model metal mixing. GEAR implements different methods to model metal mixing.
+
+.. _gear_smoothed_metallicity:
+
+Smoothed metallicity
+--------------------
+
+The smoothed metallicity scheme consists in using the SPH to smooth the metallicity of each particle over the neighbors. It is worth to point the fact that we are *not exchanging* any metals but only smoothing it. The parameter ``GEARChemistry:initial_metallicity`` set the (non smoothed) initial mass fraction of each element for all the particles and ``GEARChemistry:scale_initial_metallicity`` use the feedback table to scale the initial metallicity of each element according the Sun's composition. If ``GEARChemistry:initial_metallicity`` is negative, then the metallicities are read from the initial conditions.
+
+For this chemistry scheme the parameters are:
+
+.. code:: YAML
+
+   GEARChemistry:
+    initial_metallicity: 1         # Initial metallicity of the gas (mass fraction)
+    scale_initial_metallicity: 1   # Should we scale the initial metallicity with the solar one?
+
diff --git a/doc/RTD/source/SubgridModels/GEAR/feedback.rst b/doc/RTD/source/SubgridModels/GEAR/feedback.rst
new file mode 100644
index 0000000000000000000000000000000000000000..72a8996d29e51575e367609868e5ced72ac2a48d
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/feedback.rst
@@ -0,0 +1,144 @@
+.. GEAR sub-grid model feedback and stellar evolution
+   Loic Hausammann, 17th April 2020
+   Darwin Roduit, 30th March 2025
+
+.. _gear_stellar_evolution_and_feedback:  
+
+Stellar evolution and feedback
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The feedback is composed of a few different models:
+  - The initial mass function (IMF) defines the quantity of each type of stars,
+  - The lifetime of a star defines when a star will explode (or simply die),
+  - The supernovae of type II (SNII) defines the rates and yields,
+  - The supernovae of type Ia (SNIa) defines the rates and yields,
+  - The energy injection that defines how to inject the energy / metals into the particles.
+
+Most of the parameters are defined inside a table (``GEARFeedback:yields_table``) but can be override with some parameters in the YAML file.
+I will not describe theses parameters more than providing them at the end of this section.
+Two different models exist for the supernovae (``GEARFeedback:discrete_yields``).
+In the continuous mode, we integrate the quantities over the IMF and then explodes a floating point number of stars (can be below 1 in some cases).
+In the discrete mode, we avoid the problem of floating points by rounding the number of supernovae (using a floor and randomly adding a supernovae depending on the fractional part) and then compute the properties for a single star at a time.
+
+Initial mass function
+^^^^^^^^^^^^^^^^^^^^^
+
+GEAR is using the IMF model from `Kroupa (2001) <https://ui.adsabs.harvard.edu/abs/2001MNRAS.322..231K/abstract>`_.
+We have a difference of 1 in the exponent due to the usage of IMF in mass and not in number.
+We also restrict the mass of the stars to be inside :math:`[0.05, 50] M_\odot`.
+Here is the default model used, but it can be easily adapted through the initial mass function parameters:
+
+.. math::
+  \xi(m) \propto m^{-\alpha_i}\, \textrm{where}\,
+  \begin{cases}
+   \alpha_0 = 0.3,\, & 0.01 \leq m / M_\odot < 0.08, \\
+   \alpha_1 = 1.3,\, & 0.08 \leq m / M_\odot < 0.50, \\
+   \alpha_2 = 2.3,\, & 0.50 \leq m / M_\odot < 1.00, \\
+   \alpha_3 = 2.3,\, & 1.00 \leq m / M_\odot,
+  \end{cases}
+
+
+Lifetime
+^^^^^^^^
+
+The lifetime of a star in GEAR depends only on two parameters: first its mass and then its metallicity.
+
+.. math::
+   \log(\tau(m)) = a(Z) \log^2(m) + b(Z) \log(m) + c(Z) \\ \\
+   a(Z) = -40.110 Z^2 + 5.509 Z + 0.7824 \\
+   b(Z) = 141.929 Z^2 - 15.889 Z - 3.2557 \\
+   c(Z) = -261.365 Z^2 + 17.073 Z + 9.8661
+
+where :math:`\tau` is the lifetime in years, :math:`m` is the mass of the star (in solar mass) and Z the metallicity of the star.
+The parameters previously given are the default ones, they can be modified in the parameters file.
+
+Supernovae II
+^^^^^^^^^^^^^
+
+The supernovae rate is simply given by the number of stars massive enough that end their life at the required time.
+
+.. math::
+   \dot{N}_\textrm{SNII}(t) = \int_{M_l}^{M_u} \delta(t - \tau(m)) \frac{\phi(m)}{m} \mathrm{d}m
+
+where :math:`M_l` and :math:`M_u` are the lower and upper mass limits for a star exploding in SNII, :math:`\delta` is the Dirac function and :math:`\phi` is the initial mass function (in mass).
+
+The yields for SNII cannot be written in an analytical form, they depend on a few different tables that are based on the work of `Kobayashi et al. (2000) <https://ui.adsabs.harvard.edu/abs/2000ApJ...539...26K/abstract>`_ and `Tsujimoto et al. (1995) <https://ui.adsabs.harvard.edu/abs/1995MNRAS.277..945T/abstract>`_.
+
+Supernovae Ia
+^^^^^^^^^^^^^
+
+The supernovae Ia are a bit more complicated as they involve two different stars.
+
+.. math::
+  \dot{N}_\textrm{SNIa}(t) = \left( \int_{M_{p,l}}^{M_{p,u}} \frac{\phi(m)}{m} \mathrm{d}m \right) \sum_i b_i \int_{M_{d,l,i}}^{M_{d,u,i}}
+  \delta(t-\tau(m)) \frac{\phi_d(m)}{m}\mathrm{d}m
+
+.. math::
+   \phi_d(m) \propto m^{-0.35}
+
+where :math:`M_{p,l}` and :math:`M_{p,u}` are the mass limits for a progenitor of a white dwarf, :math:`b_i` is the probability to have a companion and
+:math:`M_{d,l,i}` and :math:`M_{d,u,i}` are the mass limits for each type of companion.
+The first parenthesis represents the number of white dwarfs and the second one the probability to form a binary.
+
++------------------+--------------------+-------------------+------------------+
+| Companion        |  :math:`M_{d,l,i}` | :math:`M_{d,u,i}` | :math:`b_i`      |
++==================+====================+===================+==================+
+| Red giant        |   0.9              |    1.5            |    0.02          |
++------------------+--------------------+-------------------+------------------+
+| Main sequence    |   1.8              |    2.5            |    0.05          |
++------------------+--------------------+-------------------+------------------+
+
+The yields are based on the same papers than the SNII.
+
+Supernova energy injection
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When a star goes into a supernova (type II and Ia), the stellar evolution determines how much energy (``GEARFeedback:supernovae_energy_erg`` TO BE CHECKED), mass and metals are released during the explosion. The energy can be ditributed as internal/thermal energy or as momentum. Thus, we need to distribute internal energy, momentum, mass and metals to the gas particles.  We will group all these in the “fluxes” term. 
+
+We have two models for the distribution of these fluxes and the subgrid modelling of the supernovae : GEAR model and GEAR mechanical model. We describe the two schemes in the  :ref:`gear_sn_feedback_models` page.
+
+
+Generating a new table
+^^^^^^^^^^^^^^^^^^^^^^
+
+The feedback table is an HDF5 file with the following structure:
+
+.. graphviz:: feedback_table.dot
+
+where the solid (dashed) squares represent a group (a dataset) with the name of the object underlined and the attributes written below. Everything is in solar mass or without units (e.g. mass fraction or unitless constant).
+In ``Data``, the attribute ``elts`` is an array of string with the element names (the last should be ``Metals``, it corresponds to the sum of all the elements), ``MeanWDMass`` is the mass of the white dwarfs
+and ``SolarMassAbundances`` is an array of float containing the mass fraction of the different element in the sun.
+In ``IMF``, ``n + 1`` is the number of part in the IMF, ``as`` are the exponent (``n+1`` elements), ``ms`` are the mass limits between each part (``n`` elements) and
+``Mmin`` (``Mmax``) is the minimal (maximal) mass of a star.
+In ``LifeTimes``, the coefficient are given in the form of a single table (``coeff_z`` with a 3x3 shape).
+In ``SNIa``, ``a`` is the exponent of the distribution of binaries, ``bb1``  and ``bb2`` are the coefficient :math:`b_i` and the other attributes follow the same names than in the SNIa formulas.
+The ``Metals`` group from the ``SNIa`` contains the name of each elements (``elts``) and the metal mass fraction ejected by each supernovae (``data``) in the same order. They must contain the same elements than in ``Data``.
+Finally for the ``SNII``, the mass limits are given by ``Mmin`` and ``Mmax``. For the yields, the datasets required are ``Ej`` (mass fraction ejected [processed]), ``Ejnp`` (mass fraction ejected [non processed]) and one dataset for each element present in ``elts``. The datasets should all have the same size, be uniformly sampled in log and contains the attributes ``min`` (mass in log for the first element) and ``step`` (difference of mass in log between two elements).
+
+.. code:: YAML
+
+  GEARFeedback:
+    supernovae_energy_erg: 0.1e51                            # Energy released by a single supernovae.
+    yields_table: chemistry-AGB+OMgSFeZnSrYBaEu-16072013.h5  # Table containing the yields.
+    discrete_yields: 0                                       # Should we use discrete yields or the IMF integrated one?
+  GEARInitialMassFunction:
+    number_function_part:  4                       # Number of different part in the IMF
+    exponents:  [0.7, -0.8, -1.7, -1.3]            # Exponents of each part of the IMF
+    mass_limits_msun:  [0.05, 0.08, 0.5, 1, 50]    # Limits in mass between each part of the IMF
+  GEARLifetime:
+   quadratic:  [-40.1107, 5.50992, 0.782432]  # Quadratic terms in the fit
+   linear:  [141.93, -15.8895, -3.25578]      # Linear terms in the fit
+   constant:  [-261.366, 17.0735, 9.86606]    # Constant terms in the fit
+  GEARSupernovaeIa:
+    exponent:  -0.35                      # Exponent for the distribution of companions
+    min_mass_white_dwarf_progenitor:  3   # Minimal mass of a progenitor of white dwarf
+    max_mass_white_dwarf_progenitor:  8   # Maximal mass of a progenitor of white dwarf
+    max_mass_red_giant:  1.5              # Maximal mass for a red giant
+    min_mass_red_giant:  0.9              # Minimal mass for a red giant
+    coef_red_giant:  0.02                 # Coefficient for the distribution of red giants companions
+    max_mass_main_sequence:  2.6          # Maximal mass for a main sequence star
+    min_mass_main_sequence:  1.8          # Minimal mass for a main sequence star
+    coef_main_sequence:  0.05             # Coefficient for the distribution of main sequence companions
+    white_dwarf_mass:  1.38               # Mass of a white dwarf
+  GEARSupernovaeII:
+  interpolation_size:  200                # Number of elements for the interpolation of the data
diff --git a/doc/RTD/source/SubgridModels/GEAR/gear_model.rst b/doc/RTD/source/SubgridModels/GEAR/gear_model.rst
new file mode 100644
index 0000000000000000000000000000000000000000..8714763a895ce78d32a938d83e0ea62b546d4ed5
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/gear_model.rst
@@ -0,0 +1,204 @@
+.. GEAR sub-grid model
+   Loic Hausammann, 17th April 2020
+   Darwin Roduit, 30th March 2025
+
+.. _gear_pressure_floor:
+
+Pressure Floor
+~~~~~~~~~~~~~~
+
+In order to avoid the artificial collapse of unresolved clumps, a minimum in pressure is applied to the particles.
+This additional pressure can be seen as the pressure due to unresolved hydrodynamics turbulence and is given by:
+
+.. math::
+    P_\textrm{Jeans} = \frac{\rho}{\gamma} \frac{4}{\pi} G h^2 \rho N_\textrm{Jeans}^{2/3}
+
+where :math:`\rho` is the density, :math:`\gamma` the adiabatic index, :math:`G` is the gravitational constant,
+:math:`h` the kernel support and :math:`N_\textrm{Jeans}` (``GEARPressureFloor:jeans_factor`` in the parameter file) is the number of particle required in order to resolve a clump.
+
+
+This must be directly implemented into the hydro schemes, therefore only a subset of schemes (Gadget-2, SPHENIX and Pressure-Energy) have the floor available.
+In order to implement it, you need equation 12 in `Hopkins 2013 <https://arxiv.org/abs/1206.5006>`_:
+
+.. math::
+   m_i \frac{\mathrm{d}v_i}{\mathrm{d}t} = - \sum_j x_i x_j \left[ \frac{P_i}{y_i^2} f_{ij} \nabla_i W_{ij}(h_i) + \frac{P_j}{y_j^2} f_{ji} \nabla_j W_{ji}(h_j) \right]
+
+and simply replace the :math:`P_i, P_j` by the pressure with the floor (when the pressure is below the floor).
+Here the :math:`x, y` are simple weights that should never have the pressure floor included even if they are related to the pressure (e.g. pressure-entropy).
+
+.. code:: YAML
+
+   GEARPressureFloor:
+    jeans_factor: 10.       # Number of particles required to suppose a resolved clump and avoid the pressure floor.
+
+
+.. _gear_star_formation:
+
+
+Star formation
+~~~~~~~~~~~~~~
+
+The star formation is done in two steps: first we check if a particle is in the star forming regime and then we use a stochastic approach to transform the gas particles into stars.
+
+A particle is in the star forming regime if:
+ - The velocity divergence is negative (:math:`\nabla\cdot v < 0`),
+ - The temperature is lower than a threshold (:math:`T < T_t` where :math:`T_t` is defined with ``GEARStarFormation:maximal_temperature_K``),
+ - The gas density is higher than a threshold (:math:`\rho > \rho_t` where :math:`\rho_t` is defined with ``GEARStarFormation:density_threshold_Hpcm3``)
+ - The particle reaches the pressure floor (:math:`\rho > \frac{\pi}{4 G N_\textrm{Jeans}^{2/3} h^2}\frac{\gamma k_B T}{\mu m_p}` where :math:`N_\textrm{Jeans}` is defined in the pressure floor).
+
+If ``GEARStarFormation:star_formation_mode`` is set to ``agora``, the condition on the pressure floor is ignored. Its default value is ``default``.
+
+A star will be able to form if a randomly drawn number is below :math:`\frac{m_g}{m_\star}\left(1 - \exp\left(-c_\star \Delta t / t_\textrm{ff}\right)\right)` where :math:`t_\textrm{ff}` is the free fall time, :math:`\Delta t` is the time step of the particle and :math:`c_\star` is the star formation coefficient (``GEARStarFormation:star_formation_efficiency``), :math:`m_g` the mass of the gas particle and :math:`m_\star` the mass of the possible future star. The mass of the star is computed from the average gas mass in the initial conditions divided by the number of possible stars formed per gas particle (``GEARStarFormation:n_stars_per_particle``). When we cannot have enough mass to form a second star (defined with the fraction of mass ``GEARStarFormation:min_mass_frac``), we fully convert the gas particle into a stellar particle. Once the star is formed, we move it a bit in a random direction and fraction of the smoothing length in order to avoid any division by 0.
+
+Currently, only the following hydro schemes are compatible: SPHENIX, Gadget2, minimal SPH, Gasoline-2 and Pressure-Energy.
+Implementing the other hydro schemes is not complicated but requires some careful thinking about the cosmological terms in the definition of the velocity divergence (comoving vs non comoving coordinates and if the Hubble flow is included or not).
+
+.. code:: YAML
+
+  GEARStarFormation:
+    star_formation_efficiency: 0.01   # star formation efficiency (c_*)
+    maximal_temperature_K:  3e4       # Upper limit to the temperature of a star forming particle
+    density_threshold_Hpcm3:   10     # Density threshold (Hydrogen atoms/cm^3) for star formation
+    n_stars_per_particle: 4           # Number of stars that an hydro particle can generate
+    min_mass_frac: 0.5                # Minimal mass for a stellar particle as a fraction of the average mass for the stellar particles.
+
+Initial Conditions
+++++++++++++++++++
+
+Note that if in the initial conditions, the time of formation of a stellar particle is given (``BirthTime``)
+and set to a negative value, the stellar particle will provide no feedback.
+A similar behavior will be obtained if the parameter ``Stars:overwrite_birth_time`` is set to 1 and
+``Stars:birth_time`` to -1.
+
+
+.. _gear_grackle_cooling:
+
+Cooling: Grackle
+~~~~~~~~~~~~~~~~
+   
+Grackle is a chemistry and cooling library presented in `B. Smith et al. 2017 <https://ui.adsabs.harvard.edu/abs/2017MNRAS.466.2217S>`_ 
+(do not forget to cite if used).  Four different modes are available:
+equilibrium, 6 species network (H, H\\( ^+ \\), e\\( ^- \\), He, He\\( ^+ \\)
+and He\\( ^{++} \\)), 9 species network (adds H\\(^-\\), H\\(_2\\) and
+H\\(_2^+\\)) and 12 species (adds D, D\\(^+\\) and HD).  Following the same
+order, the swift cooling options are ``grackle_0``, ``grackle_1``, ``grackle_2``
+and ``grackle_3`` (the numbers correspond to the value of
+``primordial_chemistry`` in Grackle).  It also includes some metal cooling (on/off with ``GrackleCooling:with_metal_cooling``), self-shielding
+methods and UV background (on/off with ``GrackleCooling:with_UV_background``).  In order to use the Grackle cooling, you will need
+to provide a HDF5 table computed by Cloudy (``GrackleCooling:cloudy_table``).
+
+Configuring and compiling SWIFT with Grackle
+++++++++++++++++++++++++++++++++++++++++++++
+
+In order to compile SWIFT with Grackle, you need to provide the options ``with-chemistry=GEAR`` and ``with-grackle=$GRACKLE_ROOT``
+where ``$GRACKLE_ROOT`` is the root of the install directory (not the ``lib``). 
+
+.. warning::
+    (State 2023) Grackle is experiencing current development, and the API is subject
+    to changes in the future. For convenience, a frozen version is hosted as a fork
+    on github here: https://github.com/mladenivkovic/grackle-swift .
+    The version available there will be tried and tested and ensured to work with
+    SWIFT.
+
+    Additionally, that repository hosts files necessary to install that specific 
+    version of grackle with spack.
+
+To compile it, run
+the following commands from the root directory of Grackle:
+``./configure; cd src/clib``.
+Update the variables ``LOCAL_HDF5_INSTALL`` and ``MACH_INSTALL_PREFIX`` in
+the file ``src/clib/Make.mach.linux-gnu``.
+Finish with ``make machine-linux-gnu; make && make install``.
+Note that we require the 64 bit float version of Grackle, which should be the default setting. 
+(The precision can be set while compiling grackle with ``make precision-64``).
+If you encounter any problem, you can look at the `Grackle documentation <https://grackle.readthedocs.io/en/latest/>`_
+
+You can now provide the path given for ``MACH_INSTALL_PREFIX`` to ``with-grackle``.
+
+Parameters
+++++++++++
+
+When starting a simulation without providing the different element fractions in the non equilibrium mode, the code supposes an equilibrium and computes them automatically.
+The code uses an iterative method in order to find the correct initial composition and this method can be tuned with two parameters. ``GrackleCooling:max_steps`` defines the maximal number of steps to reach the convergence and ``GrackleCooling:convergence_limit`` defines the tolerance in the relative error.
+
+In the parameters file, a few different parameters are available.
+
+- ``GrackleCooling:redshift`` defines the redshift to use for the UV background (for cosmological simulation, it must be set to -1 in order to use the simulation's redshift).
+
+- ``GrackleCooling:provide_*_heating_rates`` can enable the computation of user provided heating rates (such as with the radiative transfer) in either volumetric or specific units.
+
+- Feedback can be made more efficient by turning off the cooling during a few Myr (``GrackleCooling:thermal_time_myr``) for the particles touched by a supernovae.
+
+- The self shielding method is defined by ``GrackleCooling:self_shielding_method`` where 0 means no self shielding, > 0 means a method defined in Grackle (see Grackle documentation for more information) and -1 means GEAR's self shielding that simply turn off the UV background when reaching a given density (``GrackleCooling:self_shielding_threshold_atom_per_cm3``).
+
+- The initial elemental abundances can be specified with ``initial_nX_to_nY_ratio``, e.g. if you want to specify an initial HII to H abundance. A negative value ignores the parameter. The complete list can be found below.
+
+A maximal (physical) density must be set with the ``GrackleCooling:maximal_density_Hpcm3 parameter``. The density passed to Grackle is *the minimum of this density and the gas particle (physical) density*. A negative value (:math:`< 0`) deactivates the maximal density, i.e. there is no maximal density limit.
+The purpose of this parameter is the following. The Cloudy tables provided by Grackle are limited in density (typically to  :math:`10^4 \; \mathrm{hydrogen \; atoms/cm}^3`). In high-resolution simulations, particles can have densities higher than :math:`10^4 \; \mathrm{hydrogen \; atoms/cm}^3`. This maximal density ensures that we pass a density within the interpolation ranges of the table, should the density exceed it.
+It can be a solution to some of the following errors (with a translation of what the values mean):
+
+.. code:: text
+
+	  inside if statement solve rate cool:           0           0
+	  MULTI_COOL iter >        10000  at j,k =           1           1
+	  FATAL error (2) in MULTI_COOL
+	  dt =  1.092E-08 ttmin =  7.493E-12
+	  2.8E-19    // sub-cycling timestep
+	  7.5E-12    // time elapsed (in the sub-cycle)
+	  2.2E+25    // derivative of the internal energy
+	  T
+
+.. note::
+   This problem is particularly relevant with metal cooling enabled. Another solution is to modify the tables. But one is not exempted from exceeding the table maximal density value, since Grackle does not check if the particle density is larger than the table maximal density.
+
+
+Complete parameter list
+-----------------------
+
+Here is the complete section in the parameter file:
+
+.. code:: YAML
+
+  GrackleCooling:
+    cloudy_table: CloudyData_UVB=HM2012.h5       # Name of the Cloudy Table (available on the grackle bitbucket repository)
+    with_UV_background: 1                        # Enable or not the UV background
+    redshift: 0                                  # Redshift to use (-1 means time based redshift)
+    with_metal_cooling: 1                        # Enable or not the metal cooling
+    provide_volumetric_heating_rates: 0          # (optional) User provide volumetric heating rates
+    provide_specific_heating_rates: 0            # (optional) User provide specific heating rates
+    max_steps: 10000                             # (optional) Max number of step when computing the initial composition
+    convergence_limit: 1e-2                      # (optional) Convergence threshold (relative) for initial composition
+    thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
+    self_shielding_method: -1                    # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
+    self_shielding_threshold_atom_per_cm3: 0.007 # Required only with GEAR's self shielding. Density threshold of the self shielding
+    maximal_density_Hpcm3:   1e4                 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+    HydrogenFractionByMass : 1.                  # Hydrogen fraction by mass (default is 0.76)
+
+    use_radiative_transfer : 1                   # Arrays of ionization and heating rates are provided
+    RT_heating_rate_cgs    : 0                   # heating         rate in units of / nHI_cgs 
+    RT_HI_ionization_rate_cgs  : 0               # HI ionization   rate in cgs [1/s]
+    RT_HeI_ionization_rate_cgs : 0               # HeI ionization  rate in cgs [1/s]
+    RT_HeII_ionization_rate_cgs: 0               # HeII ionization rate in cgs [1/s]
+    RT_H2_dissociation_rate_cgs: 0               # H2 dissociation rate in cgs [1/s]
+
+    volumetric_heating_rates_cgs: 0              # Volumetric heating rate in cgs  [erg/s/cm3]
+    specific_heating_rates_cgs: 0                # Specific heating rate in cgs    [erg/s/g]
+    H2_three_body_rate : 1                       # Specific the H2 formation three body rate (0->5,see Grackle documentation)
+    H2_cie_cooling : 0                           # Enable/disable H2 collision-induced emission cooling from Ripamonti & Abel (2004)
+    H2_on_dust: 0                                # Flag to enable H2 formation on dust grains
+    local_dust_to_gas_ratio : -1                 # The ratio of total dust mass to gas mass in the local Universe (-1 to use the Grackle default value). 
+    cmb_temperature_floor : 1                    # Enable/disable an effective CMB temperature floor
+
+    initial_nHII_to_nH_ratio:    -1              # initial nHII   to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nHeI_to_nH_ratio:    -1              # initial nHeI   to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nHeII_to_nH_ratio:   -1              # initial nHeII  to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nHeIII_to_nH_ratio:  -1              # initial nHeIII to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nDI_to_nH_ratio:     -1              # initial nDI    to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nDII_to_nH_ratio:    -1              # initial nDII   to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nHM_to_nH_ratio:     -1              # initial nHM    to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nH2I_to_nH_ratio:    -1              # initial nH2I   to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nH2II_to_nH_ratio:   -1              # initial nH2II  to nH ratio (number density ratio). Value is ignored if set to -1.
+    initial_nHDI_to_nH_ratio:    -1              # initial nHDI   to nH ratio (number density ratio). Value is ignored if set to -1.
+
+.. note::
+   A simple example running SWIFT with Grackle can be find in ``examples/Cooling/CoolingBox``. A more advanced example combining heating and cooling (with heating and ionization sources) is given in ``examples/Cooling/CoolingHeatingBox``. ``examples/Cooling/CoolingWithPrimordialElements/`` runs a uniform cosmological box with imposed abundances and let them evolve down to redshift 0.
diff --git a/doc/RTD/source/SubgridModels/GEAR/index.rst b/doc/RTD/source/SubgridModels/GEAR/index.rst
index 82ba003a0ee2ed0916e115c71b1da6edb3cd4150..26c8b2474e893221a882fcca41306f57cbf9adf7 100644
--- a/doc/RTD/source/SubgridModels/GEAR/index.rst
+++ b/doc/RTD/source/SubgridModels/GEAR/index.rst
@@ -6,269 +6,16 @@ GEAR model
 ===========
 
 GEAR's model are mainly described in `Revaz \& Jablonka <https://ui.adsabs.harvard.edu/abs/2018A%26A...616A..96R/abstract>`_.
-This model can be selected with the configuration option ``--with-subgrid=GEAR`` and run with the option ``--gear``. A few examples exist and can be found in ``examples/GEAR``. 
+This model can be selected with the configuration option ``--with-subgrid=GEAR`` and run with the option ``--gear``. A few examples exist and can be found in ``examples/GEAR``, ``examples/SinkParticles`` or ``IsolatedGalaxy/IsolatedGalaxy_multi_component``.
 
-.. _gear_pressure_floor:
 
-Pressure Floor
-~~~~~~~~~~~~~~
+.. toctree::
+   :caption: Table of Contents
 
-In order to avoid the artificial collapse of unresolved clumps, a minimum in pressure is applied to the particles.
-This additional pressure can be seen as the pressure due to unresolved hydrodynamics turbulence and is given by:
+   gear_model
+   chemistry
+   feedback
+   supernova_feedback
+   sinks/index
+   output
 
-.. math::
-    P_\textrm{Jeans} = \frac{\rho}{\gamma} \frac{4}{\pi} G h^2 \rho N_\textrm{Jeans}^{2/3}
-
-where :math:`\rho` is the density, :math:`\gamma` the adiabatic index, :math:`G` is the gravitational constant,
-:math:`h` the kernel support and :math:`N_\textrm{Jeans}` (``GEARPressureFloor:jeans_factor`` in the parameter file) is the number of particle required in order to resolve a clump.
-
-
-This must be directly implemented into the hydro schemes, therefore only a subset of schemes (Gadget-2, SPHENIX and Pressure-Energy) have the floor available.
-In order to implement it, you need equation 12 in `Hopkins 2013 <https://arxiv.org/abs/1206.5006>`_:
-
-.. math::
-   m_i \frac{\mathrm{d}v_i}{\mathrm{d}t} = - \sum_j x_i x_j \left[ \frac{P_i}{y_i^2} f_{ij} \nabla_i W_{ij}(h_i) + \frac{P_j}{y_j^2} f_{ji} \nabla_j W_{ji}(h_j) \right]
-
-and simply replace the :math:`P_i, P_j` by the pressure with the floor (when the pressure is below the floor).
-Here the :math:`x, y` are simple weights that should never have the pressure floor included even if they are related to the pressure (e.g. pressure-entropy).
-
-.. code:: YAML
-
-   GEARPressureFloor:
-    jeans_factor: 10.       # Number of particles required to suppose a resolved clump and avoid the pressure floor.
-
-
-
-.. _gear_grackle_cooling:
-
-Cooling: Grackle
-~~~~~~~~~~~~~~~~
-   
-Grackle is a chemistry and cooling library presented in `B. Smith et al. 2017 <https://ui.adsabs.harvard.edu/abs/2017MNRAS.466.2217S>`_ 
-(do not forget to cite if used).  Four different modes are available:
-equilibrium, 6 species network (H, H\\( ^+ \\), e\\( ^- \\), He, He\\( ^+ \\)
-and He\\( ^{++} \\)), 9 species network (adds H\\(^-\\), H\\(_2\\) and
-H\\(_2^+\\)) and 12 species (adds D, D\\(^+\\) and HD).  Following the same
-order, the swift cooling options are ``grackle_0``, ``grackle_1``, ``grackle_2``
-and ``grackle_3`` (the numbers correspond to the value of
-``primordial_chemistry`` in Grackle).  It also includes some metal cooling (on/off with ``GrackleCooling:with_metal_cooling``), self-shielding
-methods and UV background (on/off with ``GrackleCooling:with_UV_background``).  In order to use the Grackle cooling, you will need
-to provide a HDF5 table computed by Cloudy (``GrackleCooling:cloudy_table``).
-
-When starting a simulation without providing the different element fractions in the non equilibrium mode, the code supposes an equilibrium and computes them automatically.
-The code uses an iterative method in order to find the correct initial composition and this method can be tuned with two parameters. ``GrackleCooling:max_steps`` defines the maximal number of steps to reach the convergence and ``GrackleCooling:convergence_limit`` defines the tolerance in the relative error.
-
-In order to compile SWIFT with Grackle, you need to provide the options ``with-chemistry=GEAR`` and ``with-grackle=$GRACKLE_ROOT``
-where ``$GRACKLE_ROOT`` is the root of the install directory (not the ``lib``). 
-
-You will need a Grackle version later than 3.1.1. To compile it, run
-the following commands from the root directory of Grackle:
-``./configure; cd src/clib``.
-Update the variables ``LOCAL_HDF5_INSTALL`` and ``MACH_INSTALL_PREFIX`` in
-the file ``src/clib/Make.mach.linux-gnu``.
-Finish with ``make machine-linux-gnu; make && make install``.
-Note that we require the 64 bit float version of Grackle, which should be the default setting. 
-(The precision can be set while compiling grackle with ``make precision-64``).
-If you encounter any problem, you can look at the `Grackle documentation <https://grackle.readthedocs.io/en/latest/>`_
-
-You can now provide the path given for ``MACH_INSTALL_PREFIX`` to ``with-grackle``.
-
-In the parameters file, a few different parameters are available.
-``GrackleCooling:redshift`` defines the redshift to use for the UV background (for cosmological simulation, it must be set to -1 in order to use the simulation's redshift) and ``GrackleCooling:provide_*_heating_rates`` can enable the computation of user provided heating rates (such as with the radiative transfer) in either volumetric or specific units.
-
-For the feedback, it can be made more efficient by turning off the cooling during a few Myr (``GrackleCooling:thermal_time_myr``) for the particles touched by a supernovae.
-
-The self shielding method is defined by ``GrackleCooling:self_shielding_method`` where 0 means no self shielding, > 0 means a method defined in Grackle (see Grackle documentation for more information) and -1 means GEAR's self shielding that simply turn off the UV background when reaching a given density (``GrackleCooling:self_shielding_threshold_atom_per_cm3``).
-
-.. code:: YAML
-
-  GrackleCooling:
-    cloudy_table: CloudyData_UVB=HM2012.h5       # Name of the Cloudy Table (available on the grackle bitbucket repository)
-    with_UV_background: 1                        # Enable or not the UV background
-    redshift: 0                                  # Redshift to use (-1 means time based redshift)
-    with_metal_cooling: 1                        # Enable or not the metal cooling
-    provide_volumetric_heating_rates: 0          # (optional) User provide volumetric heating rates
-    provide_specific_heating_rates: 0            # (optional) User provide specific heating rates
-    max_steps: 10000                             # (optional) Max number of step when computing the initial composition
-    convergence_limit: 1e-2                      # (optional) Convergence threshold (relative) for initial composition
-    thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
-    self_shielding_method: -1                    # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
-    self_shielding_threshold_atom_per_cm3: 0.007 # Required only with GEAR's self shielding. Density threshold of the self shielding
-
-.. _gear_star_formation:
-
-Star formation
-~~~~~~~~~~~~~~
-
-The star formation is done in two steps: first we check if a particle is in the star forming regime and then we use a stochastic approach to transform the gas particles into stars.
-
-A particle is in the star forming regime if:
- - The velocity divergence is negative (:math:`\nabla\cdot v < 0`),
- - The temperature is lower than a threshold (:math:`T < T_t` where :math:`T_t` is defined with ``GEARStarFormation:maximal_temperature``),
- - The gas density is higher than a threshold (:math:`\rho > \rho_t` where :math:`\rho_t` is defined with ``GEARStarFormation:density_threshold``)
- - The particle reaches the pressure floor (:math:`\rho > \frac{\pi}{4 G N_\textrm{Jeans}^{2/3} h^2}\frac{\gamma k_B T}{\mu m_p}` where :math:`N_\textrm{Jeans}` is defined in the pressure floor).
-
-If ``GEARStarFormation:star_formation_mode`` is set to ``agora``, the condition on the pressure floor is ignored. Its default value is ``default``.
-
-A star will be able to form if a randomly drawn number is below :math:`\frac{m_g}{m_\star}\left(1 - \exp\left(-c_\star \Delta t / t_\textrm{ff}\right)\right)` where :math:`t_\textrm{ff}` is the free fall time, :math:`\Delta t` is the time step of the particle and :math:`c_\star` is the star formation coefficient (``GEARStarFormation:star_formation_efficiency``), :math:`m_g` the mass of the gas particle and :math:`m_\star` the mass of the possible future star. The mass of the star is computed from the average gas mass in the initial conditions divided by the number of possible stars formed per gas particle (``GEARStarFormation:n_stars_per_particle``). When we cannot have enough mass to form a second star (defined with the fraction of mass ``GEARStarFormation:min_mass_frac``), we fully convert the gas particle into a stellar particle. Once the star is formed, we move it a bit in a random direction and fraction of the smoothing length in order to avoid any division by 0.
-
-Currently, only the following hydro schemes are compatible: SPHENIX and Gadget2.
-Implementing the other hydro schemes is not complicated but requires some careful thinking about the cosmological terms in the definition of the velocity divergence (comoving vs non comoving coordinates and if the Hubble flow is included or not).
-
-.. code:: YAML
-
-  GEARStarFormation:
-    star_formation_efficiency: 0.01   # star formation efficiency (c_*)
-    maximal_temperature:  3e4         # Upper limit to the temperature of a star forming particle
-    n_stars_per_particle: 4           # Number of stars that an hydro particle can generate
-    min_mass_frac: 0.5                # Minimal mass for a stellar particle as a fraction of the average mass for the stellar particles.
-
-
-Chemistry
-~~~~~~~~~
-
-In the chemistry, we are using the smoothed metallicity scheme that consists in using the SPH to smooth the metallicity of each particle over the neighbors. It is worth to point the fact that we are not exchanging any metals but only smoothing it. The parameter ``GEARChemistry:initial_metallicity`` set the (non smoothed) initial mass fraction of each element for all the particles and ``GEARChemistry:scale_initial_metallicity`` use the feedback table to scale the initial metallicity of each element according the Sun's composition.
-
-.. code:: YAML
-
-   GEARChemistry:
-    initial_metallicity: 1         # Initial metallicity of the gas (mass fraction)
-    scale_initial_metallicity: 1   # Should we scale the initial metallicity with the solar one?
-
-Feedback
-~~~~~~~~
-
-The feedback is composed of a few different models:
-  - The initial mass function (IMF) defines the quantity of each type of stars,
-  - The lifetime of a star defines when a star will explode (or simply die),
-  - The supernovae of type II (SNII) defines the rates and yields,
-  - The supernovae of type Ia (SNIa) defines the rates and yields,
-  - The energy injection that defines how to inject the energy / metals into the particles.
-
-Most of the parameters are defined inside a table (``GEARFeedback:yields_table``) but can be override with some parameters in the YAML file.
-I will not describe theses parameters more than providing them at the end of this section.
-Two different models exist for the supernovae (``GEARFeedback:discrete_yields``).
-In the continuous mode, we integrate the quantities over the IMF and then explodes a floating point number of stars (can be below 1 in some cases).
-In the discrete mode, we avoid the problem of floating points by rounding the number of supernovae (using a floor and randomly adding a supernovae depending on the fractional part) and then compute the properties for a single star at a time.
-
-Initial mass function
-^^^^^^^^^^^^^^^^^^^^^
-
-GEAR is using the IMF model from `Kroupa (2001) <https://ui.adsabs.harvard.edu/abs/2001MNRAS.322..231K/abstract>`_.
-We have a difference of 1 in the exponent due to the usage of IMF in mass and not in number.
-We also restrict the mass of the stars to be inside :math:`[0.05, 50] M_\odot`.
-Here is the default model used, but it can be easily adapted through the initial mass function parameters:
-
-.. math::
-  \xi(m) \propto m^{-\alpha_i}\, \textrm{where}\,
-  \begin{cases}
-   \alpha_0 = 0.3,\, & 0.01 \leq m / M_\odot < 0.08, \\
-   \alpha_1 = 1.3,\, & 0.08 \leq m / M_\odot < 0.50, \\
-   \alpha_2 = 2.3,\, & 0.50 \leq m / M_\odot < 1.00, \\
-   \alpha_3 = 2.3,\, & 1.00 \leq m / M_\odot,
-  \end{cases}
-
-
-Lifetime
-^^^^^^^^
-
-The lifetime of a star in GEAR depends only on two parameters: first its mass and then its metallicity.
-
-.. math::
-   \log(\tau(m)) = a(Z) \log^2(m) + b(Z) \log(m) + c(Z) \\ \\
-   a(Z) = -40.110 Z^2 + 5.509 Z + 0.7824 \\
-   b(Z) = 141.929 Z^2 - 15.889 Z - 3.2557 \\
-   c(Z) = -261.365 Z^2 + 17.073 Z + 9.8661
-
-where :math:`\tau` is the lifetime in years, :math:`m` is the mass of the star (in solar mass) and Z the metallicity of the star.
-The parameters previously given are the default ones, they can be modified in the parameters file.
-
-Supernovae II
-^^^^^^^^^^^^^
-
-The supernovae rate is simply given by the number of stars massive enough that end their life at the required time.
-
-.. math::
-   \dot{N}_\textrm{SNII}(t) = \int_{M_l}^{M_u} \delta(t - \tau(m)) \frac{\phi(m)}{m} \mathrm{d}m
-
-where :math:`M_l` and :math:`M_u` are the lower and upper mass limits for a star exploding in SNII, :math:`\delta` is the Dirac function and :math:`\phi` is the initial mass function (in mass).
-
-The yields for SNII cannot be written in an analytical form, they depend on a few different tables that are based on the work of `Kobayashi et al. (2000) <https://ui.adsabs.harvard.edu/abs/2000ApJ...539...26K/abstract>`_ and `Tsujimoto et al. (1995) <https://ui.adsabs.harvard.edu/abs/1995MNRAS.277..945T/abstract>`_.
-
-Supernovae Ia
-^^^^^^^^^^^^^
-
-The supernovae Ia are a bit more complicated as they involve two different stars.
-
-.. math::
-  \dot{N}_\textrm{SNIa}(t) = \left( \int_{M_{p,l}}^{M_{p,u}} \frac{\phi(m)}{m} \mathrm{d}m \right) \sum_i b_i \int_{M_{d,l,i}}^{M_{d,u,i}}
-  \delta(t-\tau(m)) \frac{\phi_d(m)}{m}\mathrm{d}m
-
-.. math::
-   \phi_d(m) \propto m^{-0.35}
-
-where :math:`M_{p,l}` and :math:`M_{p,u}` are the mass limits for a progenitor of a white dwarf, :math:`b_i` is the probability to have a companion and
-:math:`M_{d,l,i}` and :math:`M_{d,u,i}` are the mass limits for each type of companion.
-The first parenthesis represents the number of white dwarfs and the second one the probability to form a binary.
-
-+------------------+--------------------+-------------------+------------------+
-| Companion        |  :math:`M_{d,l,i}` | :math:`M_{d,u,i}` | :math:`b_i`      |
-+==================+====================+===================+==================+
-| Red giant        |   0.9              |    1.5            |    0.02          |
-+------------------+--------------------+-------------------+------------------+
-| Main sequence    |   1.8              |    2.5            |    0.05          |
-+------------------+--------------------+-------------------+------------------+
-
-The yields are based on the same papers than the SNII.
-
-Energy injection
-^^^^^^^^^^^^^^^^
-
-All the supernovae (type II and Ia) inject the same amount of energy into the surrounding gas (``GEARFeedback:supernovae_energy_erg``) and distribute it according to the hydro kernel.
-The same is done with the metals and the mass.
-
-
-Generating a new table
-^^^^^^^^^^^^^^^^^^^^^^
-
-The feedback table is an HDF5 file with the following structure:
-
-.. graphviz:: feedback_table.dot
-
-where the solid (dashed) squares represent a group (a dataset) with the name of the object underlined and the attributes written below. Everything is in solar mass or without units (e.g. mass fraction or unitless constant).
-In ``Data``, the attribute ``elts`` is an array of string with the element names (the last should be ``Metals``, it corresponds to the sum of all the elements), ``MeanWDMass`` is the mass of the white dwarfs
-and ``SolarMassAbundances`` is an array of float containing the mass fraction of the different element in the sun.
-In ``IMF``, ``n + 1`` is the number of part in the IMF, ``as`` are the exponent (``n+1`` elements), ``ms`` are the mass limits between each part (``n`` elements) and
-``Mmin`` (``Mmax``) is the minimal (maximal) mass of a star.
-In ``LifeTimes``, the coefficient are given in the form of a single table (``coeff_z`` with a 3x3 shape).
-In ``SNIa``, ``a`` is the exponent of the distribution of binaries, ``bb1``  and ``bb2`` are the coefficient :math:`b_i` and the other attributes follow the same names than in the SNIa formulas.
-The ``Metals`` group from the ``SNIa`` contains the name of each elements (``elts``) and the metal mass fraction ejected by each supernovae (``data``) in the same order. They must contain the same elements than in ``Data``.
-Finally for the ``SNII``, the mass limits are given by ``Mmin`` and ``Mmax``. For the yields, the datasets required are ``Ej`` (mass fraction ejected [processed]), ``Ejnp`` (mass fraction ejected [non processed]) and one dataset for each element present in ``elts``. The datasets should all have the same size, be uniformly sampled in log and contains the attributes ``min`` (mass in log for the first element) and ``step`` (difference of mass in log between two elements).
-
-.. code:: YAML
-
-  GEARFeedback:
-    supernovae_energy_erg: 0.1e51                            # Energy released by a single supernovae.
-    yields_table: chemistry-AGB+OMgSFeZnSrYBaEu-16072013.h5  # Table containing the yields.
-    discrete_yields: 0                                       # Should we use discrete yields or the IMF integrated one?
-  GEARInitialMassFunction:
-    number_function_part:  4                       # Number of different part in the IMF
-    exponents:  [0.7, -0.8, -1.7, -1.3]            # Exponents of each part of the IMF
-    mass_limits_msun:  [0.05, 0.08, 0.5, 1, 50]    # Limits in mass between each part of the IMF
-  GEARLifetime:
-   quadratic:  [-40.1107, 5.50992, 0.782432]  # Quadratic terms in the fit
-   linear:  [141.93, -15.8895, -3.25578]      # Linear terms in the fit
-   constant:  [-261.366, 17.0735, 9.86606]    # Constant terms in the fit
-  GEARSupernovaeIa:
-    exponent:  -0.35                      # Exponent for the distribution of companions
-    min_mass_white_dwarf_progenitor:  3   # Minimal mass of a progenitor of white dwarf
-    max_mass_white_dwarf_progenitor:  8   # Maximal mass of a progenitor of white dwarf
-    max_mass_red_giant:  1.5              # Maximal mass for a red giant
-    min_mass_red_giant:  0.9              # Minimal mass for a red giant
-    coef_red_giant:  0.02                 # Coefficient for the distribution of red giants companions
-    max_mass_main_sequence:  2.6          # Maximal mass for a main sequence star
-    min_mass_main_sequence:  1.8          # Minimal mass for a main sequence star
-    coef_main_sequence:  0.05             # Coefficient for the distribution of main sequence companions
-    white_dwarf_mass:  1.38               # Mass of a white dwarf
-  GEARSupernovaeII:
-  interpolation_size:  200                # Number of elements for the interpolation of the data
diff --git a/doc/RTD/source/SubgridModels/GEAR/output.rst b/doc/RTD/source/SubgridModels/GEAR/output.rst
new file mode 100644
index 0000000000000000000000000000000000000000..e534bb52ac7f3e12299195f55688e477f762e9e1
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/output.rst
@@ -0,0 +1,115 @@
+.. Sink particles in GEAR model
+   Darwin Roduit, 14 July 2024
+
+.. sink_GEAR_model:
+
+Snapshots ouputs
+----------------
+
+Here, we provide a summary of the quantities written in the snapshots, in addition to positions, velocities, masses, smoothing lengths and particle IDs.
+
+
+Sink particles
+~~~~~~~~~~~~~~
+
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| Name                                  | Description                                 | Units                  | Comments                                          |
++=======================================+=============================================+========================+===================================================+
+| ``NumberOfSinkSwallows``              | | Number of merger events with other sinks  | [-]                    |                                                   |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``NumberOfGasSwallows``               | | Number of gas particles accreted          | [-]                    |                                                   |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``TargetMass``                        | | Target mass required to spawn the next    | [U_M]                  | | You can use it to determine if the target mass  |
+|                                       | | star particle                             |                        | | is so huge that the sink's mass cannot spawn    |
+|                                       |                                             |                        | | such a star. Such rare behaviour may bias the   |
+|                                       |                                             |                        | | IMF towards high masses.                        |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``Nstars``                            | | Number of stars created by this sink      | [-]                    |                                                   |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``SwallowedAngularMomentum``          | | Total angular momentum of accreted        | [U_M U_L^2 U_T^{-1}]   |                                                   |
+|                                       | | material                                  |                        |                                                   |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``MetalMassFractions``                | | Mass fraction of each tracked metal       | [-]                    | | *Only in GEAR chemistry module.*                |
+|                                       | | element                                   |                        | | Array of length ``N`` (number of elements),     |
+|                                       |                                             |                        | | set at compile time via                         |
+|                                       |                                             |                        | | ``--with-chemistry=GEAR_N``.                    |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``BirthScaleFactors``                 | | Scale factor at the time of sink creation | [-]                    | | Only used in *cosmological* runs.               |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``BirthTimes``                        | | Time when the sink was created            | [U_T]                  | | Only used in *non-cosmological* runs.           |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+
+
+
+Stars
+~~~~~
+
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| Name                                  | Description                                 | Units                  | Comments                                          |
++=======================================+=============================================+========================+===================================================+
+| ``BirthScaleFactors``                 | | Scale-factors when the stars were born    | [-]                    | | Only used in cosmological runs.                 |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``BirthTimes``                        | | Time when the stars were born             | [U_T]                  | | Only used in non-cosmological runs.             |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``BirthMasses``                       | | Masses of the stars at birth time         | [U_M]                  | | SF and sinks modules                            |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``ProgenitorIDs``                     | | ID of the progenitor sinks or gas         | [-]                    | | SF and sinks modules                            |
+|                                       | | particles                                 |                        |                                                   |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``BirthDensities``                    | | Gas density at star formation             | [U_M U_L^{-3}]         | | *Only in SF module*                             |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``BirthTemperatures``                 | | Gas temperature at star formation         | [K]                    | | *Only in SF module*                             |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``Potentials``                        | | Gravitational potential of the star       | [U_L^2 U_T^{-2}]       |                                                   |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``StellarParticleType``               | | Type of the stellar particle:             | [-]                    | | 0: (discrete) single star                       |
+|                                       | |                                           |                        | | 1: continuous IMF part star                     |
+|                                       | |                                           |                        | | 2: single population star                       |
+|                                       | |                                           |                        | | The last type corresponds to legacy IMF stars.  |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+| ``MetalMassFractions``                | | Mass fraction of each metal element       | [-]                    | | *Only in GEAR chemistry module*.                |
+|                                       | |                                           |                        | | Array of length ``N`` (number of elements),     |
+|                                       | |                                           |                        | | set at compile time by                          |
+|                                       | |                                           |                        | | ``--with-chemistry=GEAR_N``.                    |
++---------------------------------------+---------------------------------------------+------------------------+---------------------------------------------------+
+
+Gas particles
+~~~~~~~~~~~~~
+
+Since hydro scheme writes its own set of outputs, we only provide the outputs that ``GEAR`` writes for gas particles. 
+
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| Name                                     | Description                                                 | Units                  | Comments                                           |
++==========================================+=============================================================+========================+====================================================+
+| ``SmoothedMetalMassFractions``           | | Mass fraction of each metal element                       | [-]                    | | *Only in GEAR chemistry module.*                 |
+|                                          | | smoothed over the SPH kernel                              |                        | | Array of length ``N``, set at compile time by    |
+|                                          |                                                             |                        | | ``--with-chemistry=GEAR_N``                      |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``MetalMassFractions``                   | | Raw (non-smoothed) mass fraction of                       | [-]                    | | *Only in GEAR chemistry module.*                 |
+|                                          | | each metal element                                        |                        | | Same layout as above.                            |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``HI``                                   | | Mass fraction of neutral H (:math:`\mathrm{H}`)           | [-]                    | | *Only if* ``GRACKLE_1 to 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``HII``                                  | | Mass fraction of ionized H (:math:`\mathrm{H}^+`)         | [-]                    | | *Only if* ``GRACKLE_1 to 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``HeI``                                  | | Mass fraction of neutral He (:math:`\mathrm{He}`)         | [-]                    | | *Only if* ``GRACKLE_1 to 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``HeII``                                 | | Mass fraction of singly ionized He (:math:`\mathrm{He}^+`)| [-]                    | | *Only if* ``GRACKLE_1 to 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``HeIII``                                | | Mass fraction of doubly ionized He                        | [-]                    | | *Only if* ``GRACKLE_1 to 3``                     |
+|                                          | | (:math:`\mathrm{He}^{++}`)                                |                        |                                                    |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``e``                                    | | Free electron mass fraction (:math:`\mathrm{e}^-`)        | [-]                    | | *Only if* ``GRACKLE_1 to 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``HM``                                   | | Mass fraction of :math:`\mathrm{H}^-`                     | [-]                    | | *Only if* ``GRACKLE_2 or 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``H2I``                                  | | Mass fraction of neutral :math:`\mathrm{H}_2`             | [-]                    | | *Only if* ``GRACKLE_2 or 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``H2II``                                 | | Mass fraction of ionized :math:`\mathrm{H}_2^+`           | [-]                    | | *Only if* ``GRACKLE_2 or 3``                     |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``DI``                                   | | Mass fraction of neutral D (:math:`\mathrm{D}`)           | [-]                    | | *Only if* ``GRACKLE_3``                          |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``DII``                                  | | Mass fraction of ionized D (:math:`\mathrm{D}^+`)         | [-]                    | | *Only if* ``GRACKLE_3``                          |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
+| ``HDI``                                  | | Mass fraction of :math:`\mathrm{HD}`                      | [-]                    | | *Only if* ``GRACKLE_3``                          |
++------------------------------------------+-------------------------------------------------------------+------------------------+----------------------------------------------------+
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/index.rst b/doc/RTD/source/SubgridModels/GEAR/sinks/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..aa4313dda9ee78a0c9b22d57c98aeac7dca2aa24
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/sinks/index.rst
@@ -0,0 +1,17 @@
+.. Sink particles in GEAR model
+   Darwin Roduit, 17 April 2024
+
+.. sink_GEAR_model:
+
+.. _sink_GEAR_model:
+
+Sink particles and star formation in GEAR model
+===============================================
+
+.. toctree::
+   :caption: Table of Contents
+
+   introduction
+   theory
+   sink_timesteps
+   params
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/introduction.rst b/doc/RTD/source/SubgridModels/GEAR/sinks/introduction.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a90c83a2f8e1aed1146818ea9f4a78418e39648e
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/sinks/introduction.rst
@@ -0,0 +1,29 @@
+.. Sink particles in GEAR model
+   Darwin Roduit, 15 March 2024
+
+.. sink_GEAR_model:
+
+Introduction
+------------
+
+GEAR sink particles provide an alternative model to star formation. Instead of stochastically transforming gas particles into stars as is done in GEAR star formation scheme under some conditions, we transform a gas into a sink particle. The main property of the sink particle is its accretion radius. When gas particles within this accretion radius are eligible to be swallowed by the sink, we remove them and transfer their mass, momentum, angular momentum, chemistry properties, etc to the sink particle.
+
+With the sink particles, the IMF splits into two parts: the continuous part and the discrete part. Those parts will correspond to two kinds of stars. Particles in the discrete part of the IMF represent individual stars. It means that discrete IMF-sampled stars have different masses. Particles in the continuous part represent a population of stars, all with the same mass.
+
+The sink particle will randomly choose a target mass, accrete gas until it reaches this target mass and finally spawn a star. Then, the sink chooses a new target mass and repeats the same procedure. When stars are spawned, they are given a new position and velocity as well as chemical properties.
+
+In ``theory.rst`` we outline all of the theory which is implemented as part of the model. This includes how sink particles are formed, how the gas is accreted, how sinks are merged and how stars are spawned. In ``params.rst`` we list and discuss all parameters used by the model. Below we outline how to configure and run the model.
+
+Compiling and running the model
+-------------------------------
+
+You can configure the model with ``--with-sink=GEAR`` in combination with other configure options of the GEAR model. The model will then be used when the ``--sinks`` flag is among the runtime options. In particular, the sink particles require ``--feedback`` runtime option.
+
+Notice that you also need to compile with ``--with-star-formation=GEAR``. The star formation module is required to collect and write star formation data. However, you do not need to run Swift with the option ``--star-formation``.
+
+Then, you do not need to do anything special. Sink particles will be created during your runs. If you want, you can have sink particles in your ICs. At the moment, sink particles do not have any special fields to be set.
+
+A full list of all relevant parameters of the model is in :ref:`sink_GEAR_parameters`. We also briefly describe the most important parameters which need to be set to run the model, as well as how to run it in different configurations.
+
+.. warning::
+   Currently, MPI is not implemented for the sink particles. If you try to run with MPI, you will encounter an error. We thus recommend configuring Swift with ``--disable-mpi`` to avoid any surprises.
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/params.rst b/doc/RTD/source/SubgridModels/GEAR/sinks/params.rst
new file mode 100644
index 0000000000000000000000000000000000000000..e542d867cebaf71599ee90e0bcf3e6ba9fe5d58f
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/sinks/params.rst
@@ -0,0 +1,137 @@
+.. Sink particles in GEAR model
+   Darwin Roduit, 15 March 2024
+
+.. sink_GEAR_model:
+
+.. _sink_GEAR_parameters:
+
+Model parameters
+----------------
+
+The parameters of the GEAR sink model are grouped into the ``GEARSink`` section of the parameter file. 
+
+The first three parameters are:
+
+* Whether we are using a fixed cut-off radius for gas and sink accretion: ``use_fixed_cut_off_radius``,
+* The sink cut-off radius: ``cut_off_radius``,
+* The sink inner accretion radius fraction in terms of the cut-off radius: ``f_acc``.
+
+The ``use_fixed_cut_off_radius`` is mandatory and should be set to 0 or 1. If set to 1, the GEAR model will use a fixed cutoff radius equal to the value of ``cut_off_radius``. If not, the cutoff radius is allowed to vary according to the local gas density. In the code, the cutoff radius is always equal to the sink's smoothing length multiplied by a constant factor ``kernel_gamma`` - setting a fixed cutoff will fix the smoothing length at the appropriate value for all sinks.
+
+The ``cut_off_radius`` parameter is optional, and is ignored if ``use_fixed_cut_off_radius`` is 0. If a fixed cut-off is used, every sink's smoothing length will be permanently set to this number divided by ``kernel_gamma`` on formation.
+
+The ``f_acc`` parameter is also optional. Its default value is :math:`0.1`. Its value must respect :math:`0 \leq f_\text{acc} \leq 1` . It describes the inner radius :math:`f_{\text{acc}} \cdot r_{\text{cut-off}}` in which gas particles are swallowed without any further checks, as explained below.
+
+The next three mandatory parameters are:
+
+* the gas temperature threshold to form a sink when :math:`\rho_\text{threshold} < \rho_\text{gas} < \rho_\text{maximal}` :``temperature_threshold_K``,
+* the minimal gas threshold density required to form a sink:``density_threshold_Hpcm3``,
+* the maximal density at which the temperature check is not performed:``maximal_density_threshold_Hpcm3`` (Default: ``FLT_MAX``).
+
+These three parameters govern the first two criteria of the sink formation scheme. If these criteria are not passed, sink particles are not created. If they are passed, the code performs further checks to form sink particles. Some of those criteria checks can be disabled, as explained below.
+
+The next set of parameters deals with the sampling of the IMF and the representation of star particles:
+
+* minimal mass of stars represented by discrete particles: ``minimal_discrete_mass_Msun``,
+* mass of the stellar particle representing the continuous part of the IMF: ``stellar_particle_mass_Msun``,
+* minimal mass of the first stars represented by discrete particles: ``minimal_discrete_mass_first_stars_Msun``,
+* mass of the stellar particle (first stars) representing the continuous part of the IMF:: ``stellar_particle_mass_first_stars_Msun``.
+
+With sink particles, star particles can represent either a single star or a population of stars in the low mass part of the IMF (continuous IMF sampling). The stars in the continuous part of the IMF are put together in a particle of mass ``stellar_particle_mass_Msun`` or ``stellar_particle_mass_first_stars_Msun``, while individual stars in the discrete part have their mass sampled from the IMF. The limit between the continuous and discrete sampling of the IMF is controlled by  ``minimal_discrete_mass_Msun`` and ``minimal_discrete_mass_first_stars_Msun``.
+
+The next set of parameters controls the sink formation scheme. More details are provided in the GEAR documentation. Here is a brief overview:
+
+* whether or not the gas must be contracting: ``sink_formation_contracting_gas_criterion`` (default: 1), 
+* whether or not the gas smoothing length must be small enough: ``sink_formation_smoothing_length_criterion`` (default: 1),
+* whether or not the gas must be in a Jeans unstable state: ``sink_formation_jeans_instability_criterion`` (default: 1),
+* whether or not the gas must be in a bound state: ``sink_formation_bound_state_criterion`` (default: 1),
+* whether or not a new sink can be formed in a region where its ``cut_off_radius`` and the one of an existing sink overlap: ``sink_formation_overlapping_sink_criterion`` (default: 1).
+
+Those criteria are checked if the density and temperature criteria are successfully passed. They control the behaviour of the sink formation scheme. By default, they are all activated and set to ``1``.
+
+The next parameter is ``disable_sink_formation`` (default: 0). It controls whether sinks are formed or not in the simulation. The main purpose is when sinks are put in initial conditions and sinks are not wanted to be added during the run. This parameter is set to ``0`` by default, i.e. sink formation is *enabled*.
+
+The next set of parameters deals with the sink time-steps:
+
+* Courant-Friedrich-Levy constant for the CFL-like time-step constraint:``CFL_condition``,
+* age (in Myr) at which a sink is considered dead (no accretion) and without time-step limitations, except for 2-body encounters involving another young/old sink and gravity: ``timestep_age_threshold_unlimited_Myr`` (default: FLT_MAX),
+* age (in Myr) at which sinks switch from young to old for time-stepping purposes:``timestep_age_threshold``  (default: FLT_MAX),
+* maximal time-step length of young sinks (in Myr): ``max_timestep_young_Myr``  (default: FLT_MAX),
+* maximal time-step length of old sinks (in Myr): ``max_timestep_old_Myr`` (default: FLT_MAX),
+* number of times the IMF mass can be swallowed in a single time-step: ``n_IMF`` (default: FLT_MAX).
+* tolerance parameter for SF timestep constraint: ``tolerance_SF_timestep`` (default: 0.5)
+
+The last parameter is ``sink_minimal_mass_Msun``. This parameter is mainly intended for low-resolution simulations with :math:`m_\text{gas} > 100 \; M_\odot`. It prevents :math:`m_\text{sink} \ll m_\text{gas}` simulations when sinks spawn stars, which can lead to gravity run away.
+
+The full section is:
+
+.. code:: YAML
+
+   GEARSink:
+     use_fixed_cut_off_radius: 1                 # Are we using a fixed cutoff radius? If we are, in GEAR the cutoff radius is fixed at the value specified below, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+     cut_off_radius: 1e-3                        # Cut off radius of all the sinks in internal units. Ignored if use_fixed_cut_off_radius is 0. 
+     f_acc: 0.1                                  # (Optional) Fraction of the cut_off_radius that determines if a gas particle should be swallowed wihtout additional check. It has to respect 0 <= f_acc <= 1. (Default: 0.1)
+     temperature_threshold_K:        100         # Max temperature (in K) for forming a sink when density_threshold_Hpcm3 <= density <= maximal_density_threshold_Hpcm3.
+     density_threshold_Hpcm3: 1e3                # Minimum gas density (in Hydrogen atoms/cm3) required to form a sink particle.
+     maximal_density_threshold_Hpcm3: 1e5        # If the gas density exceeds this value (in Hydrogen atoms/cm3), a sink forms regardless of temperature if all other criteria are passed. (Default: FLT_MAX)
+     sink_minimal_mass_Msun:     0.              # (Optional) Sink minimal mass in Msun. This parameter prevents m_sink << m_gas in low resolution simulations. (Default: 0.0)
+     stellar_particle_mass_Msun:  20             # Mass of the stellar particle representing the low mass stars (continuous IMF sampling) (in solar mass)
+     minimal_discrete_mass_Msun: 8               # Minimal mass of stars represented by discrete particles (in solar mass)
+     stellar_particle_mass_first_stars_Msun: 20      # Mass of the stellar particle representing the low mass stars (continuous IMF sampling) (in solar mass). First stars
+     minimal_discrete_mass_first_stars_Msun: 8       # Minimal mass of stars represented by discrete particles (in solar mass). First stars
+     star_spawning_sigma_factor: 0.2                 # Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+     sink_formation_contracting_gas_criterion: 1     # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+     sink_formation_smoothing_length_criterion: 1    # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+     sink_formation_jeans_instability_criterion: 1   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+     sink_formation_bound_state_criterion: 1         # (Optional) Activate the bound state check for sink formation. (Default: 1)
+     sink_formation_overlapping_sink_criterion: 1    # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+     disable_sink_formation: 0                       # (Optional) Disable sink formation. (Default: 0)
+     CFL_condition:                        0.5       # Courant-Friedrich-Levy condition for time integration.
+     timestep_age_threshold_unlimited_Myr: 100.      # (Optional) Age above which sinks no longer have time-step restrictions, except for 2-body encounters involving another young/old sink and gravity (in Mega-years). (Default: FLT_MAX)
+     timestep_age_threshold_Myr:           25.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). (Default: FLT_MAX)
+     max_timestep_young_Myr:               1.0       # (Optional) Maximal time-step length of young sinks (in Mega-years). (Default: FLT_MAX)
+     max_timestep_old_Myr:                 5.0       # (Optional) Maximal time-step length of old sinks (in Mega-years). (Default: FLT_MAX)
+     n_IMF:                                 2        # (Optional) Number of times the IMF mass can be swallowed in a single timestep. (Default: FLTM_MAX)
+     tolerance_SF_timestep:                 0.5      # (Optional) Tolerance parameter for SF timestep constraint. (Default: 0.5)
+
+.. warning::
+   Some parameter choices can greatly impact the outcome of your simulations. Think twice when choosing them.
+
+Sink accretion radius
+~~~~~~~~~~~~~~~~~~~~~
+
+The most critical parameter is ``cut_off_radius``. As explained in the theory, to form a sink, the gas smoothing kernel edge :math:`\gamma_k h` (:math:`\gamma_k` is a kernel dependent constant) must be smaller than ``cut_off_radius`` (if this criterion is enabled). Therefore, the cut-off radius strongly depends on the resolution of your simulations. Moreover, if you use a minimal gas smoothing length `h`, and plan to use sink particles, consider whether the cut-off radius will meet the smoothing length criterion. If `h` never meets the aforementioned criterion, you will never form sinks and thus never have stars.
+
+On the contrary, if you set a too high cut-off radius, then sinks will accrete a lot of gas particles and spawn a lot of stars in the same cell, which the code might not like and crash with the error:
+
+``runner_others.c:runner_do_star_formation_sink():274: Too many stars in the cell tree leaf! The sorting task will not be able to perform its duties. Possible solutions: (1) The code need to be run with different star formation parameters to reduce the number of star particles created. OR (2) The size of the sorting stack must be increased in runner_sort.c.``
+
+This problem can be mitigated by choosing a higher value of ``stellar_particle_mass_Msun`` and ``stellar_particle_mass_first_stars_Msun``, or higher values of ``minimal_discrete_mass_Msun`` and ``minimal_discrete_mass_first_stars_Msun``. Of course, this comes at the price of having fewer individual stars. Finally, all parameters will depend on your needs.
+
+*If you do not want to change your parameters*, you can increase the ``sort_stack_size`` variable at the beginning ``runner_sort.c``. The default value is 10 in powers of 2 (so the stack size is 1024 particles). Increase it to the desired value. Be careful to not overestimate this.
+
+Note that if a cutoff radius is not specified, and the radius is instead left to vary with the local gas density, the smoothing length criterion is always satisfied.
+
+Guide to choose the the accretion radius or the density threshold
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We provide some advice to help you set up the sink accretion radius or the threshold density appropriately.
+
+First, you must choose either the sink accretion radius or the threshold density. Choosing the density might be easier based on your previous work or if you have an expected star formation density. Once you fix the density or the accretion radius, you can use the following formula to estimate the remaining parameter. In the code, the gas smoothing length is determined with:
+
+.. math::
+   h = \eta \left( \frac{X_{\text{H}} m_B}{m_{\text{H}} n_{\text{H}}} \right)^{1/3} \, ,
+
+where :math:`\eta` is a constant related to the number of neighbours in the kernel, :math:`X_{\text{H}}` is the hydrogen mass fraction, :math:`m_B` the gas particle's mass, :math:`m_{\text{H}}` the hydrogen particle mass and :math:`n_{\text{H}}` the hydrogen number density.
+
+Let us provide an example. In GEAR, we do not model physical processes below the parsec scale. Hence, let us take :math:`h \sim 1` pc. In zoom-in simulations we have :math:`m_B \simeq 95 \; M_{\odot}`. The remaining parameters are :math:`\eta = 1.2348` and :math:`X_{\text{H}} = 0.76`. So, after inverting the formula, we find :math:`n_H \simeq 5500 \text{ hydrogen atoms/cm}^3`. In practice, we use :math:`n_H = 1000 \text{ hydrogen atoms/cm}^3`, close to the estimation, and an accretion radius :math:`r_{\text{acc}} = 10` pc. These values are slightly different for safety reasons, but they are consistent.
+
+Remember that this was a way, among others, to determine good accretion radius and threshold density. It can help you with your first runs with sink particles.
+
+Comment on star formation efficiency
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Notice that this model does not have parameters to control the star formation rate of the sink. The SFR is self-regulated by the gas/sink accretion and other feedback mechanisms. Supernovae tend to create bubbles of lower density at the site of star formation, removing the gas and preventing further gas accretion. However, the sink might run into this stack size problem by the time the first supernovae explode. Other pre-stellar feedback mechanisms could do the job earlier, though they are not implemented in GEAR.
+
+.. note:: 
+   We provide a piece of general advice: do some calibration on low-resolution simulations. This will help to see what works and what does not work. Keep in mind that you might want to put a higher ``stellar_particle_mass_X_Msun`` at the beginning to avoid spawning too many stars. For the high-resolution simulations, you then can lower the particle's mass.
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/sink_accretion_radius.png b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_accretion_radius.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d7deb1874750b5ebb538c63ab05ee082692423e
Binary files /dev/null and b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_accretion_radius.png differ
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/sink_imf.png b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_imf.png
new file mode 100644
index 0000000000000000000000000000000000000000..e59f0d770681a35666bfaa3224b9a4f3e5e25289
Binary files /dev/null and b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_imf.png differ
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/sink_overlapping.png b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_overlapping.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2f1434261b61a8e1db393e8dde4056f8eca2fb6
Binary files /dev/null and b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_overlapping.png differ
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/sink_scheme.png b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_scheme.png
new file mode 100644
index 0000000000000000000000000000000000000000..8353c6a34a92dbbb161e9442724be73c89fa06d3
Binary files /dev/null and b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_scheme.png differ
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/sink_timesteps.rst b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_timesteps.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f31551bd1b595e40ebf4f6a5a4a64bdd7170e89f
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/sinks/sink_timesteps.rst
@@ -0,0 +1,66 @@
+.. Sink particles in GEAR model
+   Darwin Roduit, 24 November 2024
+
+.. _sink_GEAR_timesteps:
+
+Sink timesteps
+~~~~~~~~~~~~~~
+
+Sink particles interact with the surrounding gas through accretion. To accurately follow the local gas dynamics and assess the stability of gas hydrodynamics, we must impose timestep constraints on the sink particles.
+
+First, we want to ensure sinks know the *local dynamics* and thus they obey a Courant-Friedrichs-Lewy (CFL)-like timestep constraint:
+
+.. math::
+   \Delta t_s \leq \Delta t_\text{CFL} =  C_\text{CFL} \frac{r_{\text{cut, min}}}{\sqrt{c_{s,s}^2 + \| \Delta \mathbf{v}_s \|^2}} \; ,
+
+where :math:`r_\text{cut, min} = \min(r_{\text{cut}, s}, \gamma_k \min_j(h_j))` is the minimal cut-off radius between the sink :math:`s` and the gas neighbours:math:`j`, :math:`c_{s, s}` and :math:`\Delta \mathbf{v}_s` are the gas sound-speed and the relative gas-sink velocity at the sink location. The latter two are reconstructed at the sink location with SPH interpolation. The value of :math:`C_\text{CFL}` is given in the YAML parameter file with ``GEARSink:CFL_condition``.
+
+Since sink particles accrete gas, they must anticipate the surrounding gas infall. We achieve this with a gas free-fall time criterion similar to `Grudic et al. (2021) <https://academic.oup.com/mnras/article/506/2/2199/6276745>`_:
+
+.. math::
+   \Delta t_s \leq \Delta t_\text{ff} = \sqrt{ \frac{3 \pi}{32 G \rho_s} } \quad \text{with} \quad \rho_s = \frac{3 m_s}{4 \pi {r_{\text{cut, min}}}} \; ,
+
+with :math:`m_s` the sink mass.
+
+These constraints ensure smooth gas accretion by removing a few particles per timestep. This is important since, in SPH, gas particles serve as interpolation points. By preventing the removal of a large number of particles, we ensure the stability of the hydrodynamic computations.
+
+Sink 2-body encounters
+++++++++++++++++++++++
+
+To accurately follow *sink mergers*, we implemented `Grudic et al. (2021) <https://academic.oup.com/mnras/article/506/2/2199/6276745>`_ two body timestep constraints between all sink particles:
+
+.. math::
+   \Delta t_s \leq \Delta t_\text{2-body} = \frac{ t_\text{c, min} t_\text{dyn, min}}{t_\text{c, min} + t_\text{dyn, min}} \; ,
+
+with
+
+.. math::
+  \quad t_\text{c, min} = \min_{s \neq s'} \frac{ |\varphi^{-1}(r_{ss'}, \, H_\text{sink})| }{v_{ss'}} \quad \text{and} \quad t_\text{dyn, min} = \min_{s \neq s'} \sqrt{ \frac{ |\varphi'(r_{ss'}, \, H_\text{sink})|^{-1}} { G (m_s + m_{s'})}    } \; ,
+
+where :math:`r_{ss'}` is the sinks relative separation, :math:`v_{ss'}` the relative velocity, :math:`m_{s}` and :math:`m_{s'}` their masses and :math:`H_\text{sink}` is the sink (fixed) gravitational softening. The function :math:`\varphi(r, H)` is the potential corresponding to the Wendland C2 kernel density field (see `Schaller et al. (2024) <https://doi.org/10.1093/mnras/stae922>`_ section 4.1) and  :math:`\varphi'(r, H) \equiv \frac{\mathrm{d} \varphi(r, H)}{\mathrm{d} r}` its derivative.
+
+Timesteps per sink's age categories
++++++++++++++++++++++++++++++++++++
+
+We also implemented maximal timesteps sizes depending on the sink age; :math:`\Delta t_\text{max,s}^\text{age}`. A sink can be young, old or dead. In the first two cases, the sink's timestep is :math:`\min(\Delta t_\text{max,s}^\text{age}, \Delta t_s)`. In the last case, we impose :math:`\Delta t_\text{2-body}` only if a dead sink is involved in a two-boy encounter with an alive sink. Otherwise, the sink has no timestep constraint (apart from gravity). The parameters controlling the transition between the young and old is ``GEARSink:timestep_age_threshold``, and the one between old and dead is ``GEARSink:timestep_age_threshold_unlimited_Myr``. The maximal timesteps are given by  ``GEARSink:max_timestep_young_Myr`` and  ``GEARSink:max_timestep_old_Myr``.
+
+Notice that sink particles also satisfy a gravity timestep constraint, as do all gravitational particles in Swift.
+
+Star formation constraint
++++++++++++++++++++++++++
+
+Although the stars' masses are sampled from the IMF, the stars' metallicities are not. If a sink accretes mass, it can create many star particles simultaneously. However, these stars will all have the same metallicity, which does not represent the actual metals' evolution during the accretion. In such a situation, the galaxies' properties are affected and do not represent the underlying physics.
+
+Another problem is that we can spawn many stars simultaneously, and the code may complain. Such a situation could be better. Although the constraints in the previous section will help, more is needed. Our solution is to introduce a new accretion criterion using the IMF properties. However, since our politics is that accretion should be feedback-regulated and not based on an arbitrary accretion rate, we reduce the sink time step to avoid limiting the star formation rate to an arbitrary value.
+
+The new accretion criterion is the following. The swallowed gas and sink mass does not exceed ``n_IMF`` times the IMF mass (see the IMF sampling section), but make sure to swallow at least one particle: :math:`M_\text{swallowed} \leq n_\text{IMF} M_\text{IMF} \text{ or } M_\text{swallowed} = 0`.
+
+Since we artificially restrict mass accretion, we keep track of the mass :math:`M_\text{eligible}` that would be swallowed without this criterion. Then, we compute the error :math:`\Delta M` between the restricted and unrestricted swallow. The absolute error is :math:`\Delta M = M_\text{swallowed} - M_\text{eligible}` and the relative error is :math:`| \Delta M | / M_\text{eligible}`.
+
+When :math:`\Delta M < 0` (i.e. :math:`M_\text{swallowed} \neq M_\text{eligible}`), we know the accretion was restricted and we can apply another time-step contraint. To compute a timestep, we convert :math:`\Delta M` to accretion rate by dividing by :math:`\Delta t_\text{s, estimated} = \min(\Delta t_\text{CFL}, \, \Delta t_\text{ff}, \Delta  t_\text{2-body})`. Hence, we have the constraint:
+
+.. math::
+   \Delta t_s \leq \Delta t_\text{SF} = \eta \cfrac{M_\text{eligible} \Delta t_\text{s, estimated}}{\Delta M} \text{ if } \Delta M < 0 \; ,
+
+where :math:`\eta` is a tolerance parameter. This parameter corresponds to ``GEARSink:tolerance_SF_timestep`` in the code.
+
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/theory.rst b/doc/RTD/source/SubgridModels/GEAR/sinks/theory.rst
new file mode 100644
index 0000000000000000000000000000000000000000..5991e330666414e33c2dfb4aff698cfbc95c9c76
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/sinks/theory.rst
@@ -0,0 +1,248 @@
+.. Sink particles in GEAR model
+   Darwin Roduit, 24 November 2024
+
+.. _sink_GEAR_model_summary:
+
+Model summary
+-------------
+
+.. figure:: sink_scheme.png
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: Illustration of the sink scheme.
+
+    This figure illustrates the sink scheme. Eligible gas particles (blue) are converted to sink particles (orange). Then, the sink searches for eligible gas/sink particles (red edges) to swallow and finally accretes them. The final step is to spawn star particles (yellow) by sampling an IMF. These stars represent a continuous portion of the IMF or an individual star (the figure does not distinguish the two types of stars).
+
+Here, we provide a comprehensive summary of the model. Sink particles are an alternative to the current model of star formation that transforms gas particles into sink particles under some criteria explained below. Then, the sink can accrete gas and spawn stars. Sink particles are collisionless particles, i.e. they interact with other particles only through gravity. They can be seen as particles representing unresolved regions of collapse. 
+
+We sample an IMF to draw the stars' mass and spawn them stochastically. Below, we provide a detailed explanation of the IMF sampling. In short, we split the IMF into two parts. In the lower part, star particles represent a continuous stellar population, similar to what is currently implemented in standard models. In the second upper part, star particles represent individual stars. Then, the feedback is improved to take into account both types of stars. Currently, only supernovae feedback is implemented. Thus, the sink particle method allows us to track the effects of individual stars' supernovae in the simulation. 
+
+The current model includes sink formation, gas accretion, sink merging, IMF sampling, star spawning and finally supernovae feedback (type Ia and II). The figures below illustrates the scheme and the associated tasks.
+
+Our main references are the following papers: `Bate et al. <https://ui.adsabs.harvard.edu/abs/1995MNRAS.277..362B/abstract>`_, `Price et al. <https://ui.adsabs.harvard.edu/abs/2018PASA...35...31P/abstract>`_ and `Federrath et al. <https://ui.adsabs.harvard.edu/abs/2010ApJ...713..269F/abstract>`_
+
+.. note::
+   Sink examples are available in ``examples/SinkParticles/``. They include self-gravity, cooling and feedback effects.
+
+.. figure:: ../../../Task/sink.png
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: Task dependencies for the sink scheme.
+
+    This figure shows the task dependencies for the sink scheme.
+    The first rectangle groups the tasks that determine if sink particles will swallow other
+    sink particles or gas particles.
+    In the second one, the gas particles tagged as "to be swallowed" are effectively swallowed.
+    In the third one, the sink particles tagged as "to be swallowed" are effectively swallowed.
+    This was done with SWIFT v0.9.0.
+
+
+Conversion from comoving to physical space
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the following, we always refer to physical quantities. In non-cosmological simulations, there is no ambiguity between comoving and physical quantities since the universe is not expanding, and thus, the scale factor is :math:`a(t)=1`. However, in cosmological simulation, we need to convert from comoving quantities to physical ones when needed, e.g. to compute energies. We denote physical quantities by the subscript `p` and comoving ones by `c`. Here is a recap:
+
+* :math:`\mathbf{x}_p = \mathbf{x}_c a`
+* :math:`\mathbf{v}_p = \mathbf{v}_c/a + a H \mathbf{x}_c`
+* :math:`\rho_p = \rho_c/a^3`
+* :math:`\Phi_p = \Phi_c/a + c(a)`
+* :math:`u_p = u_c/a^{3(\gamma -1)}`
+
+
+Here, :math:`H` is the Hubble constant at any redshift, :math:`c(a)` is the potential normalization constant and :math:`\gamma` the gas adiabatic index. Notice that the potential normalization constant has been chosen to be :math:`c(a) = 0`.
+
+
+Sink formation
+~~~~~~~~~~~~~~
+
+.. figure:: sink_accretion_radius.png
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: GEAR sink accretion radius representation
+
+    This figure shows a sink particle (in orange) newly formed among other gas particles (in blue). The accretion radius is :math:`r_{\text{acc}}`. It is the one used for sink formation. There is also an inner accretion radius :math:`f_{\text{acc}} r_{\text{acc}}` (:math:`0 \leq f_{\text{acc}} \leq 1`) that is used for gas swallowing. Particles within this inner radius are eaten without passing any other check, while particles between the two radii pass some check before being swallowed. 
+
+At the core of the sink particle method is the sink formation algorithm. This is critical to form sinks in regions adequate for star formation. Failing to can produce spurious sinks and stars, which is not desirable. However, there is no easy answer to the question. We chose to implement a simple and efficient algorithm.
+The primary criteria required to transform a gas particle into a sink are:
+
+1. the density of a given particle :math:`i` is exceeds a user-defined threshold density: :math:`\rho_i > \rho_{\text{threshold}}` ;
+2. if the particle's density lies between the threshold density and a user-defined maximal density: :math:`\rho_{\text{threshold}} \leq \rho_i \leq \rho_{\text{maximal}}`, the particle's temperature must also be below a user-defined threshold: :math:`T_i < T_{\text{threshold}}`;
+3. if the particle’s density exceeds the maximal density: :math:`\rho_i > \rho_{\text{threshold}}`, no temperature check is performed.
+
+The first criterion is common, but not the second one. We check the latter to ensure that sink particles, and thus stars, are not generated in hot regions. The third one ensures that if, for some reason, the cooling of the gas is not efficient, but the density gets very high, then we can form a sink. The parameters for those threshold quantities are respectively called ``density_threshold_Hpcm3``, ``maximal_density_threshold_Hpcm3`` and ``temperature_threshold_K``.
+
+Then, further criteria are checked. They are always checked for gas particles within the accretion radius :math:`r_{\text{acc}}` (called the ``cut_off_radius`` in the parameter file) of a given gas particle :math:`i`. Such gas particles are called *neighbours*.
+
+.. note::
+   Notice that in the current implementation, the accretion radius is kept *fixed and the same* for all sinks. However, for the sake of generality, the mathematical expressions are given as if the accretion radii could be different. 
+
+So, the other criteria are the following:
+
+3. The gas particle is at a local potential minimum: :math:`\Phi_i = \min_j \Phi_j`.
+4. Gas surrounding the particle is at rest or collapsing: :math:`\nabla \cdot \mathbf{v}_{i, p} \leq 0`. (Optional)
+5. The smoothing kernel's edge of the particle is less than the accretion radius: :math:`\gamma_k h_i < r_{\text{acc}}`, where :math:`\gamma_k` is kernel dependent. (Optional)
+6. All neighbours are currently active.
+7. The thermal energy of the neighbours satisfies: :math:`E_{\text{therm}} < |E_{\text{pot}}|/2`. (Optional, together with criterion 8.)
+8. The sum of thermal energy and rotational energy satisfies: :math:`E_{\text{therm}} + E_{\text{rot}} < | E_{\text{pot}}|`. (Optional, together with criterion 7.)
+9. The total energy of the neighbours is negative, i.e. the clump is bound to the sink: :math:`E_{\text{tot}} < 0`. (Optional)
+10. Forming a sink here will not overlap an existing sink :math:`s`: :math:`\left| \mathbf{x}_i - \mathbf{x}_s \right| > r_{\text{acc}, i} + r_{\text{acc}, s}`. (Optional)
+
+Some criteria are *optional* and can be *deactivated*. By default, they are all enabled. The different energies are computed as follows:
+
+* :math:`E_{\text{therm}} = \displaystyle \sum_j m_j u_{j, p}`
+* :math:`E_{\text{kin}} = \displaystyle \frac{1}{2} \sum_j m_j (\mathbf{v}_{i, p} - \mathbf{v}_{j, p})^2`
+* :math:`E_{\text{pot}} = \displaystyle \frac{G_N}{2} \sum_j m_i m_j \Phi_{j, p}`
+* :math:`E_{\text{rot}} = \displaystyle \sqrt{E_{\text{rot}, x}^2 + E_{\text{rot}, y}^2 + E_{\text{rot}, z}^2}`
+* :math:`E_{\text{rot}, x} = \displaystyle \frac{1}{2} \sum_j m_j \frac{L_{ij, x}^2}{\sqrt{(y_{i, p} - y_{j, p})^2 + (z_{i,p} - z_{j, p})^2}}`
+* :math:`E_{\text{rot}, y} = \displaystyle \frac{1}{2} \sum_j m_j \frac{L_{ij, y}^2}{\sqrt{(x_{i,p} - x_{j,p})^2 + (z_{i,p} - z_{j,p})^2}}`
+* :math:`E_{\text{rot}, z} = \displaystyle \frac{1}{2} \sum_j m_j \frac{L_{ij, z}^2}{\sqrt{(x_{i, p} - x_{j, p})^2 + (y_{i,p} - y_{j,p})^2}}`
+* The  (physical) specific angular momentum: :math:`\mathbf{L}_{ij} = ( \mathbf{x}_{i, p} - \mathbf{x}_{j, p}) \times ( \mathbf{v}_{i, p} - \mathbf{x}_{j, p})`
+* :math:`E_{\text{mag}} = \displaystyle \sum_j E_{\text{mag}, j}`
+* :math:`E_{\text{tot}} = E_{\text{kin}} + E_{\text{pot}} +  E_{\text{therm}} + E_{\text{mag}}`
+
+.. note::
+   Currently, magnetic energy is not included in the total energy, since the MHD scheme is in progress. However, the necessary modifications have already been taken care of.
+
+   The :math:`p` subscript is to recall that we are using physical quantities to compute energies.
+
+   Here, the potential is retrieved from the gravity solver. 
+
+
+Some comments about the criteria:
+
+The third criterion is mainly here to prevent two sink particles from forming at a distance smaller than the sink accretion radius. Since we allow sinks to merge, such a situation raises the question of which sink should swallow the other. This can depend on the order of the tasks, which is not a desirable property. As a result, this criterion is enforced.
+
+The tenth criterion prevents the formation of spurious sinks. Experiences have shown that removing gas within the accretion radius biases the hydro density estimates: the gas feels a force toward the sink. At some point, there is an equilibrium and gas particles accumulate at the edge of the accretion radius, which can then spawn sink particles that do not fall onto the primary sink and never merge. Moreover, the physical reason behind this criterion is that a sink represents a region of collapse. As a result, there is no need to have many sinks occupying the same space volume. They would compete for gas accretion without necessarily merging. This criterion is particularly meaningful in cosmological simulations to ensure proper sampling of the IMF. *This criterion can be disabled*.
+
+Once a sink is formed, we record it birth time (or scale factor in cosmological runs). This information is used to put the sink into three categories: young, old and dead. If a sink is dead, it cannot accrete gas or sink anymore. However, a dead sink can still be swallowed by a young/old sink. Young and old sink only differ by their maximal allowed timestep. Details are provided in :ref:`sink_GEAR_timesteps`.
+
+.. note::
+  However, notice that contrary to  `Bate et al. <https://ui.adsabs.harvard.edu/abs/1995MNRAS.277..362B/abstract>`_, no boundary conditions for sink particles are introduced in the hydrodynamics calculations.
+
+.. note::
+   Note that sink formation can be disabled. It can be useful, for example if you already have sinks in your initial conditions. 
+
+
+Gas accretion
+~~~~~~~~~~~~~
+
+Now that sink particles can populate the simulation, they need to swallow gas particles. To be accreted, gas particles need to pass a series of criteria. In the following, :math:`s` denotes a sink particle and :math:`i` is a gas particle. The criteria are the following:
+
+#. The sink is not dead. If it is dead, it does not accrete gas. A sink is considered dead if it is older than ``timestep_age_threshold_unlimited_Myr``.
+#. If the gas falls within :math:`f_{\text{acc}} r_{\text{acc}}` (:math:`0 \leq f_{\text{acc}} \leq 1`), the gas is accreted without further check.
+#. In the region  :math:`f_{\text{acc}} r_{\text{acc}} \leq |\mathbf{x}_i| \leq r_{\text{acc}}`, then, we check:
+   
+   #. The specific angular momentum is smaller than the one of a Keplerian orbit at :math:`r_{\text{acc}}`: :math:`|\mathbf{L}_{si}| \leq |\mathbf{L}_{\text{Kepler}}|`.
+   #. The gas is gravitationally bound to the sink particle: :math:`E_{\text{tot}} < 0`.
+   #. The gas size is smaller or equal to the sink size: :math:`\gamma_k h_i \leq r_{\text{acc}}`.
+   #. Out of all pairs of sink-gas, the gas is the most bound to this one. This case is illustrated in the figure below.
+   #. The total swallowed mass does not exceed ``n_IMF`` times the IMF mass (see the IMF sampling section), but make sure to swallow at least one particle: :math:`M_\text{swallowed} \leq n_\text{IMF} M_\text{IMF} \text{ or } M_\text{swallowed} = 0`.
+
+The physical specific angular momenta and the total energy are given by:
+
+* :math:`\mathbf{L}_{si} = ( \mathbf{x}_{s, p} - \mathbf{x}_{i, p}) \times ( \mathbf{v}_{s, p} - \mathbf{x}_{i, p})`,
+* :math:`|\mathbf{L}_{\text{Kepler}}| = r_{\text{acc}, p} \cdot \sqrt{G_N m_s / |\mathbf{x}_{s, p} - \mathbf{x}_{i, p}|^3}`.
+* :math:`E_{\text{tot}} = \frac{1}{2}  (\mathbf{v}_{s, p} - \mathbf{x}_{i, p})^2 - G_N \Phi(|\mathbf{x}_{s, p} - \mathbf{x}_{i, p}|) + m_i u_{i, p}`.
+
+.. note::
+   Here the potential is the softened potential of Swift.
+
+Those criteria are similar to `Price et al. <https://ui.adsabs.harvard.edu/abs/2018PASA...35...31P/abstract>`_ and `Grudic et al. (2021) <https://academic.oup.com/mnras/article/506/2/2199/6276745>`_, with the addition of the internal energy. This term ensures that the gas is cold enough to be accreted. Its main purpose is to avoid gas accretion and star spawning in hot regions far from sink/star-forming regions, which can happen, e.g., if a sink leaves a galaxy.
+
+Let's comment on the fourth criterion, specific to our star formation scheme. This criterion restricts the swallowed mass to avoid spawning too many stars in a single time step. Swallowing too many gas particles in a time-step can lead to instabilities in the hydrodynamics, given that gas particles act as interpolation points. Also, creating many stars at once is prejudicial for two reasons. First, the stars' mass samples an IMF, but the star's metallicities do not. So, all stars end up with the same metal content. This situation does not reflect the history of metal accretion and will lead to poor galaxy properties. Second, we need to specify at runtime the maximal number of memory allocated for extra stars until the next tree rebuild. If we create more stars than this limit, the code will stop and send an error.
+
+Since our politics is not to arbitrarily restrict the accretion using some arbitrary mass accretion rate (in fact, the accretion must be feedback-regulated), we then lower the sink time step to swallow the remaining gas particles soon. So, instead of eating a considerable amount of mass and spawning many stars in a big time step, we swallow smaller amounts of gas/sink and create fewer stars in smaller time steps. Details about the how we reduce the timestep are given in :ref:`sink_GEAR_timesteps`.
+
+Once a gas is eligible for accretion, its properties are assigned to the sink. The sink accretes the *entire* gas particle mass and its properties are updated in the following way:
+
+* :math:`\displaystyle \mathbf{v}_{s, c} = \frac{m_s \mathbf{v}_{s, c} + m_i \mathbf{v}_{i, c}}{m_s + m_i}`,
+* Swallowed physical angular momentum:  :math:`\mathbf{L}_{\text{acc}} = \mathbf{L}_{\text{acc}} + m_i( \mathbf{x}_{s, p} - \mathbf{x}_{i, p}) \times ( \mathbf{v}_{s, p} - \mathbf{x}_{i, p})`,
+* :math:`X_{Z, s} = \dfrac{X_{Z,i} m_i + X_{Z,s} m_s}{m_s + m_i}`, the metal mass fraction for each element,
+* :math:`m_s = m_s + m_i`.
+
+.. figure:: sink_overlapping.png
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: Example of two sinks overlapping
+
+    This figure shows two sink particles (in orange) with gas particles (in blue) falling in the accretion radii of both sinks. In such cases, the gas particles in the overlapping regions are swallowed by the sink they are the most bound to. 
+
+Sink merging
+~~~~~~~~~~~~
+
+Sinks are allowed to merge if they enter one's accretion radius. We merge two sink particles if they respect a set of criteria. The criteria are similar to the gas particles, namely:
+
+#. At least one of the sinks is not dead. A sink is considered dead if it is older than ``timestep_age_threshold_unlimited_Myr``.
+#. If one of the sinks falls within the other's inner accretion radius, :math:`f_{\text{acc}} r_{\text{acc}}` (:math:`0 \leq f_{\text{acc}} \leq 1`), the sinks are merged without further check.
+#. In the region  :math:`f_{\text{acc}} r_{\text{acc}} \leq |\mathbf{x}_i| \leq r_{\text{acc}}`, then, we check:
+
+   #. The specific angular momentum is smaller than the one of a Keplerian orbit at :math:`r_{\text{acc}}`: :math:`|\mathbf{L}_{ss'}| \leq |\mathbf{L}_{\text{Kepler}}|`.
+   #. One sink is gravitationally bound to the other: :math:`E_{\text{mec}, ss'} < 0` or  :math:`E_{\text{mec}, s's} < 0`.
+   #. The total swallowed mass does not exceed ``n_IMF`` times the IMF mass (see the IMF sampling section), but make sure to swallow at least one particle: :math:`M_\text{swallowed} \leq n_\text{IMF} M_\text{IMF} \text{ or } M_\text{swallowed} = 0`.
+
+We compute the angular momenta and total energies in the same manner as gas particles, with the difference that we do not use internal energy. Notice that we have two energies: each sink has a different potential energy since their mass can differ.
+
+When sinks merge, the sink with the smallest mass merges with the sink with the largest. If the two sinks have the same mass, we check the sink ID number and add the smallest ID to the biggest one.
+
+IMF sampling
+~~~~~~~~~~~~
+
+.. figure:: sink_imf.png
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: Initial mass function split into the continuous and discrete part.
+
+    This figure shows an IMF split into two parts by :math:`m_t`: the continuous (orange) and the discrete (blue) part. The IMF mass is :math:`M_\text{IMF} = M_c + M_d`.
+
+Now remains one critical question: how are stars formed in this scheme? Simply, by sampling an IMF. 
+In our scheme, population III stars and population II have two different IMFs. For the sake of simplicity, in the following presentation, we consider only the case of population II stars. However, this can be easily generalized to population III. 
+
+Consider an IMF such as the one above. We split it into two parts at ``minimal_discrete_mass_Msun`` (called :math:`m_t` on the illustration). The reason behind this is that we want to spawn star particles that represent *individual* (massive) stars, i.e. they are "discrete". However, for computational reasons, we cannot afford to spawn every star of the IMF as a single particle. Since the IMF is dominated by low-mass stars (< 8 :math:`M_\odot` and even smaller) that do not end up in supernovae, we would have lots of "passive" stars.
+
+.. note::
+   Recall that currently (July 2024), GEAR only implements SNIa and SNII as stellar feedback. Stars that do not undergo supernovae phases are "passive" in the current implementation.
+
+As a result, we group all those low-mass stars in one stellar particle of mass ``stellar_particle_mass_Msun``. Such star particles are called "continuous", contrary to the "discrete" individual stars.  With all that information, we can compute the number of stars in the continuous part of the IMF (called :math:`N_c`) and in the discrete part (called :math:`N_d`). Finally, we can compute the probabilities of each part, respectively called :math:`P_c` and :math:`P_d`. Notice that the mathematical derivation is given in the theory latex files.
+
+Thus, the algorithm to sample the IMF and determine the sink's ``target_mass`` is the following :
+
+* draw a random number :math:`\chi`  from a uniform distribution in the interval :math:`(0 , \; 1 ]`;
+* if  :math:`\chi < P_c`: ``sink.target_mass = stellar_particle_mass``;
+* else: ``sink_target_mass = sample_IMF_high()``.
+
+We have assumed that we have a function ``sample_IMF_high()`` that correctly samples the IMF in the discrete part.
+
+Now, what happens to the sink? After a sink forms, we give it a target mass with the abovementioned algorithm. The sink then swallows gas particles (see the task graph at the top of the page) and spawns stars.  *While the sink possesses enough mass*, we can continue to choose a new target mass. When the sink does have enough mass, the algorithm stops for this timestep. The next timestep, the sink may accrete gas and spawn stars again. The sink cannot spawn stars if it never reaches the target mass. In practice, sink particles could accumulate enough pass to spawn individual (Pop III) stars with masses 240 :math:`M_\odot` and more!
+
+For low-resolution simulations (:math:`m_\text{gas} > 100 \; M_\odot`), we also add a minimal sink mass constraint: the sink can spawn a star if ``m_sink > target_mass`` *and* ``m_sink - target_mass >= minimal_mass``. In low-resolution simulation, when a gas particle turns into a sink, the latter can have enough mass to spawn stars, depending on the sink stars and IMF parameters. As a result, the sink spawns the stars and then ends up with :math:`m_\text{sink} \ll m_\text{gas}`. Such a situation is detrimental for two reasons: 1) the sink mass is so low that gas can seldom be bound to it and thus stops spawning stars and 2) the sink can get kicked away by gravitational interactions due to the high mass difference. The parameter controlling the sink's minimal mass is ``GEARSink:sink_minimal_mass_Msun``.
+
+As explained at the beginning of this section, GEAR uses two IMFs for the population of II and III stars. The latter are called the first stars in the code. How does a sink decide which IMF to draw the target mass from? We define a threshold metallicity, ``GEARFeedback:imf_transition_metallicity`` that determines the first stars' maximal metallicity. When the sink particle's metallicity exceeds this threshold, it uses the population II IMF, defined in ``GEARFeedback:yields_table``.
+
+Star spawning
+~~~~~~~~~~~~~
+
+Once the sink spawns a star particle, we need to give properties to the star. From the sink, the star inherits the chemistry properties. The star is placed randomly within the sink's accretion radius. We draw the star's velocity components from a Gaussian distribution with mean :math:`\mu = 0` and standard deviation :math:`\sigma` determined as follows:
+
+.. math::
+   \sigma = f \cdot \sqrt{\frac{G_N M_s}{r_{\text{acc}}}} \; ,
+
+where :math:`G_N` is Newton's gravitational constant, math:`M_s` is the sink's mass before starting to spawn stars, and :math:`f` is a user-defined scaling factor. The latter corresponds to the ``star_spawning_sigma_factor`` parameter.
+
+
+Stellar feedback
+~~~~~~~~~~~~~~~~
+
+Stellar feedback *per se* is not in the sink module but in the feedback one. However, if one uses sink particles with individual stars, the feedback implementation must be adapted. Here is a recap of the GEAR feedback with sink particles. 
+
+All details and explanations about GEAR stellar feedback are provided in the GEAR :ref:`gear_stellar_evolution_and_feedback` section. Here, we only provide the changes from the previous model.
+
+In the previous model, star particles represented a population of stars with a defined IMF. Now, we have two kinds of star particles: particles representing a *continuous* portion of the IMF (see the image above) and particles representing a *single* (discrete) star. This new model requires updating the feedback model so that stars eligible for SN feedback can realise this feedback.
+
+**Discrete star particles:** Since we now have individual star particles, we can easily track SNII feedback for stars with a mass larger than 8 :math:`M_\odot`. When a star's age reaches its lifetime, it undergoes SNII feedback.
+
+**Continuous star particles**: In this case, we implemented SNII and SNIa as in the previous model. At each timestep, we determine the number of SN explosions occurring. In practice, this means that we can set the ``minimal_discrete_masss`` to any value, and the code takes care of the rest.
diff --git a/doc/RTD/source/SubgridModels/GEAR/supernova_feedback.rst b/doc/RTD/source/SubgridModels/GEAR/supernova_feedback.rst
new file mode 100644
index 0000000000000000000000000000000000000000..9a83d47c21da93165c3d3fee5edb0fe8cb7c90c6
--- /dev/null
+++ b/doc/RTD/source/SubgridModels/GEAR/supernova_feedback.rst
@@ -0,0 +1,40 @@
+.. Supernova feedback in GEAR model
+   Darwin Roduit, 30 March 2025
+
+.. gear_sn_feedback_models:
+
+.. _gear_sn_feedback_models:
+
+GEAR supernova feedback
+=======================
+
+When a star goes into a supernova, we compute the amount of internal energy, mass and metals the explosion transfers to the star's neighbouring gas particles. We will group all these in the “fluxes” term.  
+We have two models for the distribution of these fluxes and the subgrid modelling of the supernovae: GEAR model and GEAR mechanical model.
+
+.. note::
+   We may sometimes refer to GEAR feedback as GEAR thermal feedback to clearly distinguish it from GEAR mechanical feedback.
+
+
+.. _gear_sn_feedback_gear_thermal:
+
+GEAR model
+----------
+
+In the GEAR (thermal) model, the fluxes are distributed by weighing with the SPH kernel:
+
+.. math::
+
+   w_{{sj}} = W_i(\| \vec{{x}}_{{sj}} \|, \, h_s) \frac{{m_j}}{{\rho_s}}
+
+for :math:`s` the star and :math:`j` the gas (`Revaz and Jablonka 2012 <https://ui.adsabs.harvard.edu/abs/2012A%26A...538A..82R/abstract>`_).
+
+In the GEAR model, we do not inject momentum, only *internal energy*. Then, internal energy conversion to kinetic energy is left to the hydrodynamic solver, which will compute appropriately the gas density, temperature and velocity.  
+However, if the cooling radius :math:`R_{\text{cool}}` of the explosion is unresolved, i.e. the cooling radius is smaller than our simulation resolution, the cooling radiates away the internal energy.
+
+To understand why this happens, let us remind the main phases of an SN explosion in a homogeneous medium. We provide a simple picture that is more complicated than the one explained here. See `Haid et al. 2016 <https://ui.adsabs.harvard.edu/abs/2016MNRAS.460.2962H/abstract>`_ or `Thornton et al. 1998 <https://iopscience.iop.org/article/10.1086/305704>`_ for further details.
+
+* The first stage of the SN explosion is the **free expansion**. In this momentum-conserving regime, the ejecta of the stars sweeps the ISM. At the end of this phase, 72% of the initial SN energy has been converted to thermal energy.
+* Once the SN ejecta has swept an ISM mass of comparable mass, the blast wave enters the **energy-conserving Sedov-Taylor phase**. It continues with an adiabatic expansion, performing some :math:`P \, \mathrm{d}V` work on the gas. In this phase, the internal energy is converted into kinetic energy as the ejecta continues sweeping the ISM gas. This phase continues until radiative losses become significant after some radius :math:`R_{\text{cool}}`.
+* At this point, the blast wave enters the **momentum-conserving snowplough phase** and forms a thin shell. In this regime, efficient cooling radiates away the internal energy, and thus, the blast wave slows down.
+
+Now, we better understand why the internal energy is radiated away. It is a consequence of efficient cooling in the snowplough phase. When this happens, the feedback is unresolved and its energy does not affect the ISM, apart from the mass and metal injection. To circumvent this problem, GEAR thermal feedback implements a **fixed delayed cooling mechanism**. The cooling of the particles affected by feedback is deactivated during some mega year, usually 5 Myr in our simulations. The time is controlled by the ``GrackleCooling:thermal_time_myr`` parameter. This mechanism allows the internal energy to transform into kinetic energy without immediately being radiated away. However, such an approach poses the question of the time required to prevent gas from cooling in the simulations.
diff --git a/doc/RTD/source/Task/adding_your_own.rst b/doc/RTD/source/Task/adding_your_own.rst
index fe89807e2531d64c399064b05280ffc677204002..95bed21c5d8833a62242aedb89e443ead01c5657 100644
--- a/doc/RTD/source/Task/adding_your_own.rst
+++ b/doc/RTD/source/Task/adding_your_own.rst
@@ -217,7 +217,8 @@ and the second kick cannot be done before the cooling::
 
 
 The next step is to activate your task
-in ``engine_marktasks_mapper`` in ``engine_marktasks.c``::
+in the relevant section of ``cell_unskip.c`` (things are split
+by type of particles the tasks act on)::
 
   else if (t->type == task_type_cooling || t->type == task_type_sourceterms) {
     if (cell_is_active_hydro(t->ci, e)) scheduler_activate(s, t);
diff --git a/doc/RTD/source/Task/adding_your_own_neighbour_loop.rst b/doc/RTD/source/Task/adding_your_own_neighbour_loop.rst
index 937421eb7e80e136c5d66a0b2706a9d104d94212..076245820f3e3d88563f7f3b2b07c8f37671ea11 100644
--- a/doc/RTD/source/Task/adding_your_own_neighbour_loop.rst
+++ b/doc/RTD/source/Task/adding_your_own_neighbour_loop.rst
@@ -296,8 +296,9 @@ call::
 
 
 
-The next step is to activate your task
-in ``engine_marktasks_mapper`` in ``engine_marktasks.c``::
+The next step is to activate your task in the relevant section of
+ ``cell_unskip.c`` (things are split by the type of particles the
+ tasks run on)::
 
 
   /* Single-cell task? */
diff --git a/doc/RTD/source/conf.py b/doc/RTD/source/conf.py
index 17c62e4d3a2f2ee7e58c968165bcfb09374fa5a1..8c30cb88cea56c84803b74ee26ca912531c80680 100644
--- a/doc/RTD/source/conf.py
+++ b/doc/RTD/source/conf.py
@@ -23,9 +23,9 @@ copyright = "2014-2023, SWIFT Collaboration"
 author = "SWIFT Team"
 
 # The short X.Y version
-version = "1.0"
+version = "2025.01"
 # The full version, including alpha/beta/rc tags
-release = "1.0.0"
+release = "2025.01"
 
 # -- Find additional scripts to run as part of the documentation build -------
 import glob
diff --git a/doc/onboardingGuide/README.md b/doc/onboardingGuide/README.md
index 53ab61249453e58faa7bcec715eeef9ef85c0d5e..773e77fb5c661c3290d3439f421250db19557fd1 100644
--- a/doc/onboardingGuide/README.md
+++ b/doc/onboardingGuide/README.md
@@ -1,7 +1,7 @@
 SWIFT Onboarding Guide
 ======================
 
-This is an onboarding guide for SWIFT that can be found on `swiftsim.com/onboarding.pdf`.
+This is an onboarding guide for SWIFT that can be found on `https://swift.strw.leidenuniv.nl/onboarding.pdf`.
 
 You will need the `sphinx` and python package (pip install it), as well as a working
 TeX distribution.
diff --git a/doc/onboardingGuide/source/dependencies.rst b/doc/onboardingGuide/source/dependencies.rst
index ba63589f8a47f52929d8fca57e3d06f6b1dbb748..33d64eaa3ca476ed180a13cad0ac92b0ad5992de 100644
--- a/doc/onboardingGuide/source/dependencies.rst
+++ b/doc/onboardingGuide/source/dependencies.rst
@@ -8,9 +8,9 @@ To compile SWIFT, you will need the following libraries:
 HDF5
 ~~~~
 
-Version 1.8.x or higher is required. Input and output files are stored as HDF5
+Version 1.10.x or higher is required. Input and output files are stored as HDF5
 and are compatible with the GADGET-2 specification. A parallel-HDF5 build
-and HDF5 > 1.10.x is strongly recommended when running over MPI.
+and HDF5 >= 1.12.x is recommended when running over MPI.
 
 MPI
 ~~~
@@ -33,7 +33,15 @@ GSL
 ~~~
 The GSL 2.x is required for cosmological integration.
 
+In most cases the configuration script will be able to detect the libraries
+installed on the system. If that is not the case, the script can be pointed
+towards the libraries' location using the following parameters
 
+.. code-block:: bash
+
+  ./configure --with-gsl=<PATH-TO-GSL>
+
+and similar for the other libaries.
 
 Optional Dependencies
 =====================
@@ -48,10 +56,6 @@ TCmalloc/Jemalloc/TBBmalloc
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 TCmalloc/Jemalloc/TBBmalloc are used for faster memory allocations when available.
 
-DOXYGEN
-~~~~~~~
-You can build documentation for SWIFT with DOXYGEN.
-
 Python
 ~~~~~~
 To run the examples, you will need python 3 and some of the standard scientific libraries (numpy, matplotlib).
diff --git a/doc/onboardingGuide/source/getting_help.rst b/doc/onboardingGuide/source/getting_help.rst
index d2fe40be5d404e17272e0cfda47bed59b5aab4ea..cc374ebbf115a6c56c872e33725a572054f00554 100644
--- a/doc/onboardingGuide/source/getting_help.rst
+++ b/doc/onboardingGuide/source/getting_help.rst
@@ -11,4 +11,4 @@ by creating an issue.
 The code documentation is available on `swiftsim.com/docs <https://swiftsim.com/docs>`_, and
 is also shipped along with the code in the ``docs/RTD`` directory. 
 This onboarding guide is available online as well on 
-`swiftsim.com/onboarding.pdf <http://www.swiftsim.com/onboarding.pdf>`_
+`swiftsim.com/onboarding.pdf <https://swift.strw.leidenuniv.nl/onboarding.pdf>`_
diff --git a/doc/onboardingGuide/source/initial_setup.rst b/doc/onboardingGuide/source/initial_setup.rst
index 947aa1973a2ba656ef0a7d0e4e24fb154f4e5fb6..669d50e26ecf0f84de0318a9b1affe850ec7bd9e 100644
--- a/doc/onboardingGuide/source/initial_setup.rst
+++ b/doc/onboardingGuide/source/initial_setup.rst
@@ -13,13 +13,13 @@ We use autotools for setup. To get a basic running version of the code (the exec
 MacOS Specific Oddities
 ~~~~~~~~~~~~~~~~~~~~~~~
 
-To build on MacOS you will need to disable compiler warnings due to an
+To build on MacOS you will need to enable compiler warnings due to an
 incomplete implementation of pthread barriers. DOXYGEN also has some issues on
 MacOS, so it is best to leave it out. To configure:
 
 .. code-block:: bash
 
-  ./configure --disable-compiler-warnings \ 
+  ./configure --enable-compiler-warnings \
       --disable-doxygen-doc
 
 When using the ``clang`` compiler, the hand-written vectorized routines
diff --git a/examples/AGORA/AgoraDisk/agora_disk.yml b/examples/AGORA/AgoraDisk/agora_disk.yml
index b1064165ffe3328a56b73c826ee073cb3e538581..b4bacb79dcd0fee9c2fe52b70466689ffd7c54b1 100644
--- a/examples/AGORA/AgoraDisk/agora_disk.yml
+++ b/examples/AGORA/AgoraDisk/agora_disk.yml
@@ -79,15 +79,16 @@ GrackleCooling:
   max_steps: 1000
   convergence_limit: 1e-2
   thermal_time_myr: 0
+  maximal_density_Hpcm3: -1   # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
 
 
 GEARStarFormation:
   star_formation_mode: agora        # default or agora
   star_formation_efficiency: 0.01   # star formation efficiency (c_*)
-  maximal_temperature:  1e10        # Upper limit to the temperature of a star forming particle
+  maximal_temperature_K:     1e10   # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   10     # Density threshold in Hydrogen atoms/cm3
   n_stars_per_particle: 1
   min_mass_frac: 0.5
-  density_threshold:   1.67e-23     # Density threashold in g/cm3
 
 
 GEARPressureFloor:
diff --git a/examples/ChemistryTests/MetalAdvectionTestEAGLE/README b/examples/ChemistryTests/MetalAdvectionTestEAGLE/README
new file mode 100644
index 0000000000000000000000000000000000000000..385cf86d6c87635ab4b7dd9bd2b868e40222e095
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestEAGLE/README
@@ -0,0 +1,16 @@
+Metal advection test for the EAGLE chemistry scheme
+
+In some schemes, like the meshless (gizmo) scheme, particles exchange 
+masses via fluxes. This example tests whether the metals are correctly
+advected with those mass fluxes (for the EAGLE chemistry scheme)
+
+To run this test, compile with:
+    --with-hydro-dimension=2 --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-chemistry=EAGLE
+
+Due to the nature of this test, not much mass will be exchanged when running in Lagrangian mode,
+and hence not much advection will happen.
+To be able to see the effect of the advection, the hydrodynamics must be run in Eulerian mode.
+E.g. for gizmo-mvf: uncomment `#define GIZMO_FIX_PARTICLES` in src/const.h.
+
+Expected output when running in Eulerian mode is that the profiles should maintain their shape,
+but edges will be blurred due to numerical diffusion.
diff --git a/examples/Planetary/SquareTest_2D/square.yml b/examples/ChemistryTests/MetalAdvectionTestEAGLE/advect_metals.yml
similarity index 56%
rename from examples/Planetary/SquareTest_2D/square.yml
rename to examples/ChemistryTests/MetalAdvectionTestEAGLE/advect_metals.yml
index 284ebad4156d3d56dd7ea653107acadcaddd7c6a..ca4de5cee37178748f69a6c069c0363bd5f6006d 100644
--- a/examples/Planetary/SquareTest_2D/square.yml
+++ b/examples/ChemistryTests/MetalAdvectionTestEAGLE/advect_metals.yml
@@ -1,3 +1,6 @@
+MetaData:
+  run_name: advect_metals_EAGLE
+
 # Define the system of units to use internally. 
 InternalUnitSystem:
   UnitMass_in_cgs:     1   # Grams
@@ -9,31 +12,29 @@ InternalUnitSystem:
 # Parameters governing the time integration
 TimeIntegration:
   time_begin: 0.    # The starting time of the simulation (in internal units).
-  time_end:   4.   # The end time of the simulation (in internal units).
-  dt_min:     1e-6  # The minimal time-step size of the simulation (in internal units).
-  dt_max:     1e-2  # The maximal time-step size of the simulation (in internal units).
+  time_end:   0.1   # The end time of the simulation (in internal units).
+  dt_min:     1e-9  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-1  # The maximal time-step size of the simulation (in internal units).
+
 
 # Parameters governing the snapshots
 Snapshots:
-  basename:            square # Common part of the name of output files
+  basename:            output # Common part of the name of output files
   time_first:          0.     # Time of the first output (in internal units)
-  delta_time:          1e-1   # Time difference between consecutive outputs (in internal units)
+  delta_time:          0.1    # Time between snapshots
 
 # Parameters governing the conserved quantities statistics
 Statistics:
-  delta_time:          1e-2 # Time between statistics output
+  time_first:          0.
+  delta_time:          1e-1   # Time between statistics output
 
 # Parameters for the hydrodynamics scheme
 SPH:
   resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
   CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
-  viscosity_alpha:       0.8      # Override for the initial value of the artificial viscosity. In schemes that have a fixed AV, this remains as alpha throughout the run.
-  
+
 # Parameters related to the initial conditions
 InitialConditions:
-  file_name:  ./square.hdf5     # The file to read
-  periodic:   1
+  file_name:  ./advect_metals.hdf5     # The file to read
+  periodic:   1                      # periodic ICs.
 
-# Parameters related to the equation of state
-EoS:
-    planetary_use_idg_def:    1               # Default ideal gas, material ID 0
diff --git a/examples/ChemistryTests/MetalAdvectionTestEAGLE/getGlass.sh b/examples/ChemistryTests/MetalAdvectionTestEAGLE/getGlass.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a958311b6de07663c7f7c70cff9e95d86ce3efb2
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestEAGLE/getGlass.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/glassPlane_64.hdf5
diff --git a/examples/ChemistryTests/MetalAdvectionTestEAGLE/makeIC.py b/examples/ChemistryTests/MetalAdvectionTestEAGLE/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..7e301bf431d108b500e822bed9276afb09a09d66
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestEAGLE/makeIC.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import math
+
+# ---------------------------------------------------------------------
+# Test the diffusion/advection of metals by creating regions with/without
+# initial metallicities. Run with EAGLE chemistry.
+# ---------------------------------------------------------------------
+
+import numpy as np
+import h5py
+
+GAMMA = 5 / 3
+RHO = 1
+P = 1
+VELOCITY = 2.5
+ELEMENT_COUNT = 9
+
+outputfilename = "advect_metals.hdf5"
+
+
+def get_masks(boxsize, pos):
+    masks = dict()
+    x = boxsize[0]
+    y = boxsize[1]
+
+    right_half_mask = pos[:, 0] > x / 2
+    masks["TotalMetallicity"] = right_half_mask
+    masks["He"] = pos[:, 0] * 2 % x > x / 2
+    masks["C"] = right_half_mask & (pos[:, 0] < 0.75 * x)
+    masks["N"] = right_half_mask & (pos[:, 0] > 0.75 * x)
+    masks["O"] = right_half_mask & (pos[:, 1] > 0.5 * y)
+    masks["Ne"] = right_half_mask & (pos[:, 1] < 0.5 * y)
+    masks["Mg"] = right_half_mask & (pos[:, 0] < 0.75 * x) & (pos[:, 1] > 0.5 * y)
+    masks["Si"] = right_half_mask & (pos[:, 0] > 0.75 * x) & (pos[:, 1] > 0.5 * y)
+    masks["Fe"] = right_half_mask & (pos[:, 0] < 0.75 * x) & (pos[:, 1] < 0.5 * y)
+
+    return masks
+
+
+def get_element_abundances_metallicity(pos, boxsize):
+    elements = ["He", "C", "N", "O", "Ne", "Mg", "Si", "Fe"]
+    abundances = [0.2, 0.2, 0.2, 0.2, 0.2, 0.1, 0.1, 0.1]
+
+    masks = get_masks(boxsize, pos)
+    total_metallicity = np.where(masks["TotalMetallicity"], 0.7, 0.1)
+    element_abundances = [
+        np.where(masks[k], v, 0) for k, v in zip(elements, abundances)
+    ]
+
+    # Finally add H so that H + He + TotalMetallicity = 1
+    return (
+        np.stack(
+            [1 - element_abundances[0] - total_metallicity] + element_abundances, axis=1
+        ),
+        total_metallicity,
+    )
+
+
+if __name__ == "__main__":
+    glass = h5py.File("glassPlane_64.hdf5", "r")
+    parts = glass["PartType0"]
+    pos = parts["Coordinates"][:]
+    pos = np.concatenate([pos, pos + np.array([1, 0, 0])])
+    h = parts["SmoothingLength"][:]
+    h = np.concatenate([h, h])
+    glass.close()
+
+    # Set up metadata
+    boxsize = np.array([2.0, 1.0, 1.0])
+    n_part = len(h)
+    ids = np.arange(n_part) + 1
+
+    # Setup other particle quantities
+    rho = RHO * np.ones_like(h)
+    rho[pos[:, 1] < 0.5 * boxsize[1]] *= 0.5
+    masses = rho * np.prod(boxsize) / n_part
+    velocities = np.zeros((n_part, 3))
+    velocities[:, :] = 0.5 * math.sqrt(2) * VELOCITY * np.array([1.0, 1.0, 0.0])
+    internal_energy = P / (rho * (GAMMA - 1))
+
+    # Setup metallicities
+    element_abundances, metallicities = get_element_abundances_metallicity(pos, boxsize)
+
+    # Now open the file and write the data.
+    file = h5py.File(outputfilename, "w")
+
+    # Header
+    grp = file.create_group("/Header")
+    grp.attrs["BoxSize"] = boxsize
+    grp.attrs["NumPart_Total"] = [n_part, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [n_part, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFilesPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = 0
+    grp.attrs["Dimension"] = 2
+
+    # Units
+    grp = file.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = file.create_group("/PartType0")
+    grp.create_dataset("Coordinates", data=pos, dtype="d")
+    grp.create_dataset("Velocities", data=velocities, dtype="f")
+    grp.create_dataset("Masses", data=masses, dtype="f")
+    grp.create_dataset("SmoothingLength", data=h, dtype="f")
+    grp.create_dataset("InternalEnergy", data=internal_energy, dtype="f")
+    grp.create_dataset("ParticleIDs", data=ids, dtype="L")
+    grp.create_dataset("Metallicity", data=metallicities, dtype="f")
+    grp.create_dataset("ElementAbundance", data=element_abundances, dtype="f")
+
+    file.close()
+
+    # close up, and we're done!
+    file.close()
diff --git a/examples/ChemistryTests/MetalAdvectionTestEAGLE/plotAdvectedMetals.py b/examples/ChemistryTests/MetalAdvectionTestEAGLE/plotAdvectedMetals.py
new file mode 100644
index 0000000000000000000000000000000000000000..87cdeeda6962cb138b1410c9f49416f104b0611a
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestEAGLE/plotAdvectedMetals.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import math
+
+import matplotlib.pyplot as plt
+from matplotlib.cm import ScalarMappable
+from matplotlib.colors import Normalize
+import numpy as np
+import unyt
+
+import swiftsimio
+from swiftsimio.visualisation import project_gas
+from pathlib import Path
+
+from makeIC import VELOCITY
+
+
+def plot_single(ax_mass, ax_fraction, name, title, data, mass_map, kwargs_inner):
+    mass_weighted_map = project_gas(data, project=name, **kwargs_inner["projection"])
+    ax_mass.imshow(mass_weighted_map.in_cgs().value.T, **kwargs_inner["imshow_mass"])
+    ax_mass.set_title(title)
+    ax_fraction.imshow(
+        (mass_weighted_map / mass_map).T, **kwargs_inner["imshow_fraction"]
+    )
+    ax_fraction.set_title(title)
+    ax_mass.axis("off")
+    ax_fraction.axis("off")
+
+
+def plot_all(fname, savename):
+    data = swiftsimio.load(fname)
+
+    # Shift coordinates to starting position
+    velocity = 0.5 * math.sqrt(2) * VELOCITY * np.array([1, 1, 0]) * unyt.cm / unyt.s
+    data.gas.coordinates -= data.metadata.time * velocity
+
+    # Add mass weighted element mass fractions to gas dataset
+    masses = data.gas.masses
+    element_mass_fractions = data.gas.element_mass_fractions
+    data.gas.element_mass_H = element_mass_fractions.hydrogen * masses
+    data.gas.element_mass_He = element_mass_fractions.helium * masses
+    data.gas.element_mass_C = element_mass_fractions.carbon * masses
+    data.gas.element_mass_N = element_mass_fractions.nitrogen * masses
+    data.gas.element_mass_O = element_mass_fractions.oxygen * masses
+    data.gas.element_mass_Ne = element_mass_fractions.neon * masses
+    data.gas.element_mass_Mg = element_mass_fractions.magnesium * masses
+    data.gas.element_mass_Si = element_mass_fractions.silicon * masses
+    data.gas.element_mass_Fe = element_mass_fractions.iron * masses
+    data.gas.total_metal_mass = data.gas.metal_mass_fractions * masses
+
+    # Create necessary figures and axes
+    fig = plt.figure(layout="constrained", figsize=(16, 8))
+    fig.suptitle(
+        f"Profiles shifted to starting position after t={data.metadata.time:.2f}",
+        fontsize=14,
+    )
+    fig_ratios, fig_masses = fig.subfigures(2, 1)
+    fig_ratios.suptitle("Mass ratio of elements")
+    fig_masses.suptitle("Surface density in elements")
+    axes_ratios = fig_ratios.subplots(2, 5, sharex=True, sharey=True)
+    axes_masses = fig_masses.subplots(2, 5, sharex=True, sharey=True)
+
+    # parameters for swiftsimio projections
+    projection_kwargs = {
+        "region": np.array([0, 2, 0, 1, 0, 1]) * unyt.cm,
+        "resolution": 500,
+        "parallel": True,
+    }
+    # Parameters for imshow
+    norm_ratios = Normalize(0, 0.95)
+    norm_masses = Normalize(0, 0.9)
+    imshow_fraction_kwargs = dict(norm=norm_ratios, cmap="rainbow")
+    imshow_mass_kwargs = dict(norm=norm_masses, cmap="turbo")
+
+    # Plot the quantities
+    mass_map = project_gas(data, project="masses", **projection_kwargs)
+
+    # Parameters for plotting:
+    plotting_kwargs = dict(
+        data=data,
+        mass_map=mass_map,
+        kwargs_inner=dict(
+            projection=projection_kwargs,
+            imshow_mass=imshow_mass_kwargs,
+            imshow_fraction=imshow_fraction_kwargs,
+        ),
+    )
+
+    plot_single(
+        axes_masses[0][0],
+        axes_ratios[0][0],
+        "total_metal_mass",
+        "All metals",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[0][1],
+        axes_ratios[0][1],
+        "element_mass_H",
+        "Hydrogen",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[0][2],
+        axes_ratios[0][2],
+        "element_mass_He",
+        "Helium",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[0][3],
+        axes_ratios[0][3],
+        "element_mass_C",
+        "Carbon",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[0][4],
+        axes_ratios[0][4],
+        "element_mass_N",
+        "Nitrogen",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[1][0],
+        axes_ratios[1][0],
+        "element_mass_O",
+        "Oxygen",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[1][1],
+        axes_ratios[1][1],
+        "element_mass_Ne",
+        "Neon",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[1][2],
+        axes_ratios[1][2],
+        "element_mass_Mg",
+        "Magnesium",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[1][3],
+        axes_ratios[1][3],
+        "element_mass_Si",
+        "Silicon",
+        **plotting_kwargs,
+    )
+    plot_single(
+        axes_masses[1][4],
+        axes_ratios[1][4],
+        "element_mass_Fe",
+        "Iron",
+        **plotting_kwargs,
+    )
+
+    # Add Colorbars
+    cb_masses = fig_masses.colorbar(
+        ScalarMappable(**imshow_mass_kwargs), shrink=0.75, pad=0.01, ax=axes_masses
+    )
+    cb_ratios = fig_ratios.colorbar(
+        ScalarMappable(**imshow_fraction_kwargs), shrink=0.75, pad=0.01, ax=axes_ratios
+    )
+    cb_masses.ax.set_ylabel("Surface density (g/cm^2)", rotation=270, labelpad=15)
+    cb_ratios.ax.set_ylabel("Mass ratio", rotation=270, labelpad=15)
+
+    # Save output
+    fig.savefig(savename, dpi=300)
+
+
+if __name__ == "__main__":
+    cwd = Path(__file__).parent
+    print("Plotting metals for output_0000.hdf5...")
+    plot_all(cwd / "output_0000.hdf5", savename=cwd / "metal_advection_0000.png")
+    print("Plotting metals for output_0001.hdf5...")
+    plot_all(cwd / "output_0001.hdf5", savename=cwd / "metal_advection_0001.png")
+    print("Done!")
diff --git a/examples/ChemistryTests/MetalAdvectionTestEAGLE/run.sh b/examples/ChemistryTests/MetalAdvectionTestEAGLE/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d5b2f5c475fa3bc458e19c6d0190a8d4ded64c70
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestEAGLE/run.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -e glassPlane_64.hdf5 ]
+then
+    echo "Fetching initial glass file ..."
+    ./getGlass.sh
+fi
+
+if [ ! -f 'advect_metals.hdf5' ]; then
+    echo "Generating ICs"
+    python3 makeIC.py
+fi
+
+# Run SWIFT (must be compiled with --with-chemistry=EAGLE)
+../../../swift \
+    --hydro --threads=4 advect_metals.yml 2>&1 | tee output.log
+
+python3 runSanityChecksAdvectedMetals.py
+python3 plotAdvectedMetals.py
diff --git a/examples/ChemistryTests/MetalAdvectionTestEAGLE/runSanityChecksAdvectedMetals.py b/examples/ChemistryTests/MetalAdvectionTestEAGLE/runSanityChecksAdvectedMetals.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cc47db8c08e7385595263ac45ea31d3f2cdfe4a
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestEAGLE/runSanityChecksAdvectedMetals.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from pathlib import Path
+
+import numpy as np
+import swiftsimio
+
+
+def test(name):
+    def test_decorator(test_func):
+        def test_runner(*args, **kwargs):
+            try:
+                test_func(*args, **kwargs)
+            except Exception as e:
+                print(f"\tTest {name} failed!")
+                raise e
+            else:
+                print(f"\tTest {name} \u2705")
+
+        return test_runner
+
+    return test_decorator
+
+
+def approx_equal(a, b, threshold=0.001):
+    return 2 * abs(a - b) / (abs(a) + abs(b)) < threshold
+
+
+@test("mass fractions sum to 1")
+def test_sum_mass_fractions(data):
+    metal_fractions = data.gas.metal_mass_fractions
+    h_fraction = data.gas.element_mass_fractions.hydrogen
+    he_fraction = data.gas.element_mass_fractions.helium
+    total = metal_fractions + he_fraction + h_fraction
+
+    assert np.all(approx_equal(total, 1))
+
+
+@test("total metal mass conservation")
+def test_total_metal_mass_conservation(data_start, data_end):
+    def metal_mass(data):
+        return data.gas.masses * data.gas.metal_mass_fractions
+
+    assert approx_equal(np.sum(metal_mass(data_start)), np.sum(metal_mass(data_end)))
+
+
+def element_mass(data, element_name):
+    return data.gas.masses * getattr(data.gas.element_mass_fractions, element_name)
+
+
+@test("hydrogen mass conservation")
+def test_h_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "hydrogen")),
+        np.sum(element_mass(data_end, "hydrogen")),
+    )
+
+
+@test("helium mass conservation")
+def test_he_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "helium")),
+        np.sum(element_mass(data_end, "helium")),
+    )
+
+
+@test("carbon mass conservation")
+def test_c_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "carbon")),
+        np.sum(element_mass(data_end, "carbon")),
+    )
+
+
+@test("nitrogen mass conservation")
+def test_n_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "nitrogen")),
+        np.sum(element_mass(data_end, "nitrogen")),
+    )
+
+
+@test("oxygen mass conservation")
+def test_o_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "oxygen")),
+        np.sum(element_mass(data_end, "oxygen")),
+    )
+
+
+@test("neon mass conservation")
+def test_ne_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "neon")), np.sum(element_mass(data_end, "neon"))
+    )
+
+
+@test("magnesium mass conservation")
+def test_mg_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "magnesium")),
+        np.sum(element_mass(data_end, "magnesium")),
+    )
+
+
+@test("silicon mass conservation")
+def test_si_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "silicon")),
+        np.sum(element_mass(data_end, "silicon")),
+    )
+
+
+@test("iron mass conservation")
+def test_fe_mass_conservation(data_start, data_end):
+    assert approx_equal(
+        np.sum(element_mass(data_start, "iron")), np.sum(element_mass(data_end, "iron"))
+    )
+
+
+if __name__ == "__main__":
+    print("Running sanity checks...")
+
+    cwd = Path(__file__).parent
+    start = swiftsimio.load(cwd / "output_0000.hdf5")
+    end = swiftsimio.load(cwd / "output_0001.hdf5")
+
+    test_sum_mass_fractions(end)
+    test_total_metal_mass_conservation(start, end)
+    test_h_mass_conservation(start, end)
+    test_he_mass_conservation(start, end)
+    test_c_mass_conservation(start, end)
+    test_n_mass_conservation(start, end)
+    test_o_mass_conservation(start, end)
+    test_ne_mass_conservation(start, end)
+    test_mg_mass_conservation(start, end)
+    test_si_mass_conservation(start, end)
+    test_fe_mass_conservation(start, end)
+
+    print("Done!")
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/README b/examples/ChemistryTests/MetalAdvectionTestGEAR/README
new file mode 100644
index 0000000000000000000000000000000000000000..5f16d8ee8e7899fb8ac6db1bfbb504e1821dae89
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/README
@@ -0,0 +1,23 @@
+Metal advection test for the GEAR/AGORA chemistry schemes
+
+In some schemes, like the meshless (gizmo) scheme, particles exchange 
+masses via fluxes. This example tests whether the metals are correctly
+advected with those mass fluxes.
+
+To run this test with GEAR, compile with:
+    --with-hydro-dimension=2 --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-chemistry=GEAR_X
+with X the desired number of elements. The number of elements must be also be set in the makeIC.py script
+and must be the equal to one used in the compilation flag. The default value is 4.
+
+To run this test with AGORA, compile with:
+    --with-hydro-dimension=2 --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-chemistry=AGORA
+NOTE: The AGORA scheme only traces Fe mass and total metal mass, so the number of elements in makeIC.py must be set
+to the value of 2.
+
+Due to the nature of this test, not much mass will be exchanged when running in Lagrangian mode,
+and hence not much advection will happen.
+To be able to see the effect of the advection, the hydrodynamics must be run in Eulerian mode.
+E.g. for gizmo-mvf: uncomment `#define GIZMO_FIX_PARTICLES` in src/const.h.
+
+Expected output when running in Eulerian mode is that the profiles should maintain their shape,
+but edges will be blurred due to numerical diffusion.
diff --git a/examples/Planetary/GreshoVortex_3D/gresho.yml b/examples/ChemistryTests/MetalAdvectionTestGEAR/advect_metals.yml
similarity index 52%
rename from examples/Planetary/GreshoVortex_3D/gresho.yml
rename to examples/ChemistryTests/MetalAdvectionTestGEAR/advect_metals.yml
index dac2f29b5b1e3bb5040de3215c30574930e7012b..de31d05b432d578bfa5bd8cc7c3ba5eca828a9e8 100644
--- a/examples/Planetary/GreshoVortex_3D/gresho.yml
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/advect_metals.yml
@@ -1,3 +1,6 @@
+MetaData:
+  run_name: advect_metals_GEAR
+
 # Define the system of units to use internally. 
 InternalUnitSystem:
   UnitMass_in_cgs:     1   # Grams
@@ -6,38 +9,38 @@ InternalUnitSystem:
   UnitCurrent_in_cgs:  1   # Amperes
   UnitTemp_in_cgs:     1   # Kelvin
 
-Scheduler:
-  max_top_level_cells: 15
-
 # Parameters governing the time integration
 TimeIntegration:
   time_begin: 0.    # The starting time of the simulation (in internal units).
-  time_end:   1.    # The end time of the simulation (in internal units).
-  dt_min:     1e-6  # The minimal time-step size of the simulation (in internal units).
-  dt_max:     1e-2  # The maximal time-step size of the simulation (in internal units).
+  time_end:   0.1   # The end time of the simulation (in internal units).
+  dt_min:     1e-9  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-1  # The maximal time-step size of the simulation (in internal units).
+
 
 # Parameters governing the snapshots
 Snapshots:
-  basename:            gresho # Common part of the name of output files
+  basename:            output # Common part of the name of output files
   time_first:          0.     # Time of the first output (in internal units)
-  delta_time:          1e-1   # Time difference between consecutive outputs (in internal units)
-  compression:         1
-  
+  delta_time:          0.1    # Time between snapshots
+
 # Parameters governing the conserved quantities statistics
 Statistics:
-  delta_time:          1e-2 # Time between statistics output
+  time_first:          0.
+  delta_time:          1e-1   # Time between statistics output
 
 # Parameters for the hydrodynamics scheme
 SPH:
   resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
   CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
-  viscosity_alpha:       0.8      # Override for the initial value of the artificial viscosity. In schemes that have a fixed AV, this remains as alpha throughout the run.
-  
+
 # Parameters related to the initial conditions
 InitialConditions:
-  file_name:  ./greshoVortex.hdf5     # The file to read
-  periodic:   1
+  file_name:  ./advect_metals.hdf5     # The file to read
+  periodic:   1                        # periodic ICs.
+
+GEARChemistry:
+  initial_metallicity: -1         # Negative values mean that the metallicity will be read from the ICs
 
-# Parameters related to the equation of state
-EoS:
-    planetary_use_idg_def:    1               # Default ideal gas, material ID 0
+AGORAChemistry:
+  initial_metallicity: -1         # Negative values mean that the metallicity will be read from the ICs
+  scale_initial_metallicity: 1    # scale the initial metallicity using solar_abundance_Metals? (needed to prevent crash in writing of metadata)
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/getGlass.sh b/examples/ChemistryTests/MetalAdvectionTestGEAR/getGlass.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a958311b6de07663c7f7c70cff9e95d86ce3efb2
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/getGlass.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/glassPlane_64.hdf5
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/makeIC.py b/examples/ChemistryTests/MetalAdvectionTestGEAR/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..de7f269096f73953321fb50a5c7bad8e103f234a
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/makeIC.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import math
+
+# ---------------------------------------------------------------------
+# Test the diffusion/advection of metals by creating regions with/without
+# initial metallicities. Run with EAGLE chemistry.
+# ---------------------------------------------------------------------
+
+import numpy as np
+import h5py
+
+GAMMA = 5 / 3
+RHO = 1
+P = 1
+VELOCITY = 2.5
+# Set ELEMENT_COUNT to 2 for AGORA runs, or for GEAR compile swift with `--with-chemistry=GEAR_X` with X equal to
+# ELEMENT_COUNT.
+ELEMENT_COUNT = 4
+
+outputfilename = "advect_metals.hdf5"
+
+
+def get_mask(boxsize, pos, element_idx):
+    x = boxsize[0]
+    right_half_mask = pos[:, 0] > x / 3
+
+    if element_idx == ELEMENT_COUNT - 1:
+        return right_half_mask
+    else:
+        periods = 2 * (element_idx + 2)
+        periods_mask = (pos[:, 0] * periods // x) % 2 == 0
+        return right_half_mask & periods_mask
+
+
+def get_abundance(element_idx):
+    if element_idx == ELEMENT_COUNT - 1:
+        return 0.2
+    else:
+        return 0.1 * 0.5 ** element_idx
+
+
+def get_element_abundances_metallicity(pos, boxsize):
+    element_abundances = []
+    for i in range(ELEMENT_COUNT):
+        element_abundances.append(
+            np.where(get_mask(boxsize, pos, i), get_abundance(i), 0)
+        )
+
+    return np.stack(element_abundances, axis=1)
+
+
+if __name__ == "__main__":
+    glass = h5py.File("glassPlane_64.hdf5", "r")
+    parts = glass["PartType0"]
+    pos = parts["Coordinates"][:]
+    pos = np.concatenate([pos, pos + np.array([1, 0, 0])])
+    h = parts["SmoothingLength"][:]
+    h = np.concatenate([h, h])
+    glass.close()
+
+    # Set up metadata
+    boxsize = np.array([2.0, 1.0, 1.0])
+    n_part = len(h)
+    ids = np.arange(n_part) + 1
+
+    # Setup other particle quantities
+    rho = RHO * np.ones_like(h)
+    rho[pos[:, 1] < 0.5 * boxsize[1]] *= 0.5
+    masses = rho * np.prod(boxsize) / n_part
+    velocities = np.zeros((n_part, 3))
+    velocities[:, :] = 0.5 * math.sqrt(2) * VELOCITY * np.array([1.0, 1.0, 0.0])
+    internal_energy = P / (rho * (GAMMA - 1))
+
+    # Setup metallicities
+    metallicities = get_element_abundances_metallicity(pos, boxsize)
+
+    # Now open the file and write the data.
+    file = h5py.File(outputfilename, "w")
+
+    # Header
+    grp = file.create_group("/Header")
+    grp.attrs["BoxSize"] = boxsize
+    grp.attrs["NumPart_Total"] = [n_part, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [n_part, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFilesPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = 0
+    grp.attrs["Dimension"] = 2
+
+    # Units
+    grp = file.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = file.create_group("/PartType0")
+    grp.create_dataset("Coordinates", data=pos, dtype="d")
+    grp.create_dataset("Velocities", data=velocities, dtype="f")
+    grp.create_dataset("Masses", data=masses, dtype="f")
+    grp.create_dataset("SmoothingLength", data=h, dtype="f")
+    grp.create_dataset("InternalEnergy", data=internal_energy, dtype="f")
+    grp.create_dataset("ParticleIDs", data=ids, dtype="L")
+    grp.create_dataset("MetalMassFraction", data=metallicities, dtype="f")
+
+    file.close()
+
+    # close up, and we're done!
+    file.close()
+
+    if ELEMENT_COUNT == 2:
+        print(
+            f"Make sure swift was compiled with `--with-chemistry=GEAR_2` or `--with-chemistry=AGORA`"
+        )
+    else:
+        print(
+            f"Make sure swift was compiled with `--with-chemistry=GEAR_{ELEMENT_COUNT}`"
+        )
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/plotAdvectedMetals.py b/examples/ChemistryTests/MetalAdvectionTestGEAR/plotAdvectedMetals.py
new file mode 100644
index 0000000000000000000000000000000000000000..b46029275d32acf059e9eca8212a392bd5eeae70
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/plotAdvectedMetals.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import math
+
+import matplotlib.pyplot as plt
+from matplotlib.cm import ScalarMappable
+from matplotlib.colors import SymLogNorm, Normalize
+import numpy as np
+import unyt
+
+import swiftsimio
+from swiftsimio.visualisation import project_gas
+from pathlib import Path
+
+from makeIC import VELOCITY, ELEMENT_COUNT
+
+
+def plot_single(ax_mass, ax_fraction, name, title, data, mass_map, kwargs_inner):
+    mass_weighted_map = project_gas(data, project=name, **kwargs_inner["projection"])
+    ax_mass.imshow(mass_weighted_map.in_cgs().value.T, **kwargs_inner["imshow_mass"])
+    ax_mass.set_title(title)
+    ax_fraction.imshow(
+        (mass_weighted_map / mass_map).value.T, **kwargs_inner["imshow_fraction"]
+    )
+    ax_fraction.set_title(title)
+    ax_mass.axis("off")
+    ax_fraction.axis("off")
+
+
+def plot_all(fname, savename):
+    data = swiftsimio.load(fname)
+
+    # Shift coordinates to starting position
+    velocity = 0.5 * math.sqrt(2) * VELOCITY * np.array([1, 1, 0]) * unyt.cm / unyt.s
+    data.gas.coordinates -= data.metadata.time * velocity
+
+    # Add mass weighted element mass fractions to gas dataset
+    masses = data.gas.masses
+    element_mass_fractions = data.gas.metal_mass_fractions
+    columns = getattr(element_mass_fractions, "named_columns", None)
+    for i in range(ELEMENT_COUNT):
+        if columns is None:
+            data.gas.__setattr__(
+                f"element_mass_sp{i}", element_mass_fractions[:, i] * masses
+            )
+        else:
+            data.gas.__setattr__(
+                f"element_mass_sp{i}",
+                getattr(element_mass_fractions, columns[i]) * masses,
+            )
+
+    # Create necessary figures and axes
+    fig = plt.figure(layout="constrained", figsize=(8, 2 * ELEMENT_COUNT + 1))
+    fig.suptitle(
+        f"Profiles shifted to starting position after t={data.metadata.time:.2f}",
+        fontsize=14,
+    )
+    fig_ratios, fig_masses = fig.subfigures(1, 2)
+    fig_ratios.suptitle("Mass ratio of elements")
+    fig_masses.suptitle("Surface density in elements")
+    axes_ratios = fig_ratios.subplots(ELEMENT_COUNT, 1, sharex=True, sharey=True)
+    axes_masses = fig_masses.subplots(ELEMENT_COUNT, 1, sharex=True, sharey=True)
+
+    # parameters for swiftsimio projections
+    projection_kwargs = {
+        "region": np.array([0, 2, 0, 1, 0, 1]) * unyt.cm,
+        "resolution": 500,
+        "parallel": True,
+    }
+    # Parameters for imshow
+    if ELEMENT_COUNT > 5:
+        thresh = 10 ** math.floor(math.log10(0.5 ** (ELEMENT_COUNT - 2)))
+        norm_ratios = SymLogNorm(vmin=0, vmax=0.21, linthresh=thresh, base=10)
+        norm_masses = SymLogNorm(vmin=0, vmax=0.25, linthresh=thresh, base=10)
+    else:
+        norm_ratios = Normalize(vmin=0, vmax=0.21)
+        norm_masses = Normalize(vmin=0, vmax=0.25)
+    imshow_fraction_kwargs = dict(norm=norm_ratios, cmap="rainbow")
+    imshow_mass_kwargs = dict(norm=norm_masses, cmap="turbo")
+
+    # Plot the quantities
+    mass_map = project_gas(data, project="masses", **projection_kwargs)
+
+    # Parameters for plotting:
+    plotting_kwargs = dict(
+        data=data,
+        mass_map=mass_map,
+        kwargs_inner=dict(
+            projection=projection_kwargs,
+            imshow_mass=imshow_mass_kwargs,
+            imshow_fraction=imshow_fraction_kwargs,
+        ),
+    )
+
+    if columns is None:
+        columns = [f"Species {i + 1}" for i in range(ELEMENT_COUNT)]
+    for i in range(ELEMENT_COUNT):
+        plot_single(
+            axes_masses[i],
+            axes_ratios[i],
+            f"element_mass_sp{i}",
+            columns[i],
+            **plotting_kwargs,
+        )
+
+    # Add Colorbars
+    cb_masses = fig_masses.colorbar(
+        ScalarMappable(**imshow_mass_kwargs),
+        orientation="horizontal",
+        shrink=0.75,
+        pad=0.01,
+        ax=axes_masses,
+    )
+    cb_ratios = fig_ratios.colorbar(
+        ScalarMappable(**imshow_fraction_kwargs),
+        orientation="horizontal",
+        shrink=0.75,
+        pad=0.01,
+        ax=axes_ratios,
+    )
+    cb_masses.ax.set_xlabel("Surface density (g/cm^2)")
+    cb_ratios.ax.set_xlabel("Mass ratio")
+
+    # Save output
+    fig.savefig(savename, dpi=300)
+
+
+if __name__ == "__main__":
+    cwd = Path(__file__).parent
+    print("Plotting metals for output_0000.hdf5...")
+    plot_all(cwd / "output_0000.hdf5", savename=cwd / "metal_advection_0000.png")
+    print("Plotting metals for output_0001.hdf5...")
+    plot_all(cwd / "output_0001.hdf5", savename=cwd / "metal_advection_0001.png")
+    print("Done!")
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/run.sh b/examples/ChemistryTests/MetalAdvectionTestGEAR/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..27a008f3d7bac0d0931372aeec34512af257616e
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/run.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -e glassPlane_64.hdf5 ]
+then
+    echo "Fetching initial glass file ..."
+    ./getGlass.sh
+fi
+
+# Always generate ICs in case ELEMENT_COUNT has been changed
+echo "Generating ICs"
+python3 makeIC.py
+
+# Run SWIFT (must be compiled with --with-chemistry=GEAR_X or --with-chemistry=AGORA)
+../../../swift \
+    --hydro --threads=4 advect_metals.yml 2>&1 | tee output.log
+
+python3 runSanityChecksAdvectedMetals.py
+python3 plotAdvectedMetals.py
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/runSanityChecksAdvectedMetals.py b/examples/ChemistryTests/MetalAdvectionTestGEAR/runSanityChecksAdvectedMetals.py
new file mode 100644
index 0000000000000000000000000000000000000000..d9c80acacff43921c0592f59c7d53565de864742
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/runSanityChecksAdvectedMetals.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from pathlib import Path
+
+import numpy as np
+import swiftsimio
+
+from makeIC import ELEMENT_COUNT
+
+
+def test(name):
+    def test_decorator(test_func):
+        def test_runner(*args, **kwargs):
+            try:
+                test_func(*args, **kwargs)
+            except Exception as e:
+                print(f"\tTest {name} failed!")
+                raise e
+            else:
+                print(f"\tTest {name} \u2705")
+
+        return test_runner
+
+    return test_decorator
+
+
+def approx_equal(a, b, threshold=0.001):
+    return 2 * abs(a - b) / (abs(a) + abs(b)) < threshold
+
+
+@test("total metal mass conservation")
+def test_total_metal_mass_conservation(data_start, data_end):
+    def metal_mass(data):
+        columns = getattr(data.gas.metal_mass_fractions, "named_columns", None)
+        if columns is not None:
+            return sum(
+                data.gas.masses * getattr(data.gas.metal_mass_fractions, c)
+                for c in columns
+            )
+        else:
+            return data.gas.masses.reshape(-1, 1) * data.gas.metal_mass_fractions
+
+    assert approx_equal(np.sum(metal_mass(data_start)), np.sum(metal_mass(data_end)))
+
+
+def element_mass(data, element_idx):
+    columns = getattr(data.gas.metal_mass_fractions, "named_columns", None)
+    if columns is not None:
+        return data.gas.masses * getattr(
+            data.gas.metal_mass_fractions, columns[element_idx]
+        )
+    else:
+        return data.gas.masses * data.gas.metal_mass_fractions[:, element_idx]
+
+
+@test("element-wise mass conservation")
+def test_element_wise_mass_conservation(data_start, data_end):
+    for i in range(ELEMENT_COUNT):
+        assert approx_equal(
+            np.sum(element_mass(data_start, i)), np.sum(element_mass(data_end, i))
+        )
+
+
+if __name__ == "__main__":
+    print("Running sanity checks...")
+
+    cwd = Path(__file__).parent
+    start = swiftsimio.load(cwd / "output_0000.hdf5")
+    end = swiftsimio.load(cwd / "output_0001.hdf5")
+
+    test_total_metal_mass_conservation(start, end)
+    test_element_wise_mass_conservation(start, end)
+
+    print("Done!")
diff --git a/examples/Cooling/CoolingBox/README b/examples/Cooling/CoolingBox/README
new file mode 100644
index 0000000000000000000000000000000000000000..49ab9afad7e5b893018c1d77b4690cd75ffcb16b
--- /dev/null
+++ b/examples/Cooling/CoolingBox/README
@@ -0,0 +1,14 @@
+Runs a uniform box exposed to constant cooling rates computed with 
+the Grackle library.
+
+To run with Grackle mode 0, configure swift with:
+  ./configure --with-cooling=grackle_0 --with-grackle=$GRACKLE_ROOT
+
+To run with Grackle mode 1, configure swift with:
+  ./configure --with-cooling=grackle_1 --with-grackle=$GRACKLE_ROOT
+  
+To run with Grackle mode 2, configure swift with:
+  ./configure --with-cooling=grackle_2 --with-grackle=$GRACKLE_ROOT
+  
+To run with Grackle mode 3, configure swift with:
+  ./configure --with-cooling=grackle_3 --with-grackle=$GRACKLE_ROOT    
diff --git a/examples/Cooling/CoolingBox/coolingBox.yml b/examples/Cooling/CoolingBox/coolingBox.yml
index f2bb1e75d706363b672a0683951545f14e2b8db0..dc92d6aedca0a670fa179e0d8fdf7eb3a2584c35 100644
--- a/examples/Cooling/CoolingBox/coolingBox.yml
+++ b/examples/Cooling/CoolingBox/coolingBox.yml
@@ -55,6 +55,7 @@ GrackleCooling:
   thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
   self_shielding_method: 0                    # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
   self_shielding_threshold_atom_per_cm3: 0.007 # Required only with GEAR's self shielding. Density threshold of the self shielding
+  maximal_density_Hpcm3: -1                    # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
 
 GEARChemistry:
   initial_metallicity: 0.01295
diff --git a/examples/Cooling/CoolingBox/run.sh b/examples/Cooling/CoolingBox/run.sh
index d83974dd34c71310d1eca7c7376de83fd20d3210..ed66cfe12d469e6f7b756b7df7bfaa3fbaf07932 100755
--- a/examples/Cooling/CoolingBox/run.sh
+++ b/examples/Cooling/CoolingBox/run.sh
@@ -17,7 +17,7 @@ fi
 if [ ! -e CloudyData_UVB=HM2012.h5 ]
 then
     echo "Fetching the Cloudy tables required by Grackle..."
-    ../getCoolingTable.sh
+    ../getGrackleCoolingTable.sh
 fi
 
 # Run SWIFT
diff --git a/examples/Cooling/CoolingHeatingBox/README b/examples/Cooling/CoolingHeatingBox/README
new file mode 100644
index 0000000000000000000000000000000000000000..999269bf9d7667f5e1b5d49debf0af227299c5c5
--- /dev/null
+++ b/examples/Cooling/CoolingHeatingBox/README
@@ -0,0 +1,7 @@
+Runs a uniform box exposed to constant heating and ionization rates.
+This tests is the Iliev+2006 (ui.adsabs.harvard.edu/abs/2006MNRAS.369.1625I/) 
+Test 0 part 3, however without stopping the heating and ionization rates
+after 0.5 Myr. While the cooling is present, gas heats up and ionize.
+
+To run with GEAR-RT, configure swift with:
+  ./configure --with-cooling=grackle_1 --with-grackle=$GRACKLE_ROOT
diff --git a/examples/Cooling/CoolingHeatingBox/coolingBox.yml b/examples/Cooling/CoolingHeatingBox/coolingBox.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0fda668e80147cc2485b298a05232b0bee303874
--- /dev/null
+++ b/examples/Cooling/CoolingHeatingBox/coolingBox.yml
@@ -0,0 +1,73 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841e43    # 1e10 Msol  #2.0e33     # Solar masses
+  UnitLength_in_cgs:   3.08567758e24 # Mpc        #3.0857e21  # Kiloparsecs
+  UnitVelocity_in_cgs: 1.0e5      # Kilometers per second
+  UnitCurrent_in_cgs:  1          # Amperes
+  UnitTemp_in_cgs:     1          # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   5e-6  # The end time of the simulation (in internal units).
+  dt_min:     1e-10 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-8  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            snap/snapshot # Common part of the name of output files
+  time_first:          0.         # Time of the first output (in internal units)
+  delta_time:          1e-7       # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1e-3 # Time between statistics output
+
+Scheduler:
+  tasks_per_cell: 64
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature: 100.       # Kelvin
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./coolingBox.hdf5     # The file to read
+  periodic:   1
+  
+# Dimensionless pre-factor for the time-step condition
+LambdaCooling:
+  lambda_nH2_cgs:              1e-22 # Cooling rate divided by square Hydrogen number density (in cgs units [erg * s^-1 * cm^3])
+  cooling_tstep_mult:          1.0        # Dimensionless pre-factor for the time-step condition
+
+# Cooling with Grackle 3.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5       # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 0                        # Enable or not the UV background
+  redshift: 0                                  # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 0                        # Enable or not the metal cooling
+  max_steps: 10000                             # (optional) Max number of step when computing the initial composition
+  convergence_limit: 1e-2                      # (optional) Convergence threshold (relative) for initial composition
+  thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
+  self_shielding_method: 0                     # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
+  self_shielding_threshold_atom_per_cm3: 0.007 # Required only with GEAR's self shielding. Density threshold of the self shielding
+  HydrogenFractionByMass : 1.                  # Hydrogen fraction by mass (default is 0.76)
+  use_radiative_transfer : 1                   # Arrays of ionization and heating rates are provided
+  RT_heating_rate_cgs        : 1.65117e-17     # heating         rate in units of / nHI_cgs (corresponding to a flux of 10^12 photons s−1 cm−2 , with a 10^5 K blackbody spectrum)
+  RT_HI_ionization_rate_cgs  : 1.63054e-06     # HI ionization   rate in cgs [1/s]          (corresponding to a flux of 10^12 photons s−1 cm−2 , with a 10^5 K blackbody spectrum)
+  RT_HeI_ionization_rate_cgs : 0               # HeI ionization  rate in cgs [1/s]
+  RT_HeII_ionization_rate_cgs: 0               # HeII ionization rate in cgs [1/s]
+  RT_H2_dissociation_rate_cgs: 0               # H2 dissociation rate in cgs [1/s]
+  maximal_density_Hpcm3:  -1                   # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+  
+
+GEARChemistry:
+  initial_metallicity: 0.01295
+
+
+
+GEARPressureFloor:
+  jeans_factor: 0.       # Number of particles required to suppose a resolved clump and avoid the pressure floor.
+
diff --git a/examples/Cooling/CoolingHeatingBox/getGlass.sh b/examples/Cooling/CoolingHeatingBox/getGlass.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ffd92e88deae6e91237059adac2a6c2067caee46
--- /dev/null
+++ b/examples/Cooling/CoolingHeatingBox/getGlass.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/glassCube_32.hdf5
diff --git a/examples/Cooling/CoolingHeatingBox/getResults.sh b/examples/Cooling/CoolingHeatingBox/getResults.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d178c5952f8f63c8d1fd28648e4508728a2ff567
--- /dev/null
+++ b/examples/Cooling/CoolingHeatingBox/getResults.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ReferenceSolutions/CoolingHeatingBox_results.txt
diff --git a/examples/Cooling/CoolingHeatingBox/makeIC.py b/examples/Cooling/CoolingHeatingBox/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..8746a96204a0145296b0a5d155b2fd81024cb56c
--- /dev/null
+++ b/examples/Cooling/CoolingHeatingBox/makeIC.py
@@ -0,0 +1,122 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Yves Revaz (yves.revaz@epfl.ch)
+#                    Loic Hausammann (loic.hausammann@id.ethz.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+
+# Generates a SWIFT IC file with a constant density and pressure
+
+# Parameters
+periodic = 1  # 1 For periodic box
+boxSize = 1  # 1 kiloparsec
+# rho = 3.2e3  # Density in code units (3.2e6 is 0.1 hydrogen atoms per cm^3)
+
+# Iliev test values
+rho = (
+    2471402.49842514
+)  # Density in code units (1-hydrogen atoms per cm^3 assuming Hydrogen only)
+T = 100  # Initial Temperature
+
+
+gamma = 5.0 / 3.0  # Gas adiabatic index
+fileName = "coolingBox.hdf5"
+# ---------------------------------------------------
+
+# defines some constants
+# need to be changed in plotResults.py too
+h_frac = 1.0  # 0.76
+mu = 4.0 / (1.0 + 3.0 * h_frac)
+
+m_h_cgs = 1.67262192369e-24
+k_b_cgs = 1.380649e-16
+
+# defines units
+unit_length = 3.08567758e24  # Mpc
+unit_mass = 1.98841e43  # 1e10 solar mass
+unit_velocity = 1e5  # 1e5 km/s
+unit_time = unit_length / unit_velocity
+
+# Read id, position and h from glass
+glass = h5py.File("glassCube_32.hdf5", "r")
+ids = glass["/PartType0/ParticleIDs"][:]
+pos = glass["/PartType0/Coordinates"][:, :] * boxSize
+h = glass["/PartType0/SmoothingLength"][:] * boxSize
+
+# Compute basic properties
+numPart = np.size(pos) // 3
+mass = boxSize ** 3 * rho / numPart
+internalEnergy = k_b_cgs * T * mu / ((gamma - 1.0) * m_h_cgs)
+internalEnergy *= (unit_time / unit_length) ** 2
+
+# File
+f = h5py.File(fileName, "w")
+
+# Header
+grp = f.create_group("/Header")
+grp.attrs["BoxSize"] = boxSize
+grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+grp.attrs["Time"] = 0.0
+grp.attrs["NumFilesPerSnapshot"] = 1
+grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+grp.attrs["Flag_Entropy_ICs"] = 0
+
+# Runtime parameters
+grp = f.create_group("/RuntimePars")
+grp.attrs["PeriodicBoundariesOn"] = periodic
+
+# Units
+grp = f.create_group("/Units")
+grp.attrs["Unit length in cgs (U_L)"] = unit_length
+grp.attrs["Unit mass in cgs (U_M)"] = unit_mass
+grp.attrs["Unit time in cgs (U_t)"] = unit_time
+grp.attrs["Unit current in cgs (U_I)"] = 1.0
+grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+# Particle group
+grp = f.create_group("/PartType0")
+
+v = np.zeros((numPart, 3))
+ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+ds[()] = v
+
+m = np.full((numPart, 1), mass)
+ds = grp.create_dataset("Masses", (numPart, 1), "f")
+ds[()] = m
+
+h = np.reshape(h, (numPart, 1))
+ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+ds[()] = h
+
+u = np.full((numPart, 1), internalEnergy)
+ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+ds[()] = u
+
+ids = np.reshape(ids, (numPart, 1))
+ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+ds[()] = ids
+
+ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+ds[()] = pos
+
+f.close()
+
+print("Initial condition generated")
diff --git a/examples/Cooling/CoolingHeatingBox/plotResults.py b/examples/Cooling/CoolingHeatingBox/plotResults.py
new file mode 100644
index 0000000000000000000000000000000000000000..2609ddf45f85c05dcf4447f0e6dd263cf4a21ba8
--- /dev/null
+++ b/examples/Cooling/CoolingHeatingBox/plotResults.py
@@ -0,0 +1,118 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Yves Revaz (yves.revaz@epfl.ch)
+#                    Loic Hausammann (loic.hausammann@id.ethz.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import matplotlib
+
+import matplotlib.pyplot as plt
+import numpy as np
+from glob import glob
+import h5py
+
+plt.style.use("../../../tools/stylesheets/mnras.mplstyle")
+
+
+##########################################
+# read specific energy from the solution
+##########################################
+
+# Read in units.
+resultfile = "CoolingHeatingBox_results.txt"
+
+f = open(resultfile, "r")
+firstline = f.readline()
+massline = f.readline()
+lengthline = f.readline()
+velline = f.readline()
+f.close()
+units = []
+for l in [massline, lengthline, velline]:
+    before, after = l.split("used:")
+    val, unit = after.split("[")
+    val = val.strip()
+    units.append(float(val))
+
+mass_units = units[0]
+length_units = units[1]
+velocity_units = units[2]
+time_units = velocity_units / length_units
+density_units = mass_units / length_units ** 3
+
+# Read in all other data
+data = np.loadtxt(resultfile)
+
+Time = data[:, 1]
+Time_Myr = Time * 1e-6
+Energy = data[:, 12]  # internal energy IU
+
+
+##########################################
+# compute specific energy from the models
+##########################################
+
+
+# First snapshot
+snap_filename = "snap/snapshot_0000.hdf5"
+
+# Read the initial state of the gas
+f = h5py.File(snap_filename, "r")
+
+# Read the units parameters from the snapshot
+units = f["InternalCodeUnits"]
+unit_mass = units.attrs["Unit mass in cgs (U_M)"]
+unit_length = units.attrs["Unit length in cgs (U_L)"]
+unit_time = units.attrs["Unit time in cgs (U_t)"]
+
+
+# Read snapshots
+files = glob("snap/snapshot_*.hdf5")
+files.sort()
+N = len(files)
+energy_snap = np.zeros(N)
+time_snap_cgs = np.zeros(N)
+for i in range(N):
+    snap = h5py.File(files[i], "r")
+    masses = snap["/PartType0/Masses"][:]
+    u = snap["/PartType0/InternalEnergies"][:] * masses
+    u = sum(u) / masses.sum()
+    energy_snap[i] = u
+    time_snap_cgs[i] = snap["/Header"].attrs["Time"] * unit_time
+
+
+##########################################
+# plot
+##########################################
+
+plt.figure()
+
+Myr_in_s = 1e6 * 365.25 * 24.0 * 60.0 * 60.0
+yr_in_s = 365.25 * 24.0 * 60.0 * 60.0
+
+plt.plot(Time_Myr, Energy, "r", ms=3, label="Expected solution")
+
+plt.plot(time_snap_cgs / Myr_in_s, energy_snap, "k", ms=2)
+
+
+plt.legend(loc="right", fontsize=8, frameon=False, handlelength=3, ncol=1)
+plt.xlabel("${\\rm{Time~[Myr]}}$", labelpad=0)
+plt.ylabel(r"$\rm{Internal Energy\,\,[IU]}$")
+
+
+plt.tight_layout()
+
+plt.show()
diff --git a/examples/Cooling/CoolingHeatingBox/run.sh b/examples/Cooling/CoolingHeatingBox/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..466c2281b987d5adf76555890383db3548ff1d14
--- /dev/null
+++ b/examples/Cooling/CoolingHeatingBox/run.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+# Generate the initial conditions if they are not present.
+if [ ! -e glassCube_32.hdf5 ]
+then
+    echo "Fetching initial glass file for the cooling box example..."
+    ./getGlass.sh
+fi
+if [ ! -e coolingBox.hdf5 ]
+then
+    echo "Generating initial conditions for the cooling box example..."
+    python3 makeIC.py
+fi
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ../getGrackleCoolingTable.sh
+fi
+
+# Get the results
+if [ ! -e CoolingHeatingBox_results.txt]
+then
+    echo "Fetching the the results..."
+    ./getResults.sh
+fi
+
+
+# Create output directory
+rm -rf snap
+mkdir snap
+
+# Run SWIFT
+../../../swift --hydro --cooling --threads=14  coolingBox.yml
+
+# Check energy conservation and cooling rate
+python3 plotResults.py
diff --git a/examples/Cooling/CoolingWithPrimordialElements/CoolingWithPrimordialElements.yml b/examples/Cooling/CoolingWithPrimordialElements/CoolingWithPrimordialElements.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5b421a12fae543b7be7a87155c75c4d96a3721aa
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/CoolingWithPrimordialElements.yml
@@ -0,0 +1,109 @@
+# Define some meta-data about the simulation
+MetaData:
+  run_name: CosmoBox00
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841e43    # 10^10 M_sun in grams
+  UnitLength_in_cgs:   3.08567758e24 # Mpc in centimeters
+  UnitVelocity_in_cgs: 1e5           # km/s in centimeters per second
+  UnitCurrent_in_cgs:  1             # Amperes
+  UnitTemp_in_cgs:     1             # Kelvin
+
+
+# Cosmological parameters
+Cosmology:
+  h:              0.67321       # Reduced qHubble constant
+  a_begin:        0.01282051    # Initial scale-factor of the simulation (z=77)
+  a_end:          1.0           # Final scale factor of the simulation
+  Omega_cdm:      0.26641139    # Cold Dark Matter density parameter  (value without neutrinos : 0.26499)
+  Omega_lambda:   0.6842        # Dark-energy density parameter
+  Omega_b:        0.049389      # Baryon density parameter 
+
+# Parameters governing the time integration
+TimeIntegration:
+  dt_min:     1e-10 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-2  # The maximal time-step size of the simulation (in internal units).
+  
+# Parameters governing the snapshots
+Snapshots:
+  basename:            snap/snapshot # Common part of the name of output files
+  output_list_on:      1
+  output_list:         ./output_list.txt
+  compression:         4
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:           1.01
+  scale_factor_first:   0.02
+
+# Parameters for the self-gravity scheme
+Gravity:
+  eta:                         0.025     # Constant dimensionless multiplier for time integration.
+  MAC:                         adaptive  # Use the geometric opening angle condition
+  theta_cr:                    0.7       # Opening angle (Multipole acceptance criterion)
+  epsilon_fmm:                 0.001     # Adaptive opening angle
+  mesh_side_length:            256
+  comoving_DM_softening:       0.0066   # Comoving softening for DM
+  max_physical_DM_softening:   0.00174  # Physical softening for DM
+  comoving_baryon_softening:       0.0066   # Comoving softening for DM
+  max_physical_baryon_softening:   0.00174  # Physical softening for DM
+    
+    
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:                    1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  h_min_ratio:                       0.01     # Minimal smoothing length in units of softening.
+  h_max:                             0.5      # Maximal smoothing length in co-moving internal units.
+  CFL_condition:                     0.2      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:               10.0      # (internal units)
+
+# Cooling with Grackle 3.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5       # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 0                        # Enable or not the UV background
+  redshift: -1                                 # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 0                        # Enable or not the metal cooling
+  max_steps: 10000                             # (optional) Max number of step when computing the initial composition
+  convergence_limit: 1e-2                      # (optional) Convergence threshold (relative) for initial composition
+  thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
+  self_shielding_method: 0                     # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
+  self_shielding_threshold_atom_per_cm3: 0.007 # Required only with GEAR's self shielding. Density threshold of the self shielding
+  HydrogenFractionByMass : 0.76                # Hydrogen fraction by mass (default is 0.76)
+  use_radiative_transfer : 0                   # Arrays of ionization and heating rates are provided
+  H2_three_body_rate : 1                       # (optional) H2 formation three body rate (see grackle documentation)
+  H2_cie_cooling : 0                           # Enable/disable H2 collision-induced emission cooling from Ripamonti & Abel (2004)
+  cmb_temperature_floor : 1                    # Enable/disable an effective CMB temperature floor
+  initial_nHII_to_nH_ratio:   2.393761E-004    # initial nHII   to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHeI_to_nH_ratio:   7.900000E-002    # initial nHeI   to nH ratio (number density ratio). Value is ignored if set to -1.  
+  initial_nHeII_to_nH_ratio:  5.692753E-020    # initial nHeII  to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHeIII_to_nH_ratio: 0                # initial nHeIII to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nDI_to_nH_ratio:    2.529538E-005    # initial nDI    to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nDII_to_nH_ratio:   4.284496E-009    # initial nDII   to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHM_to_nH_ratio:    6.509693E-012    # initial nHM    to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nH2I_to_nH_ratio:   1.891128E-006    # initial nH2I   to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nH2II_to_nH_ratio:  4.153181E-014    # initial nH2II  to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHDI_to_nH_ratio:   3.356904E-010    # initial nHDI   to nH ratio (number density ratio). Value is ignored if set to -1.
+  maximal_density_Hpcm3: -1                    # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+
+
+
+Scheduler:
+  max_top_level_cells:   12
+  #cell_split_size:       200
+  
+Restarts:
+  onexit:       1
+  delta_hours:  6.0
+  max_run_time: 71.5                 # Three days minus fergie time
+  #resubmit_on_exit:   1
+  #resubmit_command:   ./resub.sh
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  homogeneousCube_32.hdf5
+  periodic:   1
+  cleanup_h_factors: 0               # Remove the h-factors inherited from Gadget
+  cleanup_velocity_factors: 0        # Remove the sqrt(a) factor in the velocities inherited from Gadget
+  
diff --git a/examples/Cooling/CoolingWithPrimordialElements/README b/examples/Cooling/CoolingWithPrimordialElements/README
new file mode 100644
index 0000000000000000000000000000000000000000..248a64c11dc7df99263d677100d0502111c06512
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/README
@@ -0,0 +1,11 @@
+Runs a uniform cosmological box with imposed abundances for a set of
+elements entering the Grackle cooling. 
+The simulation is run  with the Grackle thermochemistry, down to redhift 0
+where the resulting abundances are compared with predictions.
+
+To run, configure swift with:
+  ./configure --with-cooling=grackle_3 --with-grackle=$GRACKLE_ROOT
+
+
+Note : the script getPrimordialAbundances.py helps in the setting of the
+initial abundances.
diff --git a/examples/Cooling/CoolingWithPrimordialElements/faure2024.dat b/examples/Cooling/CoolingWithPrimordialElements/faure2024.dat
new file mode 100644
index 0000000000000000000000000000000000000000..9e0427ad507dcaf4b3b2f63c3d894e3a65d81a6a
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/faure2024.dat
@@ -0,0 +1,193 @@
+! Fractional density (n(X)/nH)
+!             1              2              3              4              5              6              7              8              9             10             11             12             13             14             15             16             17             18             19             20             21             22             23             24
+       Redshift          ctime           Trad             Tn             nb             nH             e-              H             He              D             H2             HD             D2             H+             D+            He+            H2+            HD+            D2+           HeH+            H3+           H2D+           HD2+            D3+             H-
+  3.401000E+003  0.000000E+000  9.269426E+003  9.269426E+003  8.151816E+003  7.547801E+003  1.080025E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.000000E+000  2.530000E-005  8.000000E-002  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000
+  3.400876E+003  3.168879E+000  9.269089E+003  9.269088E+003  8.150927E+003  7.546978E+003  1.080024E+000  1.796857E-010  1.705857E-006  4.546047E-015  6.941758E-036  3.211260E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.554308E-028  0.000000E+000  0.000000E+000  1.268827E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.306460E-028
+  3.400867E+003  3.485767E+000  9.269064E+003  9.269063E+003  8.150861E+003  7.546917E+003  1.080024E+000  1.796933E-010  1.705992E-006  4.546240E-015  6.942382E-036  3.211549E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.554494E-028  0.000000E+000  0.000000E+000  1.268924E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.306636E-028
+  3.400858E+003  3.834344E+000  9.269039E+003  9.269038E+003  8.150795E+003  7.546856E+003  1.080024E+000  1.797009E-010  1.706126E-006  4.546433E-015  6.943006E-036  3.211838E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.554679E-028  0.000000E+000  0.000000E+000  1.269021E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.306812E-028
+  3.400842E+003  4.217778E+000  9.268995E+003  9.268994E+003  8.150680E+003  7.546749E+003  1.080024E+000  1.797142E-010  1.706362E-006  4.546770E-015  6.944097E-036  3.212344E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.555003E-028  0.000000E+000  0.000000E+000  1.269191E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.307120E-028
+  3.400825E+003  4.639556E+000  9.268948E+003  9.268947E+003  8.150556E+003  7.546635E+003  1.080024E+000  1.797285E-010  1.706613E-006  4.547131E-015  6.945265E-036  3.212885E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.555350E-028  0.000000E+000  0.000000E+000  1.269372E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.307450E-028
+  3.400808E+003  5.103511E+000  9.268902E+003  9.268901E+003  8.150433E+003  7.546521E+003  1.080024E+000  1.797427E-010  1.706865E-006  4.547491E-015  6.946434E-036  3.213426E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.555697E-028  0.000000E+000  0.000000E+000  1.269554E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.307780E-028
+  3.400790E+003  5.613863E+000  9.268855E+003  9.268854E+003  8.150310E+003  7.546406E+003  1.080024E+000  1.797570E-010  1.707117E-006  4.547852E-015  6.947602E-036  3.213968E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.556044E-028  0.000000E+000  0.000000E+000  1.269735E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.308109E-028
+  3.400771E+003  6.175249E+000  9.268802E+003  9.268801E+003  8.150170E+003  7.546277E+003  1.080024E+000  1.797732E-010  1.707403E-006  4.548261E-015  6.948928E-036  3.214582E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.556437E-028  0.000000E+000  0.000000E+000  1.269941E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.308483E-028
+  3.400745E+003  6.792774E+000  9.268730E+003  9.268729E+003  8.149981E+003  7.546102E+003  1.080024E+000  1.797950E-010  1.707789E-006  4.548813E-015  6.950717E-036  3.215410E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.556968E-028  0.000000E+000  0.000000E+000  1.270219E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.308988E-028
+  3.400723E+003  7.472051E+000  9.268670E+003  9.268669E+003  8.149822E+003  7.545955E+003  1.080024E+000  1.798133E-010  1.708113E-006  4.549277E-015  6.952221E-036  3.216107E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.557414E-028  0.000000E+000  0.000000E+000  1.270453E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.309412E-028
+  3.400696E+003  8.219256E+000  9.268598E+003  9.268597E+003  8.149633E+003  7.545779E+003  1.080024E+000  1.798353E-010  1.708500E-006  4.549832E-015  6.954020E-036  3.216940E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.557948E-028  0.000000E+000  0.000000E+000  1.270732E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.309919E-028
+  3.400664E+003  9.041182E+000  9.268511E+003  9.268510E+003  8.149402E+003  7.545566E+003  1.080024E+000  1.798620E-010  1.708972E-006  4.550508E-015  6.956208E-036  3.217954E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.558597E-028  0.000000E+000  0.000000E+000  1.271072E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.310537E-028
+  3.400633E+003  9.945300E+000  9.268424E+003  9.268423E+003  8.149174E+003  7.545355E+003  1.080024E+000  1.798883E-010  1.709437E-006  4.551174E-015  6.958368E-036  3.218954E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.559238E-028  0.000000E+000  0.000000E+000  1.271407E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.311145E-028
+  3.400592E+003  1.093983E+001  9.268314E+003  9.268313E+003  8.148882E+003  7.545084E+003  1.080024E+000  1.799221E-010  1.710035E-006  4.552029E-015  6.961143E-036  3.220240E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.560060E-028  0.000000E+000  0.000000E+000  1.271838E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.311927E-028
+  3.400549E+003  1.203381E+001  9.268196E+003  9.268195E+003  8.148573E+003  7.544798E+003  1.080024E+000  1.799579E-010  1.710668E-006  4.552934E-015  6.964078E-036  3.221599E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.560931E-028  0.000000E+000  0.000000E+000  1.272294E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.312755E-028
+  3.400507E+003  1.323719E+001  9.268081E+003  9.268080E+003  8.148268E+003  7.544516E+003  1.080024E+000  1.799931E-010  1.711291E-006  4.553826E-015  6.966971E-036  3.222939E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.561788E-028  0.000000E+000  0.000000E+000  1.272743E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.313570E-028
+  3.400462E+003  1.456091E+001  9.267959E+003  9.267958E+003  8.147946E+003  7.544218E+003  1.080024E+000  1.800305E-010  1.711952E-006  4.554771E-015  6.970036E-036  3.224359E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.562696E-028  0.000000E+000  0.000000E+000  1.273219E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.314433E-028
+  3.400409E+003  1.601701E+001  9.267814E+003  9.267813E+003  8.147565E+003  7.543865E+003  1.080024E+000  1.800746E-010  1.712733E-006  4.555888E-015  6.973662E-036  3.226039E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.563771E-028  0.000000E+000  0.000000E+000  1.273782E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.315454E-028
+  3.400349E+003  1.761871E+001  9.267652E+003  9.267651E+003  8.147137E+003  7.543469E+003  1.080024E+000  1.801242E-010  1.713610E-006  4.557142E-015  6.977733E-036  3.227925E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.564977E-028  0.000000E+000  0.000000E+000  1.274414E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.316600E-028
+  3.400284E+003  1.938058E+001  9.267475E+003  9.267474E+003  8.146670E+003  7.543036E+003  1.080024E+000  1.801783E-010  1.714568E-006  4.558511E-015  6.982181E-036  3.229985E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999829E-002  4.566294E-028  0.000000E+000  0.000000E+000  1.275105E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.317852E-028
+  3.400213E+003  2.131863E+001  9.267281E+003  9.267280E+003  8.146160E+003  7.542564E+003  1.080024E+000  1.802375E-010  1.715616E-006  4.560008E-015  6.987044E-036  3.232237E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999828E-002  4.567733E-028  0.000000E+000  0.000000E+000  1.275859E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.319220E-028
+  3.400135E+003  2.345050E+001  9.267068E+003  9.267067E+003  8.145596E+003  7.542042E+003  1.080024E+000  1.803029E-010  1.716774E-006  4.561662E-015  6.992421E-036  3.234728E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999828E-002  4.569323E-028  0.000000E+000  0.000000E+000  1.276694E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.320732E-028
+  3.400048E+003  2.579555E+001  9.266831E+003  9.266830E+003  8.144972E+003  7.541464E+003  1.080024E+000  1.803753E-010  1.718057E-006  4.563495E-015  6.998379E-036  3.237488E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999828E-002  4.571085E-028  0.000000E+000  0.000000E+000  1.277618E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.322407E-028
+  3.399950E+003  2.837510E+001  9.266563E+003  9.266562E+003  8.144265E+003  7.540809E+003  1.080024E+000  1.804574E-010  1.719512E-006  4.565571E-015  7.005134E-036  3.240617E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999828E-002  4.573082E-028  0.000000E+000  0.000000E+000  1.278666E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.324304E-028
+  3.399845E+003  3.121261E+001  9.266278E+003  9.266277E+003  8.143515E+003  7.540115E+003  1.080024E+000  1.805445E-010  1.721057E-006  4.567777E-015  7.012313E-036  3.243942E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999828E-002  4.575203E-028  0.000000E+000  0.000000E+000  1.279780E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.326320E-028
+  3.399730E+003  3.433387E+001  9.265964E+003  9.265964E+003  8.142687E+003  7.539349E+003  1.080024E+000  1.806407E-010  1.722763E-006  4.570210E-015  7.020235E-036  3.247612E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999828E-002  4.577542E-028  0.000000E+000  0.000000E+000  1.281009E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.328543E-028
+  3.399586E+003  3.776726E+001  9.265571E+003  9.265570E+003  8.141651E+003  7.538389E+003  1.080024E+000  1.807613E-010  1.724903E-006  4.573260E-015  7.030175E-036  3.252216E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999828E-002  4.580475E-028  0.000000E+000  0.000000E+000  1.282550E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.331331E-028
+  3.399460E+003  4.154399E+001  9.265230E+003  9.265229E+003  8.140750E+003  7.537555E+003  1.080024E+000  1.808661E-010  1.726765E-006  4.575913E-015  7.038829E-036  3.256225E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999827E-002  4.583026E-028  0.000000E+000  0.000000E+000  1.283892E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.333756E-028
+  3.399310E+003  4.569838E+001  9.264820E+003  9.264819E+003  8.139670E+003  7.536555E+003  1.080024E+000  1.809921E-010  1.729002E-006  4.579100E-015  7.049228E-036  3.261041E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999827E-002  4.586090E-028  0.000000E+000  0.000000E+000  1.285504E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.336668E-028
+  3.399134E+003  5.026822E+001  9.264341E+003  9.264340E+003  8.138409E+003  7.535387E+003  1.080024E+000  1.811391E-010  1.731616E-006  4.582820E-015  7.061378E-036  3.266670E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999827E-002  4.589667E-028  0.000000E+000  0.000000E+000  1.287387E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.340067E-028
+  3.398944E+003  5.529505E+001  9.263823E+003  9.263823E+003  8.137044E+003  7.534124E+003  1.080024E+000  1.812985E-010  1.734451E-006  4.586851E-015  7.074558E-036  3.272775E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999827E-002  4.593543E-028  0.000000E+000  0.000000E+000  1.289429E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.343751E-028
+  3.398754E+003  6.082455E+001  9.263304E+003  9.263303E+003  8.135675E+003  7.532856E+003  1.080024E+000  1.814586E-010  1.737302E-006  4.590902E-015  7.087816E-036  3.278916E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999826E-002  4.597437E-028  0.000000E+000  0.000000E+000  1.291482E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.347453E-028
+  3.398528E+003  6.690701E+001  9.262688E+003  9.262687E+003  8.134052E+003  7.531353E+003  1.080024E+000  1.816485E-010  1.740686E-006  4.595707E-015  7.103558E-036  3.286208E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999826E-002  4.602057E-028  0.000000E+000  0.000000E+000  1.293920E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.351844E-028
+  3.398284E+003  7.359771E+001  9.262023E+003  9.262022E+003  8.132301E+003  7.529732E+003  1.080024E+000  1.818536E-010  1.744345E-006  4.600897E-015  7.120582E-036  3.294094E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999826E-002  4.607047E-028  0.000000E+000  0.000000E+000  1.296555E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.356586E-028
+  3.398001E+003  8.095748E+001  9.261253E+003  9.261252E+003  8.130273E+003  7.527854E+003  1.080024E+000  1.820918E-010  1.748597E-006  4.606922E-015  7.140372E-036  3.303261E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999825E-002  4.612839E-028  0.000000E+000  0.000000E+000  1.299617E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.362091E-028
+  3.397713E+003  8.905322E+001  9.260468E+003  9.260467E+003  8.128205E+003  7.525940E+003  1.080024E+000  1.823348E-010  1.752942E-006  4.613072E-015  7.160601E-036  3.312632E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999825E-002  4.618750E-028  0.000000E+000  0.000000E+000  1.302746E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.367709E-028
+  3.397387E+003  9.795855E+001  9.259577E+003  9.259577E+003  8.125861E+003  7.523769E+003  1.080024E+000  1.826110E-010  1.757883E-006  4.620058E-015  7.183620E-036  3.323295E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999824E-002  4.625466E-028  0.000000E+000  0.000000E+000  1.306305E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.374092E-028
+  3.397027E+003  1.077544E+002  9.258597E+003  9.258596E+003  8.123279E+003  7.521379E+003  1.080024E+000  1.829156E-010  1.763343E-006  4.627766E-015  7.209061E-036  3.335080E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999824E-002  4.632876E-028  0.000000E+000  0.000000E+000  1.310236E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.381134E-028
+  3.396623E+003  1.185298E+002  9.257497E+003  9.257496E+003  8.120386E+003  7.518699E+003  1.080024E+000  1.832579E-010  1.769487E-006  4.636426E-015  7.237702E-036  3.348348E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999823E-002  4.641200E-028  0.000000E+000  0.000000E+000  1.314659E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.389045E-028
+  3.396173E+003  1.303828E+002  9.256269E+003  9.256268E+003  8.117154E+003  7.515708E+003  1.080024E+000  1.836411E-010  1.776375E-006  4.646119E-015  7.269834E-036  3.363232E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999822E-002  4.650517E-028  0.000000E+000  0.000000E+000  1.319618E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.397900E-028
+  3.395667E+003  1.434211E+002  9.254892E+003  9.254891E+003  8.113531E+003  7.512353E+003  1.080024E+000  1.840719E-010  1.784135E-006  4.657019E-015  7.306053E-036  3.380010E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999822E-002  4.660994E-028  0.000000E+000  0.000000E+000  1.325204E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.407856E-028
+  3.395176E+003  1.577632E+002  9.253553E+003  9.253552E+003  8.110012E+003  7.509094E+003  1.080024E+000  1.844916E-010  1.791711E-006  4.667638E-015  7.341432E-036  3.396399E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999821E-002  4.671200E-028  0.000000E+000  0.000000E+000  1.330656E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.417556E-028
+  3.394607E+003  1.735395E+002  9.252001E+003  9.252000E+003  8.105931E+003  7.505316E+003  1.080023E+000  1.849797E-010  1.800539E-006  4.679987E-015  7.382689E-036  3.415511E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999820E-002  4.683068E-028  0.000000E+000  0.000000E+000  1.337010E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.428834E-028
+  3.393944E+003  1.908935E+002  9.250193E+003  9.250193E+003  8.101181E+003  7.500918E+003  1.080023E+000  1.855501E-010  1.810880E-006  4.694416E-015  7.431051E-036  3.437914E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999819E-002  4.696934E-028  0.000000E+000  0.000000E+000  1.344450E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.442011E-028
+  3.393270E+003  2.099828E+002  9.248357E+003  9.248356E+003  8.096357E+003  7.496451E+003  1.080023E+000  1.861315E-010  1.821450E-006  4.709126E-015  7.480525E-036  3.460833E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999818E-002  4.711069E-028  0.000000E+000  0.000000E+000  1.352055E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.455444E-028
+  3.392492E+003  2.309811E+002  9.246238E+003  9.246237E+003  8.090794E+003  7.491300E+003  1.080023E+000  1.868049E-010  1.833727E-006  4.726163E-015  7.538043E-036  3.487479E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999817E-002  4.727439E-028  0.000000E+000  0.000000E+000  1.360886E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.470999E-028
+  3.391652E+003  2.540792E+002  9.243949E+003  9.243948E+003  8.084785E+003  7.485737E+003  1.080023E+000  1.875355E-010  1.847092E-006  4.744649E-015  7.600719E-036  3.516514E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999815E-002  4.745199E-028  0.000000E+000  0.000000E+000  1.370497E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.487876E-028
+  3.390717E+003  2.794872E+002  9.241399E+003  9.241398E+003  8.078097E+003  7.479545E+003  1.080023E+000  1.883531E-010  1.862099E-006  4.765334E-015  7.671178E-036  3.549155E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999814E-002  4.765071E-028  0.000000E+000  0.000000E+000  1.381288E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.506759E-028
+  3.389698E+003  3.074359E+002  9.238623E+003  9.238622E+003  8.070818E+003  7.472805E+003  1.080023E+000  1.892481E-010  1.878592E-006  4.787977E-015  7.748699E-036  3.585068E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999812E-002  4.786822E-028  0.000000E+000  0.000000E+000  1.393144E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.527426E-028
+  3.388580E+003  3.381795E+002  9.235576E+003  9.235575E+003  8.062837E+003  7.465415E+003  1.080023E+000  1.902355E-010  1.896865E-006  4.812958E-015  7.834710E-036  3.624915E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999810E-002  4.810816E-028  0.000000E+000  0.000000E+000  1.406277E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.550225E-028
+  3.387340E+003  3.719974E+002  9.232196E+003  9.232195E+003  8.053987E+003  7.457221E+003  1.080023E+000  1.913380E-010  1.917366E-006  4.840852E-015  7.931343E-036  3.669684E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999808E-002  4.837605E-028  0.000000E+000  0.000000E+000  1.421006E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.575677E-028
+  3.385987E+003  4.091972E+002  9.228508E+003  9.228507E+003  8.044338E+003  7.448286E+003  1.080023E+000  1.925493E-010  1.940005E-006  4.871498E-015  8.038232E-036  3.719204E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999806E-002  4.867032E-028  0.000000E+000  0.000000E+000  1.437268E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.603635E-028
+  3.384476E+003  4.501169E+002  9.224391E+003  9.224390E+003  8.033576E+003  7.438322E+003  1.080023E+000  1.939116E-010  1.965613E-006  4.905963E-015  8.159354E-036  3.775320E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999803E-002  4.900123E-028  0.000000E+000  0.000000E+000  1.455655E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.635073E-028
+  3.382854E+003  4.951286E+002  9.219968E+003  9.219967E+003  8.022028E+003  7.427629E+003  1.080023E+000  1.953871E-010  1.993524E-006  4.943293E-015  8.291628E-036  3.836604E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999801E-002  4.935959E-028  0.000000E+000  0.000000E+000  1.475689E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.669116E-028
+  3.381056E+003  5.446414E+002  9.215068E+003  9.215067E+003  8.009243E+003  7.415792E+003  1.080023E+000  1.970369E-010  2.024948E-006  4.985034E-015  8.440868E-036  3.905750E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999798E-002  4.976022E-028  0.000000E+000  0.000000E+000  1.498236E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.707174E-028
+  3.379078E+003  5.991056E+002  9.209678E+003  9.209677E+003  7.995198E+003  7.402788E+003  1.080023E+000  1.988697E-010  2.060123E-006  5.031404E-015  8.608317E-036  3.983334E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999794E-002  5.020520E-028  0.000000E+000  0.000000E+000  1.523463E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.749441E-028
+  3.376897E+003  6.590161E+002  9.203734E+003  9.203733E+003  7.979728E+003  7.388464E+003  1.080023E+000  2.009134E-010  2.099674E-006  5.083110E-015  8.797093E-036  4.070802E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999790E-002  5.070128E-028  0.000000E+000  0.000000E+000  1.551814E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.796559E-028
+  3.374528E+003  7.249177E+002  9.197276E+003  9.197275E+003  7.962943E+003  7.372922E+003  1.080023E+000  2.031608E-010  2.143566E-006  5.139968E-015  9.007193E-036  4.168153E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999786E-002  5.124668E-028  0.000000E+000  0.000000E+000  1.583262E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.848357E-028
+  3.371891E+003  7.974095E+002  9.190088E+003  9.190087E+003  7.944286E+003  7.355648E+003  1.080023E+000  2.056959E-010  2.193581E-006  5.204107E-015  9.247356E-036  4.279438E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999781E-002  5.186176E-028  0.000000E+000  0.000000E+000  1.619075E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.906768E-028
+  3.369041E+003  8.771505E+002  9.182322E+003  9.182321E+003  7.924164E+003  7.337017E+003  1.080023E+000  2.084750E-010  2.249020E-006  5.274418E-015  9.514490E-036  4.403225E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999775E-002  5.253586E-028  0.000000E+000  0.000000E+000  1.658748E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.970777E-028
+  3.365875E+003  9.648655E+002  9.173692E+003  9.173692E+003  7.901843E+003  7.316350E+003  1.080023E+000  2.116131E-010  2.312387E-006  5.353812E-015  9.820992E-036  4.545260E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999769E-002  5.329682E-028  0.000000E+000  0.000000E+000  1.704062E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.043027E-028
+  3.362424E+003  1.061352E+003  9.164287E+003  9.164286E+003  7.877564E+003  7.293869E+003  1.080023E+000  2.150944E-010  2.383633E-006  5.441889E-015  1.016705E-035  4.705634E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999762E-002  5.414074E-028  0.000000E+000  0.000000E+000  1.754972E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.123143E-028
+  3.358634E+003  1.167487E+003  9.153957E+003  9.153956E+003  7.850956E+003  7.269233E+003  1.080023E+000  2.189926E-010  2.464592E-006  5.540512E-015  1.056212E-035  4.888726E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999754E-002  5.508539E-028  0.000000E+000  0.000000E+000  1.812776E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.212810E-028
+  3.354466E+003  1.284236E+003  9.142597E+003  9.142596E+003  7.821763E+003  7.242203E+003  1.080023E+000  2.233721E-010  2.557032E-006  5.651314E-015  1.101551E-035  5.098861E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999744E-002  5.614630E-028  0.000000E+000  0.000000E+000  1.878717E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.313496E-028
+  3.349924E+003  1.412660E+003  9.130217E+003  9.130217E+003  7.790032E+003  7.212824E+003  1.080023E+000  2.282579E-010  2.662010E-006  5.774925E-015  1.153328E-035  5.338847E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999734E-002  5.732940E-028  0.000000E+000  0.000000E+000  1.953527E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.425759E-028
+  3.344917E+003  1.553926E+003  9.116573E+003  9.116572E+003  7.755159E+003  7.180534E+003  1.080023E+000  2.337833E-010  2.783072E-006  5.914719E-015  1.213407E-035  5.617330E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999722E-002  5.866680E-028  0.000000E+000  0.000000E+000  2.039708E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.552639E-028
+  3.339472E+003  1.709318E+003  9.101731E+003  9.101730E+003  7.717344E+003  7.145522E+003  1.080022E+000  2.399658E-010  2.921468E-006  6.071136E-015  1.282554E-035  5.937868E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999708E-002  6.016254E-028  0.000000E+000  0.000000E+000  2.138110E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.694510E-028
+  3.333475E+003  1.880250E+003  9.085387E+003  9.085386E+003  7.675845E+003  7.107098E+003  1.080022E+000  2.469888E-010  3.082426E-006  6.248817E-015  1.363574E-035  6.313475E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999692E-002  6.186076E-028  0.000000E+000  0.000000E+000  2.252409E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.855547E-028
+  3.326913E+003  2.068275E+003  9.067501E+003  9.067500E+003  7.630600E+003  7.065205E+003  1.080022E+000  2.549421E-010  3.269501E-006  6.450035E-015  1.458517E-035  6.753661E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999673E-002  6.378291E-028  0.000000E+000  0.000000E+000  2.385065E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.037768E-028
+  3.319747E+003  2.275102E+003  9.047971E+003  9.047970E+003  7.581401E+003  7.019652E+003  1.080022E+000  2.639580E-010  3.487699E-006  6.678138E-015  1.570258E-035  7.271781E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999651E-002  6.596062E-028  0.000000E+000  0.000000E+000  2.539556E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.244151E-028
+  3.311914E+003  2.502613E+003  9.026622E+003  9.026621E+003  7.527862E+003  6.970079E+003  1.080022E+000  2.742283E-010  3.744148E-006  6.937975E-015  1.702897E-035  7.886861E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999626E-002  6.843977E-028  0.000000E+000  0.000000E+000  2.720826E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.479017E-028
+  3.303385E+003  2.752874E+003  9.003377E+003  9.003376E+003  7.469855E+003  6.916370E+003  1.080021E+000  2.859268E-010  4.046446E-006  7.233948E-015  1.860963E-035  8.619929E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999595E-002  7.126185E-028  0.000000E+000  0.000000E+000  2.934120E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.746263E-028
+  3.294034E+003  3.028161E+003  8.977890E+003  8.977889E+003  7.406596E+003  6.857799E+003  1.080021E+000  2.994054E-010  4.408109E-006  7.574957E-015  2.052350E-035  9.507644E-040  0.000000E+000  1.000000E+000  2.530000E-005  7.999559E-002  7.451109E-028  0.000000E+000  0.000000E+000  3.188798E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.053820E-028
+  3.283807E+003  3.330977E+003  8.950017E+003  8.950016E+003  7.337827E+003  6.794126E+003  1.080020E+000  3.149724E-010  4.843492E-006  7.968801E-015  2.285819E-035  1.059069E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.999516E-002  7.826100E-028  0.000000E+000  0.000000E+000  3.494735E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.408579E-028
+  3.272705E+003  3.664075E+003  8.919758E+003  8.919757E+003  7.263652E+003  6.725446E+003  1.080020E+000  3.329145E-010  5.368646E-006  8.422737E-015  2.571554E-035  1.191639E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.999463E-002  8.257973E-028  0.000000E+000  0.000000E+000  3.862900E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.816908E-028
+  3.260624E+003  4.030483E+003  8.886831E+003  8.886830E+003  7.183508E+003  6.651241E+003  1.080019E+000  3.537590E-010  6.009879E-006  8.950102E-015  2.926061E-035  1.356143E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.999399E-002  8.759305E-028  0.000000E+000  0.000000E+000  4.311323E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  8.290586E-028
+  3.247426E+003  4.433531E+003  8.850859E+003  8.850858E+003  7.096629E+003  6.570799E+003  1.080018E+000  3.782288E-010  6.804944E-006  9.569189E-015  3.373417E-035  1.563769E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.999320E-002  9.347344E-028  0.000000E+000  0.000000E+000  4.865826E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  8.845752E-028
+  3.233108E+003  4.876884E+003  8.811836E+003  8.811835E+003  7.003178E+003  6.484272E+003  1.080018E+000  4.069526E-010  7.795943E-006  1.029590E-014  3.941925E-035  1.827675E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.999220E-002  1.003703E-027  0.000000E+000  0.000000E+000  5.554975E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  9.496292E-028
+  3.217536E+003  5.364572E+003  8.769394E+003  8.769393E+003  6.902471E+003  6.391027E+003  1.080016E+000  4.410151E-010  9.051038E-006  1.115768E-014  4.677445E-035  2.169181E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.999095E-002  1.085419E-027  0.000000E+000  0.000000E+000  6.425057E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.026629E-027
+  3.200676E+003  5.901030E+003  8.723442E+003  8.723441E+003  6.794530E+003  6.291084E+003  1.080015E+000  4.815571E-010  1.065653E-005  1.218339E-014  5.640617E-035  2.616493E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.998934E-002  1.182598E-027  0.000000E+000  0.000000E+000  7.534329E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.118089E-027
+  3.182373E+003  6.491133E+003  8.673558E+003  8.673557E+003  6.678635E+003  6.183776E+003  1.080013E+000  5.303788E-010  1.274882E-005  1.341859E-014  6.928636E-035  3.214819E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.998725E-002  1.299526E-027  0.000000E+000  0.000000E+000  8.974783E-024  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.227986E-027
+  3.162576E+003  7.140246E+003  8.619600E+003  8.619599E+003  6.554767E+003  6.069087E+003  1.080010E+000  5.895455E-010  1.551381E-005  1.491550E-014  8.679808E-035  4.028523E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.998449E-002  1.441119E-027  0.000000E+000  0.000000E+000  1.087111E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.360854E-027
+  3.141198E+003  7.854270E+003  8.561335E+003  8.561334E+003  6.422741E+003  5.946843E+003  1.080006E+000  6.618974E-010  1.923109E-005  1.674600E-014  1.110883E-034  5.157545E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.998077E-002  1.614142E-027  0.000000E+000  0.000000E+000  1.341024E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.522920E-027
+  3.118088E+003  8.639697E+003  8.498350E+003  8.498349E+003  6.282027E+003  5.816555E+003  1.080001E+000  7.515217E-010  2.433956E-005  1.901350E-014  1.456383E-034  6.763981E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.997566E-002  1.828341E-027  0.000000E+000  0.000000E+000  1.688471E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.723130E-027
+  3.093267E+003  9.503667E+003  8.430700E+003  8.430699E+003  6.133196E+003  5.678752E+003  1.079994E+000  8.632373E-010  3.147266E-005  2.183991E-014  1.957434E-034  9.094524E-039  0.000000E+000  1.000000E+000  2.530000E-005  7.996853E-002  2.095214E-027  0.000000E+000  0.000000E+000  2.171437E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.971966E-027
+  3.066538E+003  1.045403E+004  8.357849E+003  8.357848E+003  5.975571E+003  5.532806E+003  1.079984E+000  1.004794E-009  4.170518E-005  2.542129E-014  2.706796E-034  1.258144E-038  0.000000E+000  1.000000E+000  2.530000E-005  7.995829E-002  2.433284E-027  0.000000E+000  0.000000E+000  2.860991E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.286296E-027
+  3.037873E+003  1.149944E+004  8.279723E+003  8.279722E+003  5.809561E+003  5.379097E+003  1.079969E+000  1.186138E-009  5.671800E-005  3.000929E-014  3.857919E-034  1.794016E-038  0.000000E+000  1.000000E+000  2.530000E-005  7.994328E-002  2.866375E-027  0.000000E+000  0.000000E+000  3.867710E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.687652E-027
+  3.006971E+003  1.264938E+004  8.195500E+003  8.195499E+003  5.634070E+003  5.216609E+003  1.079946E+000  1.423677E-009  7.953418E-005  3.601904E-014  5.698576E-034  2.651294E-038  0.000000E+000  1.000000E+000  2.530000E-005  7.992047E-002  3.433866E-027  0.000000E+000  0.000000E+000  5.389900E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.211546E-027
+  2.974293E+003  1.391432E+004  8.106437E+003  8.106435E+003  5.452377E+003  5.048379E+003  1.079911E+000  1.734197E-009  1.145930E-004  4.387519E-014  8.689447E-034  4.045009E-038  0.000000E+000  1.000000E+000  2.530000E-005  7.988541E-002  4.176272E-027  0.000000E+000  0.000000E+000  7.716660E-023  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.893875E-027
+  2.939323E+003  1.530575E+004  8.011125E+003  8.011124E+003  5.262309E+003  4.872394E+003  1.079854E+000  2.152673E-009  1.709143E-004  5.446265E-014  1.379965E-033  6.427667E-038  0.000000E+000  1.000000E+000  2.530000E-005  7.982909E-002  5.178073E-027  0.000000E+000  0.000000E+000  1.143478E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.809855E-027
+  2.902049E+003  1.683633E+004  7.909535E+003  7.909534E+003  5.064642E+003  4.689374E+003  1.079761E+000  2.726529E-009  2.644566E-004  6.898120E-014  2.288874E-033  1.066816E-037  0.000000E+000  1.000000E+000  2.530000E-005  7.973554E-002  6.554510E-027  0.000000E+000  0.000000E+000  1.757715E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.060762E-027
+  2.862514E+003  1.851996E+004  7.801783E+003  7.801782E+003  4.860461E+003  4.500322E+003  1.079600E+000  3.527527E-009  4.252025E-004  8.924646E-014  3.974719E-033  1.853877E-037  0.000000E+000  1.000000E+000  2.530000E-005  7.957480E-002  8.481044E-027  0.000000E+000  0.000000E+000  2.807653E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.799136E-027
+  2.820812E+003  2.037195E+004  7.688123E+003  7.688122E+003  4.651112E+003  4.306484E+003  1.079314E+000  4.665929E-009  7.110243E-004  1.180481E-013  7.240458E-033  3.379666E-037  0.000000E+000  1.000000E+000  2.530000E-005  7.928898E-002  1.122926E-026  0.000000E+000  0.000000E+000  4.664929E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.025806E-026
+  2.776511E+003  2.240915E+004  7.567380E+003  7.567379E+003  4.435397E+003  4.106753E+003  1.078780E+000  6.339030E-009  1.245274E-003  1.603776E-013  1.397458E-032  6.528472E-037  0.000000E+000  1.000000E+000  2.530000E-005  7.875473E-002  1.528807E-026  0.000000E+000  0.000000E+000  8.119388E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.385278E-026
+  2.730003E+003  2.465007E+004  7.440623E+003  7.440622E+003  4.216225E+003  3.903821E+003  1.077752E+000  8.838186E-009  2.273604E-003  2.236063E-013  2.852758E-032  1.333932E-036  0.000000E+000  1.000000E+000  2.530000E-005  7.772640E-002  2.138915E-026  0.000000E+000  0.000000E+000  1.473816E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.918858E-026
+  2.681693E+003  2.711507E+004  7.308954E+003  7.308953E+003  3.996332E+003  3.700221E+003  1.075736E+000  1.263021E-008  4.289434E-003  3.195446E-013  6.142425E-032  2.874977E-036  0.000000E+000  1.000000E+000  2.530000E-005  7.571057E-002  3.072010E-026  0.000000E+000  0.000000E+000  2.766039E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.721933E-026
+  2.630516E+003  2.982658E+004  7.169472E+003  7.169471E+003  3.771875E+003  3.492395E+003  1.071645E+000  1.867746E-008  8.380230E-003  4.725407E-013  1.423594E-031  6.670353E-036  0.000000E+000  1.000000E+000  2.530000E-005  7.161977E-002  4.574927E-026  0.000000E+000  0.000000E+000  5.379452E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.987738E-026
+  2.577675E+003  3.280924E+004  7.025454E+003  7.025453E+003  3.549107E+003  3.286133E+003  1.063811E+000  2.835652E-008  1.621403E-002  7.174219E-013  3.489977E-031  1.637148E-035  0.000000E+000  1.000000E+000  2.530000E-005  6.378597E-002  7.010289E-026  0.000000E+000  0.000000E+000  1.037132E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.978013E-026
+  2.522514E+003  3.609016E+004  6.875111E+003  6.875110E+003  3.326098E+003  3.079648E+003  1.050384E+000  4.449115E-008  2.964092E-002  1.125631E-012  9.174778E-031  4.309316E-035  0.000000E+000  1.000000E+000  2.530000E-005  5.035908E-002  1.113169E-025  0.000000E+000  0.000000E+000  1.891555E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  9.213721E-026
+  2.465474E+003  3.969918E+004  6.719649E+003  6.719649E+003  3.105530E+003  2.875423E+003  1.032626E+000  7.221282E-008  4.739956E-002  1.826998E-012  2.593958E-030  1.220022E-034  0.000000E+000  9.999999E-001  2.530000E-005  3.260044E-002  1.834268E-025  0.000000E+000  0.000000E+000  3.022347E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.463181E-025
+  2.406345E+003  4.366909E+004  6.558493E+003  6.558492E+003  2.887407E+003  2.673462E+003  1.016357E+000  1.226510E-007  6.366810E-002  3.103111E-012  8.105726E-030  3.818032E-034  0.000000E+000  9.999999E-001  2.530000E-005  1.633190E-002  3.174686E-025  0.000000E+000  0.000000E+000  4.063849E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.435470E-025
+  2.345928E+003  4.803600E+004  6.393828E+003  6.393827E+003  2.675338E+003  2.477107E+003  1.006504E+000  2.184024E-007  7.352125E-002  5.525706E-012  2.816851E-029  1.328930E-033  0.000000E+000  9.999998E-001  2.529999E-005  6.478749E-003  5.784070E-025  0.000000E+000  0.000000E+000  4.707913E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.278973E-025
+  2.283783E+003  5.283960E+004  6.224451E+003  6.224449E+003  2.468306E+003  2.285415E+003  1.002162E+000  4.111166E-007  7.786308E-002  1.040170E-011  1.108248E-028  5.237495E-033  0.000000E+000  9.999996E-001  2.529999E-005  2.136923E-003  1.119470E-024  0.000000E+000  0.000000E+000  5.015197E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.996486E-025
+  2.220005E+003  5.812356E+004  6.050625E+003  6.050623E+003  2.267235E+003  2.099243E+003  1.000636E+000  8.198028E-007  7.938820E-002  2.074279E-011  4.955900E-028  2.346501E-032  0.000000E+000  9.999992E-001  2.529998E-005  6.118041E-004  2.308267E-024  0.000000E+000  0.000000E+000  5.159509E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.588982E-024
+  2.155714E+003  6.393592E+004  5.875399E+003  5.875397E+003  2.075907E+003  1.922091E+003  1.000182E+000  1.717212E-006  7.984194E-002  4.345315E-011  2.472590E-027  1.173052E-031  0.000000E+000  9.999983E-001  2.529996E-005  1.580624E-004  5.028965E-024  0.000000E+000  0.000000E+000  5.253913E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.324095E-024
+  2.090388E+003  7.032951E+004  5.697353E+003  5.697352E+003  1.892846E+003  1.752594E+003  1.000058E+000  3.819751E-006  7.996355E-002  9.667670E-011  1.409523E-026  6.701507E-031  0.000000E+000  9.999962E-001  2.529990E-005  3.645421E-005  1.171721E-023  0.000000E+000  0.000000E+000  5.349618E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.396908E-024
+  2.024039E+003  7.736246E+004  5.516517E+003  5.516516E+003  1.718268E+003  1.590951E+003  1.000024E+000  9.083787E-006  7.999256E-002  2.300160E-010  9.320662E-026  4.441800E-030  0.000000E+000  9.999909E-001  2.529977E-005  7.435585E-006  2.942456E-023  0.000000E+000  0.000000E+000  5.466781E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.762688E-023
+  1.957371E+003  8.509871E+004  5.334815E+003  5.334813E+003  1.554010E+003  1.438865E+003  1.000004E+000  2.304835E-005  7.999865E-002  5.842124E-010  7.122032E-025  3.402529E-029  0.000000E+000  9.999770E-001  2.529942E-005  1.347633E-006  7.952322E-023  0.000000E+000  0.000000E+000  5.614358E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.489950E-023
+  1.890718E+003  9.360858E+004  5.153152E+003  5.153150E+003  1.400602E+003  1.296824E+003  9.999630E-001  6.253200E-005  7.999978E-002  1.587891E-009  6.322436E-024  3.028616E-028  0.000000E+000  9.999375E-001  2.529841E-005  2.162309E-007  2.319867E-022  0.000000E+000  0.000000E+000  5.799232E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.225429E-022
+  1.823730E+003  1.029694E+005  4.970576E+003  4.970574E+003  1.256945E+003  1.163810E+003  9.998415E-001  1.838010E-004  7.999997E-002  4.678031E-009  6.715386E-023  3.226145E-027  0.000000E+000  9.998162E-001  2.529532E-005  2.998809E-008  7.413397E-022  0.000000E+000  0.000000E+000  6.030835E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.631876E-022
+  1.757110E+003  1.132664E+005  4.789003E+003  4.789001E+003  1.124168E+003  1.040872E+003  9.994419E-001  5.834377E-004  8.000000E-002  1.487696E-008  8.482586E-022  4.087718E-026  0.000000E+000  9.994166E-001  2.528512E-005  3.614538E-009  2.587754E-021  0.000000E+000  0.000000E+000  6.316241E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.165232E-021
+  1.690769E+003  1.245930E+005  4.608192E+003  4.608190E+003  1.001585E+003  9.273713E+002  9.980078E-001  2.017416E-003  8.000000E-002  5.149792E-008  1.300238E-020  6.286518E-025  0.000000E+000  9.979826E-001  2.524850E-005  3.720185E-010  9.960912E-021  0.000000E+000  0.000000E+000  6.663902E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.080555E-021
+  1.624984E+003  1.370523E+005  4.428893E+003  4.428891E+003  8.891632E+002  8.232800E+002  9.924409E-001  7.584178E-003  8.000000E-002  1.937150E-007  2.413797E-019  1.171141E-023  0.000000E+000  9.924158E-001  2.510628E-005  3.254678E-011  4.211770E-020  0.000000E+000  0.000000E+000  7.066382E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.552399E-020
+  1.560044E+003  1.507576E+005  4.251900E+003  4.251900E+003  7.867648E+002  7.284689E+002  9.697415E-001  3.028306E-002  8.000000E-002  7.736873E-007  5.189459E-018  2.526862E-022  0.000000E+000  9.697169E-001  2.452631E-005  2.438196E-012  1.887841E-019  0.000000E+000  0.000000E+000  7.437139E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.186790E-020
+  1.495959E+003  1.658333E+005  4.077237E+003  4.077246E+003  6.937353E+002  6.423324E+002  8.833419E-001  1.166804E-001  8.000000E-002  2.979604E-006  1.070255E-016  5.227758E-021  0.000000E+000  8.833196E-001  2.232040E-005  1.621813E-013  7.745461E-019  0.000000E+000  0.000000E+000  7.379167E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.227260E-019
+  1.432889E+003  1.824166E+005  3.905338E+003  3.905377E+003  6.096377E+002  5.644661E+002  6.610883E-001  3.389284E-001  8.000000E-002  8.637399E-006  1.296519E-015  6.344764E-020  0.000000E+000  6.610716E-001  1.666260E-005  1.079821E-014  2.006082E-018  0.000000E+000  0.000000E+000  6.089954E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.989448E-019
+  1.371360E+003  2.006583E+005  3.737641E+003  3.737762E+003  5.344273E+002  4.948285E+002  3.744022E-001  6.256072E-001  8.000000E-002  1.589593E-005  6.546740E-015  3.207561E-019  0.000000E+000  3.743928E-001  9.404068E-006  8.024331E-016  2.547514E-018  0.000000E+000  0.000000E+000  3.852133E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.401022E-019
+  1.311036E+003  2.207241E+005  3.573229E+003  3.573582E+003  4.669588E+002  4.323592E+002  1.696684E-001  8.303359E-001  8.000000E-002  2.105021E-005  1.764202E-014  8.662553E-019  0.000000E+000  1.696641E-001  4.249786E-006  6.410424E-017  1.903765E-018  0.000000E+000  0.000000E+000  1.978523E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.382740E-019
+  1.252051E+003  2.427966E+005  3.412465E+003  3.413494E+003  4.067246E+002  3.765880E+002  6.731842E-002  9.326833E-001  8.000000E-002  2.361679E-005  3.462626E-014  1.706246E-018  0.000000E+000  6.731674E-002  1.683212E-006  5.880040E-018  1.080424E-018  0.000000E+000  0.000000E+000  9.040086E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.579608E-019
+  1.194477E+003  2.670762E+005  3.255548E+003  3.258428E+003  3.531574E+002  3.269899E+002  2.531832E-002  9.746823E-001  8.000000E-002  2.466765E-005  5.742875E-014  2.842619E-018  0.000000E+000  2.531768E-002  6.323499E-007  8.766468E-019  5.556832E-019  0.000000E+000  0.000000E+000  3.983841E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.550635E-020
+  1.138933E+003  2.937838E+005  3.104163E+003  3.110772E+003  3.061466E+002  2.834625E+002  1.010285E-002  9.898974E-001  8.000000E-002  2.504788E-005  8.332902E-014  4.145769E-018  0.000000E+000  1.010260E-002  2.521219E-007  3.036711E-019  3.027660E-019  0.000000E+000  0.000000E+000  1.894570E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.821402E-020
+  1.085222E+003  3.231622E+005  2.957773E+003  2.968475E+003  2.648440E+002  2.452202E+002  4.799868E-003  9.952003E-001  8.000000E-002  2.518030E-005  1.092894E-013  5.466184E-018  0.000000E+000  4.799748E-003  1.196955E-007  1.873528E-019  2.001991E-019  0.000000E+000  0.000000E+000  1.093417E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.443178E-020
+  1.032744E+003  3.554784E+005  2.814743E+003  2.827338E+003  2.282506E+002  2.113382E+002  2.819167E-003  9.971809E-001  8.000000E-002  2.522975E-005  1.355824E-013  6.821360E-018  0.000000E+000  2.819096E-003  7.024961E-008  1.459420E-019  1.687948E-019  0.000000E+000  0.000000E+000  7.988044E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  9.172137E-021
+  9.821738E+002  3.910263E+005  2.676915E+003  2.689644E+003  1.963358E+002  1.817882E+002  1.957161E-003  9.980429E-001  8.000000E-002  2.525127E-005  1.638260E-013  8.294937E-018  0.000000E+000  1.957112E-003  4.873113E-008  1.250446E-019  1.736497E-019  0.000000E+000  0.000000E+000  7.057700E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.939794E-021
+  9.339302E+002  4.301289E+005  2.545427E+003  2.557763E+003  1.688021E+002  1.562946E+002  1.504991E-003  9.984950E-001  8.000000E-002  2.526256E-005  1.960920E-013  9.995393E-018  0.000000E+000  1.504954E-003  3.744152E-008  1.121874E-019  2.042375E-019  0.000000E+000  0.000000E+000  7.060247E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.862608E-021
+  8.870018E+002  4.731418E+005  2.417523E+003  2.429306E+003  1.446132E+002  1.338980E+002  1.225188E-003  9.987748E-001  8.000000E-002  2.526955E-005  2.361168E-013  1.212204E-017  0.000000E+000  1.225157E-003  3.045331E-008  1.031439E-019  2.656682E-019  0.000000E+000  0.000000E+000  7.699139E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.301280E-021
+  8.416509E+002  5.204560E+005  2.293920E+003  2.305038E+003  1.235465E+002  1.143922E+002  1.036264E-003  9.989638E-001  8.000000E-002  2.527427E-005  2.886628E-013  1.493235E-017  0.000000E+000  1.036238E-003  2.573277E-008  9.638274E-020  3.757184E-019  0.000000E+000  0.000000E+000  8.992751E-022  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.038734E-021
+  7.986964E+002  5.725016E+005  2.176847E+003  2.187198E+003  1.055795E+002  9.775650E+001  9.025311E-004  9.990975E-001  8.000000E-002  2.527761E-005  3.605374E-013  1.879652E-017  0.000000E+000  9.025087E-004  2.238935E-008  9.118621E-020  5.685059E-019  0.000000E+000  0.000000E+000  1.110228E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.981913E-021
+  7.572431E+002  6.297517E+005  2.063866E+003  2.073321E+003  8.997886E+001  8.331181E+001  8.012929E-004  9.991987E-001  8.000000E-002  2.528014E-005  4.672246E-013  2.455759E-017  0.000000E+000  8.012730E-004  1.985642E-008  8.697504E-020  9.237185E-019  0.000000E+000  0.000000E+000  1.446941E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.089102E-021
+  7.176872E+002  6.927269E+005  1.956057E+003  1.964482E+003  7.660200E+001  7.092612E+001  7.228051E-004  9.992772E-001  8.000000E-002  2.528211E-005  6.345095E-013  3.362845E-017  0.000000E+000  7.227872E-004  1.789088E-008  8.351591E-020  1.602406E-018  0.000000E+000  0.000000E+000  1.981419E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.350934E-021
+  6.795804E+002  7.619996E+005  1.852196E+003  1.859434E+003  6.503648E+001  6.021756E+001  6.596547E-004  9.993404E-001  8.000000E-002  2.528369E-005  9.181211E-013  4.907423E-017  0.000000E+000  6.596384E-004  1.630759E-008  8.059096E-020  2.987794E-018  0.000000E+000  0.000000E+000  2.859385E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.782234E-021
+  6.431924E+002  8.381995E+005  1.753021E+003  1.758905E+003  5.513879E+001  5.105324E+001  6.081762E-004  9.993918E-001  8.000000E-002  2.528498E-005  1.429905E-012  7.708265E-017  0.000000E+000  6.081612E-004  1.501511E-008  7.810134E-020  5.973003E-018  0.000000E+000  0.000000E+000  4.340157E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.411292E-021
+  6.089863E+002  9.220195E+005  1.659792E+003  1.664169E+003  4.680122E+001  4.333344E+001  5.660495E-004  9.994340E-001  8.000000E-002  2.528604E-005  2.400340E-012  1.304801E-016  0.000000E+000  5.660355E-004  1.395561E-008  7.598628E-020  1.267937E-017  0.000000E+000  0.000000E+000  6.885307E-021  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.274095E-021
+  5.757396E+002  1.014221E+006  1.569178E+003  1.571825E+003  3.954693E+001  3.661667E+001  5.298575E-004  9.994702E-001  8.000000E-002  2.528696E-005  4.453827E-012  2.441502E-016  0.000000E+000  5.298444E-004  1.304350E-008  7.410845E-020  2.943669E-017  0.000000E+000  0.000000E+000  1.162341E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  8.481751E-021
+  5.445589E+002  1.115644E+006  1.484195E+003  1.484935E+003  3.346332E+001  3.098383E+001  4.994679E-004  9.995005E-001  8.000000E-002  2.528772E-005  8.979111E-012  4.962371E-016  0.000000E+000  4.994556E-004  1.227574E-008  7.248523E-020  7.288551E-017  0.000000E+000  0.000000E+000  2.055583E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.011345E-020
+  5.145064E+002  1.227208E+006  1.402287E+003  1.400879E+003  2.822324E+001  2.613202E+001  4.729392E-004  9.995271E-001  8.000000E-002  2.528840E-005  2.009058E-011  1.119241E-015  0.000000E+000  4.729276E-004  1.160355E-008  7.103154E-020  1.981782E-016  0.000000E+000  0.000000E+000  3.878911E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.239724E-020
+  4.860155E+002  1.349929E+006  1.324635E+003  1.320855E+003  2.378947E+001  2.202678E+001  4.499443E-004  9.995501E-001  8.000000E-002  2.528898E-005  4.941042E-011  2.773665E-015  0.000000E+000  4.499333E-004  1.101886E-008  6.974258E-020  5.859542E-016  0.000000E+000  0.000000E+000  7.762859E-020  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.559031E-020
+  4.591644E+002  1.484922E+006  1.251453E+003  1.245085E+003  2.006038E+001  1.857399E+001  4.299473E-004  9.995701E-001  8.000000E-002  2.528949E-005  1.329528E-010  7.515672E-015  0.000000E+000  4.299368E-004  1.050831E-008  6.859909E-020  1.878781E-015  0.000000E+000  0.000000E+000  1.645004E-019  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.009704E-020
+  4.334508E+002  1.633414E+006  1.181370E+003  1.172150E+003  1.687539E+001  1.562499E+001  4.121395E-004  9.995879E-001  8.000000E-002  2.528995E-005  3.989955E-010  2.269635E-014  0.000000E+000  4.121295E-004  1.005145E-008  6.756275E-020  6.690440E-015  0.000000E+000  0.000000E+000  3.750779E-019  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.669675E-020
+  4.090984E+002  1.796755E+006  1.114998E+003  1.102685E+003  1.418789E+001  1.313662E+001  3.963512E-004  9.996037E-001  8.000000E-002  2.529036E-005  1.324580E-009  7.574860E-014  0.000000E+000  3.963416E-004  9.644087E-009  6.662959E-020  2.617282E-014  0.000000E+000  0.000000E+000  9.164692E-019  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.649314E-020
+  3.857217E+002  1.976431E+006  1.051285E+003  1.035592E+003  1.189205E+001  1.101090E+001  3.820777E-004  9.996179E-001  8.000000E-002  2.529073E-005  4.927859E-009  2.832806E-013  0.000000E+000  3.820685E-004  9.273328E-009  6.577438E-020  1.127801E-013  0.000000E+000  0.000000E+000  2.443255E-018  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.166126E-020
+  3.639938E+002  2.174074E+006  9.920651E+002  9.728189E+002  9.993472E+000  9.252998E+000  3.695091E-004  9.996305E-001  8.000000E-002  2.529105E-005  1.866458E-008  1.084869E-012  0.000000E+000  3.695002E-004  8.944286E-009  6.501238E-020  4.530293E-013  0.000000E+000  0.000000E+000  6.908488E-018  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.500241E-020
+  3.432870E+002  2.391481E+006  9.356287E+002  9.125825E+002  8.383134E+000  7.761980E+000  3.580978E-004  9.996418E-001  8.000000E-002  2.529135E-005  5.989336E-008  3.608318E-012  0.000000E+000  3.580892E-004  8.642818E-009  6.431350E-020  1.165447E-012  0.000000E+000  0.000000E+000  2.126364E-017  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.128468E-019
+  3.236451E+002  2.630629E+006  8.820948E+002  8.550316E+002  7.024925E+000  6.504408E+000  3.477379E-004  9.996520E-001  8.000000E-002  2.529163E-005  1.255368E-007  7.993816E-012  0.000000E+000  3.477295E-004  8.366207E-009  6.367348E-020  1.472356E-012  0.000000E+000  0.000000E+000  7.042222E-017  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.760637E-019
+  3.050449E+002  2.893692E+006  8.313998E+002  8.001292E+002  5.882007E+000  5.446176E+000  3.383067E-004  9.996613E-001  8.000000E-002  2.529188E-005  1.886700E-007  1.260394E-011  0.000000E+000  3.382986E-004  8.111268E-009  6.308658E-020  1.362583E-012  0.000000E+000  0.000000E+000  2.399975E-016  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.853146E-019
+  2.874388E+002  3.183061E+006  7.834146E+002  7.477748E+002  4.921196E+000  4.556558E+000  3.296891E-004  9.996698E-001  8.000000E-002  2.529211E-005  2.405386E-007  1.674265E-011  0.000000E+000  3.296812E-004  7.874972E-009  6.254708E-020  1.190105E-012  0.000000E+000  0.000000E+000  7.231293E-016  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.813245E-019
+  2.707080E+002  3.501368E+006  7.378146E+002  6.976579E+002  4.110906E+000  3.806306E+000  3.217534E-004  9.996777E-001  8.000000E-002  2.529233E-005  2.822013E-007  2.043435E-011  0.000000E+000  3.217458E-004  7.653745E-009  6.204788E-020  1.030719E-012  0.000000E+000  0.000000E+000  1.472924E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  8.496359E-019
+  2.549640E+002  3.851504E+006  6.949044E+002  6.501654E+002  3.434558E+000  3.180073E+000  3.144902E-004  9.996849E-001  8.000000E-002  2.529253E-005  3.152023E-007  2.375973E-011  0.000000E+000  3.144828E-004  7.447373E-009  6.158931E-020  8.914602E-013  0.000000E+000  0.000000E+000  1.948183E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.565098E-018
+  2.400542E+002  4.236655E+006  6.542678E+002  6.048959E+002  2.866569E+000  2.654169E+000  3.077765E-004  9.996915E-001  8.000000E-002  2.529272E-005  3.414619E-007  2.685360E-011  0.000000E+000  3.077692E-004  7.252407E-009  6.116435E-020  7.694722E-013  0.000000E+000  0.000000E+000  2.130842E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.028962E-018
+  2.260149E+002  4.660320E+006  6.160037E+002  5.620211E+002  2.392465E+000  2.215194E+000  3.015853E-004  9.996977E-001  8.000000E-002  2.529290E-005  3.622012E-007  2.979956E-011  0.000000E+000  3.015782E-004  7.068093E-009  6.077187E-020  6.634237E-013  0.000000E+000  0.000000E+000  2.224080E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.155043E-018
+  2.128577E+002  5.126352E+006  5.801436E+002  5.216440E+002  1.998491E+000  1.850412E+000  2.958844E-004  9.997034E-001  8.000000E-002  2.529307E-005  3.784893E-007  3.267646E-011  0.000000E+000  2.958775E-004  6.893548E-009  6.041027E-020  5.717113E-013  0.000000E+000  0.000000E+000  2.304485E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.312013E-017
+  2.003365E+002  5.638987E+006  5.460171E+002  4.830739E+002  1.666152E+000  1.542697E+000  2.905378E-004  9.997087E-001  8.000000E-002  2.529324E-005  3.914559E-007  3.561552E-011  0.000000E+000  2.905311E-004  6.724588E-009  6.007128E-020  4.912252E-013  0.000000E+000  0.000000E+000  2.390321E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.979033E-017
+  1.884700E+002  6.202886E+006  5.136751E+002  4.464301E+002  1.387271E+000  1.284481E+000  2.855303E-004  9.997137E-001  8.000000E-002  2.529340E-005  4.017047E-007  3.869226E-011  0.000000E+000  2.855237E-004  6.560588E-009  5.975419E-020  4.209836E-013  0.000000E+000  0.000000E+000  2.484928E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.219384E-017
+  1.773194E+002  6.823175E+006  4.832842E+002  4.119633E+002  1.155323E+000  1.069719E+000  2.808671E-004  9.997183E-001  8.000000E-002  2.529356E-005  4.097224E-007  4.196705E-011  0.000000E+000  2.808607E-004  6.401657E-009  5.945953E-020  3.602798E-013  0.000000E+000  0.000000E+000  2.588695E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.861310E-016
+  1.668922E+002  7.505492E+006  4.548648E+002  3.797551E+002  9.632579E-001  8.918849E-001  2.765339E-004  9.997226E-001  8.000000E-002  2.529371E-005  4.159676E-007  4.550673E-011  0.000000E+000  2.765276E-004  6.247353E-009  5.918651E-020  3.081267E-013  0.000000E+000  0.000000E+000  2.701793E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.097017E-016
+  1.569445E+002  8.256041E+006  4.277524E+002  3.491054E+002  8.010747E-001  7.417188E-001  2.724155E-004  9.997267E-001  8.000000E-002  2.529386E-005  4.209474E-007  4.947789E-011  0.000000E+000  2.724094E-004  6.093447E-009  5.892799E-020  2.625034E-013  0.000000E+000  0.000000E+000  2.827500E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.520678E-015
+  1.475456E+002  9.081646E+006  4.021356E+002  3.202765E+002  6.656001E-001  6.162822E-001  2.685295E-004  9.997306E-001  8.000000E-002  2.529401E-005  4.249571E-007  5.395526E-011  0.000000E+000  2.685235E-004  5.940310E-009  5.868515E-020  2.230842E-013  0.000000E+000  0.000000E+000  2.965979E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.926504E-015
+  1.388552E+002  9.989810E+006  3.784499E+002  2.937939E+002  5.547801E-001  5.136734E-001  2.649327E-004  9.997342E-001  8.000000E-002  2.529415E-005  4.284069E-007  5.894256E-011  0.000000E+000  2.649269E-004  5.790258E-009  5.846153E-020  1.898079E-013  0.000000E+000  0.000000E+000  3.115063E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.694196E-014
+  1.305486E+002  1.098879E+007  3.558103E+002  2.686983E+002  4.610535E-001  4.268915E-001  2.614835E-004  9.997377E-001  8.000000E-002  2.529430E-005  4.325177E-007  6.475503E-011  0.000000E+000  2.614778E-004  5.637300E-009  5.824833E-020  1.608325E-013  0.000000E+000  0.000000E+000  3.280948E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.466064E-014
+  1.227552E+002  1.208767E+007  3.345694E+002  2.454073E+002  3.833136E-001  3.549118E-001  2.582296E-004  9.997409E-001  8.000000E-002  2.529445E-005  4.410285E-007  7.159025E-011  0.000000E+000  2.582241E-004  5.483251E-009  5.804850E-020  1.361446E-013  0.000000E+000  0.000000E+000  3.462336E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.668644E-013
+  1.153897E+002  1.329644E+007  3.144947E+002  2.236816E+002  3.183725E-001  2.947826E-001  2.551309E-004  9.997439E-001  8.000000E-002  2.529459E-005  4.694552E-007  8.034811E-011  0.000000E+000  2.551256E-004  5.326054E-009  5.785958E-020  1.150133E-013  0.000000E+000  0.000000E+000  3.662241E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.154340E-012
+  1.084853E+002  1.462608E+007  2.956768E+002  2.036265E+002  2.645741E-001  2.449704E-001  2.521974E-004  9.997467E-001  8.000000E-002  2.529474E-005  5.714643E-007  9.380365E-011  0.000000E+000  2.521923E-004  5.166091E-009  5.768216E-020  9.712431E-014  0.000000E+000  0.000000E+000  3.880899E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.182390E-012
+  1.019356E+002  1.608869E+007  2.778255E+002  1.849347E+002  2.194886E-001  2.032256E-001  2.493805E-004  9.997490E-001  8.000000E-002  2.529488E-005  8.216663E-007  1.187613E-010  0.000000E+000  2.493755E-004  5.000532E-009  5.751333E-020  8.184546E-014  0.000000E+000  0.000000E+000  4.123221E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  8.160158E-012
+  9.581458E+001  1.769756E+007  2.611427E+002  1.678096E+002  1.822757E-001  1.687701E-001  2.467118E-004  9.997510E-001  8.000000E-002  2.529501E-005  1.142950E-006  1.590901E-010  0.000000E+000  2.467070E-004  4.831034E-009  5.735486E-020  6.902839E-014  0.000000E+000  0.000000E+000  4.388152E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  8.910460E-012
+  9.006415E+001  1.946731E+007  2.454699E+002  1.520695E+002  1.513874E-001  1.401704E-001  2.441674E-004  9.997530E-001  8.000000E-002  2.529513E-005  1.436475E-006  2.115081E-010  0.000000E+000  2.441628E-004  4.656084E-009  5.720515E-020  5.824676E-014  0.000000E+000  0.000000E+000  4.679584E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  8.190582E-012
+  8.463566E+001  2.141405E+007  2.306745E+002  1.375633E+002  1.256302E-001  1.163217E-001  2.417261E-004  9.997549E-001  8.000000E-002  2.529525E-005  1.684471E-006  2.715331E-010  0.000000E+000  2.417217E-004  4.474129E-009  5.706287E-020  4.916109E-014  0.000000E+000  0.000000E+000  5.002302E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  7.316823E-012
+  7.951202E+001  2.355545E+007  2.167100E+002  1.242255E+002  1.041675E-001  9.644927E-002  2.393804E-004  9.997568E-001  8.000000E-002  2.529538E-005  1.891128E-006  3.356904E-010  0.000000E+000  2.393761E-004  4.284496E-009  5.692753E-020  4.153181E-014  0.000000E+000  0.000000E+000  5.360465E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  6.509693E-012
+  7.467543E+001  2.591099E+007  2.035279E+002  1.119863E+002  8.629126E-002  7.989760E-002  2.371226E-004  9.997588E-001  8.000000E-002  2.529551E-005  2.062866E-006  4.013810E-010  0.000000E+000  2.371185E-004  4.086547E-009  5.679862E-020  3.514648E-014  0.000000E+000  0.000000E+000  5.759118E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.787095E-012
+  7.017007E+001  2.850209E+007  1.912485E+002  1.009243E+002  7.159611E-002  6.629127E-002  2.349752E-004  9.997606E-001  8.000000E-002  2.529565E-005  2.203600E-006  4.655855E-010  0.000000E+000  2.349713E-004  3.882664E-009  5.667735E-020  2.988679E-014  0.000000E+000  0.000000E+000  6.197841E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  5.151647E-012
+  6.592292E+001  3.135230E+007  1.796729E+002  9.082379E+001  5.936669E-002  5.496798E-002  2.329057E-004  9.997625E-001  8.000000E-002  2.529580E-005  2.320266E-006  5.273060E-010  0.000000E+000  2.329020E-004  3.670394E-009  5.656180E-020  2.551285E-014  0.000000E+000  0.000000E+000  6.687499E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.587058E-012
+  6.194158E+001  3.448753E+007  1.688218E+002  8.166895E+001  4.924706E-002  4.559816E-002  2.309199E-004  9.997643E-001  8.000000E-002  2.529596E-005  2.416364E-006  5.848059E-010  0.000000E+000  2.309164E-004  3.451026E-009  5.645221E-020  2.190560E-014  0.000000E+000  0.000000E+000  7.232558E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  4.089009E-012
+  5.810253E+001  3.793629E+007  1.583585E+002  7.315137E+001  4.064608E-002  3.763446E-002  2.289569E-004  9.997661E-001  8.000000E-002  2.529614E-005  2.497607E-006  6.385807E-010  0.000000E+000  2.289537E-004  3.218247E-009  5.634519E-020  1.885758E-014  0.000000E+000  0.000000E+000  7.860068E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.638098E-012
+  5.463936E+001  4.172992E+007  1.489196E+002  6.574776E+001  3.380262E-002  3.129806E-002  2.271399E-004  9.997677E-001  8.000000E-002  2.529633E-005  2.561867E-006  6.846069E-010  0.000000E+000  2.271369E-004  2.988244E-009  5.624736E-020  1.645156E-014  0.000000E+000  0.000000E+000  8.537545E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  3.256642E-012
+  5.130393E+001  4.590291E+007  1.398289E+002  5.888496E+001  2.798242E-002  2.590910E-002  2.253428E-004  9.997694E-001  8.000000E-002  2.529653E-005  2.616166E-006  7.258462E-010  0.000000E+000  2.253400E-004  2.747058E-009  5.615181E-020  1.442451E-014  0.000000E+000  0.000000E+000  9.320518E-015  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.912471E-012
+  4.821007E+001  5.049320E+007  1.313965E+002  5.276826E+001  2.321917E-002  2.149878E-002  2.236289E-004  9.997711E-001  8.000000E-002  2.529673E-005  2.660290E-006  7.607670E-010  0.000000E+000  2.236263E-004  2.504933E-009  5.606187E-020  1.278221E-014  0.000000E+000  0.000000E+000  1.019681E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.614047E-012
+  4.522805E+001  5.554252E+007  1.232691E+002  4.711226E+001  1.917154E-002  1.775105E-002  2.219285E-004  9.997727E-001  8.000000E-002  2.529695E-005  2.697497E-006  7.909465E-010  0.000000E+000  2.219263E-004  2.254244E-009  5.597383E-020  1.140009E-014  0.000000E+000  0.000000E+000  1.122239E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.345813E-012
+  4.251272E+001  6.109677E+007  1.158684E+002  4.217719E+001  1.592172E-002  1.474202E-002  2.203335E-004  9.997742E-001  8.000000E-002  2.529717E-005  2.727125E-006  8.151859E-010  0.000000E+000  2.203315E-004  2.011385E-009  5.589237E-020  1.029823E-014  0.000000E+000  0.000000E+000  1.236195E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  2.118536E-012
+  3.986658E+001  6.720645E+007  1.086564E+002  3.757461E+001  1.312986E-002  1.215703E-002  2.187309E-004  9.997758E-001  8.000000E-002  2.529740E-005  2.752354E-006  8.356743E-010  0.000000E+000  2.187291E-004  1.762374E-009  5.581165E-020  9.353267E-015  0.000000E+000  0.000000E+000  1.372642E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.913042E-012
+  3.747910E+001  7.392709E+007  1.021493E+002  3.360472E+001  1.090940E-002  1.010109E-002  2.172388E-004  9.997772E-001  8.000000E-002  2.529762E-005  2.772234E-006  8.514375E-010  0.000000E+000  2.172373E-004  1.529050E-009  5.573755E-020  8.596120E-015  0.000000E+000  0.000000E+000  1.524847E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.741611E-012
+  3.517334E+001  8.131980E+007  9.586494E+001  2.994199E+001  9.017251E-003  8.349132E-003  2.157511E-004  9.997787E-001  8.000000E-002  2.529784E-005  2.789008E-006  8.642026E-010  0.000000E+000  2.157498E-004  1.298583E-009  5.566472E-020  7.937007E-015  0.000000E+000  0.000000E+000  1.707692E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.589122E-012
+  3.302470E+001  8.945178E+007  9.000884E+001  2.668604E+001  7.463631E-003  6.910626E-003  2.143184E-004  9.997801E-001  8.000000E-002  2.529804E-005  2.802639E-006  8.739627E-010  0.000000E+000  2.143173E-004  1.082663E-009  5.559560E-020  7.373232E-015  0.000000E+000  0.000000E+000  1.921788E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.459148E-012
+  3.100436E+001  9.839696E+007  8.450238E+001  2.376766E+001  6.175918E-003  5.718324E-003  2.129253E-004  9.997814E-001  8.000000E-002  2.529824E-005  2.813813E-006  8.813342E-010  0.000000E+000  2.129244E-004  8.825753E-010  5.552938E-020  6.873859E-015  0.000000E+000  0.000000E+000  2.176804E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.348265E-012
+  2.913074E+001  1.082367E+008  7.939585E+001  2.118924E+001  5.122572E-003  4.743024E-003  2.115888E-004  9.997828E-001  8.000000E-002  2.529841E-005  2.822848E-006  8.867003E-010  0.000000E+000  2.115881E-004  7.037638E-010  5.546679E-020  6.424560E-015  0.000000E+000  0.000000E+000  2.478812E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.256027E-012
+  2.734339E+001  1.190603E+008  7.452442E+001  1.884777E+001  4.236338E-003  3.922453E-003  2.102690E-004  9.997841E-001  8.000000E-002  2.529857E-005  2.830358E-006  8.906108E-010  0.000000E+000  2.102685E-004  5.435595E-010  5.540591E-020  5.995520E-015  0.000000E+000  0.000000E+000  2.850692E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.178497E-012
+  2.568486E+001  1.309664E+008  7.000409E+001  1.678121E+001  3.511277E-003  3.251114E-003  2.090005E-004  9.997853E-001  8.000000E-002  2.529870E-005  2.836422E-006  8.932981E-010  0.000000E+000  2.090001E-004  4.079754E-010  5.534829E-020  5.584660E-015  0.000000E+000  0.000000E+000  3.301291E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.116866E-012
+  2.411109E+001  1.440630E+008  6.571478E+001  1.491720E+001  2.904584E-003  2.689374E-003  2.077532E-004  9.997866E-001  8.000000E-002  2.529881E-005  2.841425E-006  8.951240E-010  0.000000E+000  2.077529E-004  2.944540E-010  5.529250E-020  5.173227E-015  0.000000E+000  0.000000E+000  3.866340E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.069046E-012
+  2.262709E+001  1.584693E+008  6.167012E+001  1.324807E+001  2.400597E-003  2.222729E-003  2.065333E-004  9.997878E-001  8.000000E-002  2.529890E-005  2.845519E-006  8.963068E-010  0.000000E+000  2.065331E-004  2.036036E-010  5.523878E-020  4.754201E-015  0.000000E+000  0.000000E+000  4.581304E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.035351E-012
+  2.122592E+001  1.743162E+008  5.785125E+001  1.175282E+001  1.981678E-003  1.834848E-003  2.053381E-004  9.997890E-001  8.000000E-002  2.529897E-005  2.848865E-006  8.970395E-010  0.000000E+000  2.053379E-004  1.339654E-010  5.518699E-020  4.322880E-015  0.000000E+000  0.000000E+000  5.501588E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.016136E-012
+  1.995168E+001  1.917478E+008  5.437831E+001  1.046252E+001  1.645780E-003  1.523839E-003  2.042100E-004  9.997901E-001  8.000000E-002  2.529902E-005  2.851499E-006  8.974576E-010  0.000000E+000  2.042099E-004  8.486382E-011  5.513888E-020  3.894012E-015  0.000000E+000  0.000000E+000  6.654599E-014  0.000000E+000  0.000000E+000  0.000000E+000  0.000000E+000  1.012244E-012
diff --git a/examples/Cooling/CoolingWithPrimordialElements/getICs.sh b/examples/Cooling/CoolingWithPrimordialElements/getICs.sh
new file mode 100755
index 0000000000000000000000000000000000000000..1d641db93bce61580301b14c634552df6f368671
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/getICs.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/homogeneousCube_32.hdf5
diff --git a/examples/Cooling/CoolingWithPrimordialElements/getPrimordialAbundances.py b/examples/Cooling/CoolingWithPrimordialElements/getPrimordialAbundances.py
new file mode 100755
index 0000000000000000000000000000000000000000..278dd20496648eb7fdc7946fda5bb1ca1fd3c980
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/getPrimordialAbundances.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+
+"""
+This script is an example of how to compute the
+fractional abundances of different elements with
+respect to Hydrogen.
+
+For comparison, see
+for example Dalgarno 2005
+"""
+
+# some parameters (Planck 2018)
+Omega0 = 0.315
+Omega_b = 0.049
+h0 = 0.674
+T0 = 2.73
+fH = 0.76
+z0 = 300
+
+z = 77
+
+# Temperature
+T = T0 / (1 + 200) * (1 + z) ** 2  # Anninos & Norman 1996 eq.11
+
+# all abundances per unit of nH
+nHII = 1.2e-5 * Omega0 ** 0.5 / (h0 * Omega_b)  # Anninos & Norman 1996 eq. 9
+
+nHeI = (1 - fH) / fH / 4
+nHeII = 0  # assume neutral He
+nHeIII = 0  # assume neutral He
+
+nHM = 2e-11 * (1 + z) ** (1.76) * nHII  # Anninos & Norman 1996 eq. 12
+
+nH2I = (
+    2e-20 * fH * Omega0 ** (3 / 2.0) / (h0 * Omega_b) * (1 + z0) ** (5.1)
+)  # Anninos & Norman 1996 eq. 15
+nH2II = 1e-17 * (1 + z) ** (3.6) * nHII  # Anninos & Norman 1996 eq. 13
+
+nDI = 4e-5  # Bromm 20002 from Galli & Palla 1998
+nDII = 1e-8  # Bromm 20002
+
+nHDI = 1e-3 * nH2I  # Bromm 20002
+
+
+print("T                           : %g K" % T)
+
+print("initial_nHII_to_nH_ratio    : %g" % nHII)
+
+print("initial_nHeI_to_nH_ratio    : %g" % nHeI)
+print("initial_nHeII_to_nH_ratio   : %g" % nHeII)
+print("initial_nHeIII_to_nH_ratio  : %g" % nHeIII)
+
+print("initial_nDI_to_nH_ratio     : %g" % nDI)
+print("initial_nDII_to_nH_ratio    : %g" % nDII)
+
+print("initial_nHM_to_nH_ratio     : %g" % nHM)
+
+print("initial_nH2I_to_nH_ratio    : %g" % nH2I)
+print("initial_nH2II_to_nH_ratio   : %g" % nH2II)
+
+
+print("initial_nHDI_to_nH_ratio    : %g" % nHDI)
diff --git a/examples/Cooling/CoolingWithPrimordialElements/local.mplstyle b/examples/Cooling/CoolingWithPrimordialElements/local.mplstyle
new file mode 100644
index 0000000000000000000000000000000000000000..f0a096801d2065ef70102d0f6c9256b0bb057f27
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/local.mplstyle
@@ -0,0 +1,28 @@
+# Line Properties
+lines.linewidth: 2
+
+# Font options
+font.size: 8
+font.family: STIXGeneral
+mathtext.fontset: stix
+
+# LaTeX options
+text.usetex: False
+
+# Legend options
+#legend.frameon: False
+#legend.fontsize: 8
+
+# Figure options for publications
+figure.dpi: 300
+figure.figsize: 3.321, 3.0 # Correct width for MNRAS
+
+# Histogram options
+hist.bins: auto
+
+# Ticks inside plots; more space devoted to plot.
+xtick.direction: in
+ytick.direction: in
+# Always plot ticks on top of data
+axes.axisbelow: False
+
diff --git a/examples/Cooling/CoolingWithPrimordialElements/output_list.txt b/examples/Cooling/CoolingWithPrimordialElements/output_list.txt
new file mode 100644
index 0000000000000000000000000000000000000000..47d7c037b0d3d86dcc0217a5ed48cf6a4282c21a
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/output_list.txt
@@ -0,0 +1,28 @@
+# Redshift
+77
+70
+60
+50
+40
+30
+20
+19
+18
+17
+16
+15
+14
+13
+12
+11
+10
+9
+8
+7
+6
+5
+4
+3
+2
+1
+0
diff --git a/examples/Cooling/CoolingWithPrimordialElements/plotAbundances.py b/examples/Cooling/CoolingWithPrimordialElements/plotAbundances.py
new file mode 100755
index 0000000000000000000000000000000000000000..546a56891a192d199c5711f2ac60b4267be052df
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/plotAbundances.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Yves Revaz (yves.revaz@epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import matplotlib.pyplot as plt
+
+plt.style.use("local.mplstyle")
+
+import numpy as np
+from glob import glob
+import h5py
+import sys
+
+filenames = sys.argv[1:]
+
+n = len(filenames)
+
+
+rs = np.zeros(n)
+nHIs = np.zeros(n)
+nHIIs = np.zeros(n)
+nHeIs = np.zeros(n)
+nHeIIs = np.zeros(n)
+nHeIIIs = np.zeros(n)
+nHMs = np.zeros(n)
+nH2Is = np.zeros(n)
+nH2IIs = np.zeros(n)
+nDIs = np.zeros(n)
+nDIIs = np.zeros(n)
+nHDIs = np.zeros(n)
+
+
+for i, filename in enumerate(filenames):
+
+    with h5py.File(filename, "r") as f:
+
+        XHI = f["PartType0/HI"][:]
+        XHII = f["PartType0/HII"][:]
+        XHeI = f["PartType0/HeI"][:]
+        XHeII = f["PartType0/HeII"][:]
+        XHeIII = f["PartType0/HeIII"][:]
+        XHM = f["PartType0/HM"][:]
+        XH2I = f["PartType0/H2I"][:]
+        XH2II = f["PartType0/H2II"][:]
+        XDI = f["PartType0/DI"][:]
+        XDII = f["PartType0/DII"][:]
+        XHDI = f["PartType0/HDI"][:]
+
+        redshift = f["Header"].attrs["Redshift"][0]
+        rs[i] = redshift
+
+        # compute hydrogen mass fraction
+        XH = (
+            XHI.mean()
+            + XHII.mean()
+            + XHM.mean()
+            + XH2I.mean()
+            + XH2II.mean()
+            + XDI.mean()
+            + XDII.mean()
+            + XHDI.mean()
+        )
+
+        # make it equal to the hydrogen number density (do not consider density on purpose)
+        nH = XH
+
+        # all abundances per unit of nH
+
+        nHI = XHI / nH
+        nHII = XHII / nH
+        nHeI = XHeI / nH / 4
+        nHeII = XHeII / nH / 4
+        nHeIII = XHeIII / nH / 4
+        nHM = XHM / nH
+        nH2I = XH2I / nH / 2
+        nH2II = XH2II / nH / 2
+        nDI = XDI / nH / 2
+        nDII = XDII / nH / 2
+        nHDI = XHDI / nH / 3
+
+        nHIs[i] = nHI.mean()
+        nHIIs[i] = nHII.mean()
+        nHeIs[i] = nHeI.mean()
+        nHeIIs[i] = nHeII.mean()
+        nHeIIIs[i] = nHeIII.mean()
+        nHMs[i] = nHM.mean()
+        nH2Is[i] = nH2I.mean()
+        nH2IIs[i] = nH2II.mean()
+        nDIs[i] = nDI.mean()
+        nDIIs[i] = nDII.mean()
+        nHDIs[i] = nHDI.mean()
+
+
+# data from Faure, A., Hily-Blant, Pineau des Forêts, Flower 2024
+# The last lines have been removed on purpose. Their predictions
+# are not reliable below z=20.
+# Note: the "Redshift" field = z+1
+
+with open("faure2024.dat", "r") as f:
+    f.readline()
+    f.readline()
+    header = str.split(f.readline())
+    lines = f.readlines()
+    data = np.array(list(map(str.split, lines)), float)
+    dic = {x: i for i, x in enumerate(header)}
+
+
+##########################################
+# plot
+##########################################
+
+plt.figure()
+plt.subplots_adjust(left=0.17, bottom=0.12, right=0.97, top=0.97)
+
+ax = plt.gca()
+
+rs = rs + 1
+
+pHI = ax.plot(rs, nHIs, ms=2, lw=1, label="HI")
+pHII = ax.plot(rs, nHIIs, ms=2, lw=1, label="HII")
+pHeI = ax.plot(rs, nHeIs, ms=2, lw=1, label="HeI")
+pHeII = ax.plot(rs, nHeIIs, ms=2, lw=1, label="HeII")
+pHeIII = ax.plot(rs, nHeIIIs, ms=2, lw=1, label="HeIII")
+pHM = ax.plot(rs, nHMs, ms=2, lw=1, label="HM")
+pH2I = ax.plot(rs, nH2Is, ms=2, lw=1, label="H2I")
+pH2II = ax.plot(rs, nH2IIs, ms=2, lw=1, label="H2II")
+pDI = ax.plot(rs, nDIs, ms=2, lw=1, label="DI")
+pDII = ax.plot(rs, nDIIs, ms=2, lw=1, label="DII")
+pHDI = ax.plot(rs, nHDIs, ms=2, lw=1, label="HDI")
+
+
+# add data
+ax.plot(data[:, dic["Redshift"]], data[:, dic["H"]], "--", color=pHI[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["H+"]], "--", color=pHII[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["He"]], "--", color=pHeI[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["He+"]], "--", color=pHeII[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["H-"]], "--", color=pHM[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["H2"]], "--", color=pH2I[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["H2+"]], "--", color=pH2II[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["D"]], "--", color=pDI[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["D+"]], "--", color=pDII[0].get_color())
+ax.plot(data[:, dic["Redshift"]], data[:, dic["HD"]], "--", color=pHDI[0].get_color())
+
+
+ax.set_xlabel(r"$\rm{1+z}$")
+ax.set_xlim(100, 0.9)
+ax.set_ylabel(r"$\rm{Fractional abundances}$")
+ax.set_ylim(1e-21, 2)
+
+ax.loglog()
+ax.legend(loc="lower right")
+
+plt.show()
diff --git a/examples/Cooling/CoolingWithPrimordialElements/run.sh b/examples/Cooling/CoolingWithPrimordialElements/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f3d5ffa09a2aec289600f5dad668709b98d21a1d
--- /dev/null
+++ b/examples/Cooling/CoolingWithPrimordialElements/run.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# Generate the initial conditions if they are not present.
+if [ ! -e homogeneousCube_32.hdf5 ]
+then
+    echo "Fetching initial conditions..."
+    ./getICS.sh
+fi
+
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ../getGrackleCoolingTable.sh
+fi
+
+
+# Create output directory
+rm -rf snap
+mkdir snap
+
+# Run SWIFT
+../../../swift --hydro --cooling --cosmology --threads=14  CoolingWithPrimordialElements.yml
+
+# Check energy conservation and cooling rate
+python3 plotAbundances.py snap/snapshot*.hdf5
+
diff --git a/examples/Cooling/FeedbackEvent_3D/feedback.yml b/examples/Cooling/FeedbackEvent_3D/feedback.yml
index 2b61d6c3c2bf51278000f1f7c73f9cd3d58315db..f3122a489a937cb7ff153171c92d619538f54f9d 100644
--- a/examples/Cooling/FeedbackEvent_3D/feedback.yml
+++ b/examples/Cooling/FeedbackEvent_3D/feedback.yml
@@ -54,6 +54,7 @@ GrackleCooling:
   max_steps: 1000
   convergence_limit: 1e-2
   thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
+  maximal_density_Hpcm3: -1 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
   
 GearChemistry:
   initial_metallicity: 0.01295
diff --git a/examples/Cooling/getGrackleCoolingTable.sh b/examples/Cooling/getGrackleCoolingTable.sh
index 5f702aef17df41bfb2f56f1cb474fe3762b1b232..1d75bc6ed2eadb1f83c517b7d5a272bb5b5504a1 100755
--- a/examples/Cooling/getGrackleCoolingTable.sh
+++ b/examples/Cooling/getGrackleCoolingTable.sh
@@ -1,3 +1,4 @@
 #!/bin/bash
 wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012.h5
-wget https://obswww.unige.ch/~lhausamm/links/CloudyData_UVB=HM2012_shielded.h5
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012_shielded.h5
+
diff --git a/examples/EAGLE_ICs/EAGLE_100/eagle_100.yml b/examples/EAGLE_ICs/EAGLE_100/eagle_100.yml
index 0fd2c5f589557b1738631ca43c7f8c46ff91bdb7..85ae233751d7df7e915e0df8fc3cbf75eb3dbc88 100644
--- a/examples/EAGLE_ICs/EAGLE_100/eagle_100.yml
+++ b/examples/EAGLE_ICs/EAGLE_100/eagle_100.yml
@@ -76,6 +76,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells:   64
diff --git a/examples/EAGLE_ICs/EAGLE_100/run.sh b/examples/EAGLE_ICs/EAGLE_100/run.sh
index 7b2d0e95bc83036cddba847e0acb405a198dbb13..f397d6ca7ed688c0145b975e47f1315b4975b0e8 100755
--- a/examples/EAGLE_ICs/EAGLE_100/run.sh
+++ b/examples/EAGLE_ICs/EAGLE_100/run.sh
@@ -14,10 +14,10 @@ then
     ../getEagleYieldtable.sh
 fi
 
-if [ ! -e coolingtables ]
+if [ ! -e UV_dust1_CR1_G1_shield1.hdf5 ]
 then
-    echo "Fetching EAGLE cooling tables..."
-    ../getEagleCoolingTable.sh
+    echo "Fetching EAGLE-XL cooling tables..."
+    ../getPS2020CoolingTables.sh
 fi
 
 if [ ! -e photometry ]
diff --git a/examples/EAGLE_ICs/EAGLE_12/eagle_12.yml b/examples/EAGLE_ICs/EAGLE_12/eagle_12.yml
index 2e33e031e1c56423f55760d32c5a39e1444de312..0742866a4ab5be0367a6adbc99aed25ebccf1fe3 100644
--- a/examples/EAGLE_ICs/EAGLE_12/eagle_12.yml
+++ b/examples/EAGLE_ICs/EAGLE_12/eagle_12.yml
@@ -76,6 +76,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells:   8
diff --git a/examples/EAGLE_ICs/EAGLE_12/run.sh b/examples/EAGLE_ICs/EAGLE_12/run.sh
index 0140f598a2697209756731934121186b946fcdfa..f53b975f1319a58cb7787f04e7bf21b817ce43ea 100755
--- a/examples/EAGLE_ICs/EAGLE_12/run.sh
+++ b/examples/EAGLE_ICs/EAGLE_12/run.sh
@@ -14,10 +14,10 @@ then
     ../getEagleYieldTable.sh
 fi
 
-if [ ! -e coolingtables ]
+if [ ! -e UV_dust1_CR1_G1_shield1.hdf5 ]
 then
-    echo "Fetching EAGLE cooling tables..."
-    ../getEagleCoolingTable.sh
+    echo "Fetching EAGLE-XL cooling tables..."
+    ../getPS2020CoolingTables.sh
 fi
 
 if [ ! -e photometry ]
diff --git a/examples/EAGLE_ICs/EAGLE_25/eagle_25.yml b/examples/EAGLE_ICs/EAGLE_25/eagle_25.yml
index 77355071563c56be04be67fcb617613ef6cc44af..e45a3dd456d7309d804d51e263d5cf5b79b832b4 100644
--- a/examples/EAGLE_ICs/EAGLE_25/eagle_25.yml
+++ b/examples/EAGLE_ICs/EAGLE_25/eagle_25.yml
@@ -76,6 +76,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells:   16
diff --git a/examples/EAGLE_ICs/EAGLE_25/run.sh b/examples/EAGLE_ICs/EAGLE_25/run.sh
index 04635c149fa19181386880f79034f413c96f8848..8d18b4519ef37bafd1bbd1b1ce6a2413cded07ad 100755
--- a/examples/EAGLE_ICs/EAGLE_25/run.sh
+++ b/examples/EAGLE_ICs/EAGLE_25/run.sh
@@ -14,10 +14,10 @@ then
     ../getEagleYieldTable.sh
 fi
 
-if [ ! -e coolingtables ]
+if [ ! -e UV_dust1_CR1_G1_shield1.hdf5 ]
 then
-    echo "Fetching EAGLE cooling tables..."
-    ../getEagleCoolingTable.sh
+    echo "Fetching EAGLE-XL cooling tables..."
+    ../getPS2020CoolingTables.sh
 fi
 
 if [ ! -e photometry ]
diff --git a/examples/EAGLE_ICs/EAGLE_25_low_res/eagle_25.yml b/examples/EAGLE_ICs/EAGLE_25_low_res/eagle_25.yml
index 743fa0d5dede782ecb1445d861d60f6190171037..cdb4d98e80c243ff6b40407ca20eace355d0ae7f 100644
--- a/examples/EAGLE_ICs/EAGLE_25_low_res/eagle_25.yml
+++ b/examples/EAGLE_ICs/EAGLE_25_low_res/eagle_25.yml
@@ -76,6 +76,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells:   16
diff --git a/examples/EAGLE_ICs/EAGLE_25_low_res/run.sh b/examples/EAGLE_ICs/EAGLE_25_low_res/run.sh
index 200fab538cb38515c0df665c9191139fd9cd9960..36a95333b99e0d4859b1618152f224b0489c9bf4 100755
--- a/examples/EAGLE_ICs/EAGLE_25_low_res/run.sh
+++ b/examples/EAGLE_ICs/EAGLE_25_low_res/run.sh
@@ -14,10 +14,10 @@ then
     ../getEagleYieldTable.sh
 fi
 
-if [ ! -e coolingtables ]
+if [ ! -e UV_dust1_CR1_G1_shield1.hdf5 ]
 then
-    echo "Fetching EAGLE cooling tables..."
-    ../getEagleCoolingTable.sh
+    echo "Fetching EAGLE-XL cooling tables..."
+    ../getPS2020CoolingTables.sh
 fi
 
 if [ ! -e photometry ]
diff --git a/examples/EAGLE_ICs/EAGLE_50/eagle_50.yml b/examples/EAGLE_ICs/EAGLE_50/eagle_50.yml
index 76f454627764c12224c1724892a5d5b05cf65d32..29ca9fe3909d9ff3479af7c876b7bdd65ca678ac 100644
--- a/examples/EAGLE_ICs/EAGLE_50/eagle_50.yml
+++ b/examples/EAGLE_ICs/EAGLE_50/eagle_50.yml
@@ -76,6 +76,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells:   32
diff --git a/examples/EAGLE_ICs/EAGLE_50/run.sh b/examples/EAGLE_ICs/EAGLE_50/run.sh
index 026019b44d05d8d8272b49c5baa2d7c2b8f4ef6c..a48a559444d0e42f64b027793f42c11d464695c7 100755
--- a/examples/EAGLE_ICs/EAGLE_50/run.sh
+++ b/examples/EAGLE_ICs/EAGLE_50/run.sh
@@ -14,10 +14,10 @@ then
     ../getEagleYieldTable.sh
 fi
 
-if [ ! -e coolingtables ]
+if [ ! -e UV_dust1_CR1_G1_shield1.hdf5 ]
 then
-    echo "Fetching EAGLE cooling tables..."
-    ../getEagleCoolingTable.sh
+    echo "Fetching EAGLE-XL cooling tables..."
+    ../getPS2020CoolingTables.sh
 fi
 
 if [ ! -e photometry ]
diff --git a/examples/EAGLE_ICs/EAGLE_50_low_res/eagle_50.yml b/examples/EAGLE_ICs/EAGLE_50_low_res/eagle_50.yml
index 97af6a1f60019d1ebd5fa6e2670b36ade68ef572..a14bb9e5ac51729afa55f3c0217c1ebdd1c6fedf 100644
--- a/examples/EAGLE_ICs/EAGLE_50_low_res/eagle_50.yml
+++ b/examples/EAGLE_ICs/EAGLE_50_low_res/eagle_50.yml
@@ -76,6 +76,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells:   16
diff --git a/examples/EAGLE_ICs/EAGLE_50_low_res/run.sh b/examples/EAGLE_ICs/EAGLE_50_low_res/run.sh
index 83f388aeb01c968aa4aac2cd50b9065993cee96b..235c4fdb1e589635ff053a22a577446f61c9e619 100755
--- a/examples/EAGLE_ICs/EAGLE_50_low_res/run.sh
+++ b/examples/EAGLE_ICs/EAGLE_50_low_res/run.sh
@@ -14,10 +14,10 @@ then
     ../getEagleYieldTable.sh
 fi
 
-if [ ! -e coolingtables ]
+if [ ! -e UV_dust1_CR1_G1_shield1.hdf5 ]
 then
-    echo "Fetching EAGLE cooling tables..."
-    ../getEagleCoolingTable.sh
+    echo "Fetching EAGLE-XL cooling tables..."
+    ../getPS2020CoolingTables.sh
 fi
 
 if [ ! -e photometry ]
diff --git a/examples/EAGLE_ICs/EAGLE_6/eagle_6.yml b/examples/EAGLE_ICs/EAGLE_6/eagle_6.yml
index 4b8446c30d5401f31c9bc3650b9389fb47fc7c14..1a22107d119eb3389d8898d82c28c6862966a1bb 100644
--- a/examples/EAGLE_ICs/EAGLE_6/eagle_6.yml
+++ b/examples/EAGLE_ICs/EAGLE_6/eagle_6.yml
@@ -76,6 +76,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells:   8
diff --git a/examples/EAGLE_ICs/EAGLE_6/run.sh b/examples/EAGLE_ICs/EAGLE_6/run.sh
index aa1064d380a97740c918ee961a7316822b5b8f43..564eb67b80ffb741bd1c1a8dd84be80c5c2a843b 100755
--- a/examples/EAGLE_ICs/EAGLE_6/run.sh
+++ b/examples/EAGLE_ICs/EAGLE_6/run.sh
@@ -14,10 +14,10 @@ then
     ../getEagleYieldTable.sh
 fi
 
-if [ ! -e coolingtables ]
+if [ ! -e UV_dust1_CR1_G1_shield1.hdf5 ]
 then
-    echo "Fetching EAGLE cooling tables..."
-    ../getEagleCoolingTable.sh
+    echo "Fetching EAGLE-XL cooling tables..."
+    ../getPS2020CoolingTables.sh
 fi
 
 if [ ! -e photometry ]
diff --git a/examples/EAGLE_ICs/README b/examples/EAGLE_ICs/README
index 0ef04690337001875f8a81cf7bf86e759ad6336a..23cc0ad77cc57425b28f8444e96a100d984107f9 100644
--- a/examples/EAGLE_ICs/README
+++ b/examples/EAGLE_ICs/README
@@ -99,7 +99,8 @@ run the EAGLE model. Plotting scripts are also provided
 for basic quantities.
 
 To use the cooling model based on the Wiersma+09 tables, replace
-EAGLE-XL by EAGLE in the configuration command line.
+EAGLE-XL by EAGLE in the configuration command line. The tables
+can then be loaded using the getEagleCoolingTable.sh script.
 
 VELOCIraptor can be run on the output. The code is compiled
 using
diff --git a/examples/EAGLE_low_z/EAGLE_12/eagle_12.yml b/examples/EAGLE_low_z/EAGLE_12/eagle_12.yml
index 96fe8efe01082f79a676110c5ae6e59e25f06dc1..032b8b6e7d0c1a5ebc07c71c905ac7bb0787ddac 100644
--- a/examples/EAGLE_low_z/EAGLE_12/eagle_12.yml
+++ b/examples/EAGLE_low_z/EAGLE_12/eagle_12.yml
@@ -83,6 +83,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.5e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.91        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.005       # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 # Parameters related to the initial conditions
 InitialConditions:
diff --git a/examples/EAGLE_low_z/EAGLE_25/eagle_25.yml b/examples/EAGLE_low_z/EAGLE_25/eagle_25.yml
index 8ba338df28bffa5775d509fd5422913a8870e1d3..729be0c4ad628d727168cefc5a8e26e02844cac7 100644
--- a/examples/EAGLE_low_z/EAGLE_25/eagle_25.yml
+++ b/examples/EAGLE_low_z/EAGLE_25/eagle_25.yml
@@ -91,6 +91,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.5e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.91        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.005       # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 # Parameters related to the initial conditions
 InitialConditions:
diff --git a/examples/EAGLE_low_z/EAGLE_50/eagle_50.yml b/examples/EAGLE_low_z/EAGLE_50/eagle_50.yml
index e59d92389a6bfd526495ac70a008492b4d8ba8f2..e0e847d9c8e131a5211eb8fe7cb3941127ea3eb7 100644
--- a/examples/EAGLE_low_z/EAGLE_50/eagle_50.yml
+++ b/examples/EAGLE_low_z/EAGLE_50/eagle_50.yml
@@ -82,6 +82,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.5e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.91        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.005       # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 # Parameters related to the initial conditions
 InitialConditions:
diff --git a/examples/EAGLE_low_z/EAGLE_6/check_fof.py b/examples/EAGLE_low_z/EAGLE_6/check_fof.py
new file mode 100644
index 0000000000000000000000000000000000000000..4859af47497cebd50c46af3c8e441ff5f5af9829
--- /dev/null
+++ b/examples/EAGLE_low_z/EAGLE_6/check_fof.py
@@ -0,0 +1,472 @@
+import numpy as np
+import h5py as h5
+from tqdm import tqdm
+from numba import jit, prange
+
+snapname = "eagle_0000/eagle_0000.hdf5"
+fofname = "fof_output_0000/fof_output_0000.0.hdf5"
+# snapname = "eagle_0000.hdf5"
+# fofname = "fof_output_0000.hdf5"
+
+######################################################
+
+snap = h5.File(snapname, "r")
+
+nogrp_grp_id = int(snap["/Parameters"].attrs.get("FOF:group_id_default"))
+
+pos_gas = snap["/PartType0/Coordinates"][:, :]
+ids_gas = snap["/PartType0/ParticleIDs"][:]
+grp_gas = snap["/PartType0/FOFGroupIDs"][:]
+mass_gas = snap["/PartType0/Masses"][:]
+
+pos_DM = snap["/PartType1/Coordinates"][:, :]
+ids_DM = snap["/PartType1/ParticleIDs"][:]
+grp_DM = snap["/PartType1/FOFGroupIDs"][:]
+mass_DM = snap["/PartType1/Masses"][:]
+
+pos_star = snap["/PartType4/Coordinates"][:, :]
+ids_star = snap["/PartType4/ParticleIDs"][:]
+grp_star = snap["/PartType4/FOFGroupIDs"][:]
+mass_star = snap["/PartType4/Masses"][:]
+
+####################################################
+
+fof = h5.File(fofname, "r")
+num_files = fof["/Header/"].attrs["NumFilesPerSnapshot"][0]
+num_groups = fof["/Header/"].attrs["NumGroups_Total"][0]
+fof.close()
+
+fof_grp = np.zeros(num_groups, dtype=np.int32)
+fof_size = np.zeros(num_groups, dtype=np.int32)
+fof_mass = np.zeros(num_groups)
+
+# Read the distributed catalog
+offset = 0
+for i in range(num_files):
+
+    my_filename = fofname[:-6]
+    my_filename = my_filename + str(i) + ".hdf5"
+    fof = h5.File(my_filename, "r")
+
+    my_fof_grp = fof["/Groups/GroupIDs"][:]
+    my_fof_size = fof["/Groups/Sizes"][:]
+    my_fof_mass = fof["/Groups/Masses"][:]
+
+    num_this_file = fof["/Header"].attrs["NumGroups_ThisFile"][0]
+    fof.close()
+
+    fof_grp[offset : offset + num_this_file] = my_fof_grp
+    fof_size[offset : offset + num_this_file] = my_fof_size
+    fof_mass[offset : offset + num_this_file] = my_fof_mass
+
+    offset += num_this_file
+
+####################################################
+
+boxsize = snap["/Header"].attrs.get("BoxSize")[0]
+N_DM = snap["/Header"].attrs.get("NumPart_ThisFile")[1]
+
+l = 0.2 * boxsize / float(N_DM) ** (1.0 / 3.0)
+
+print("Checking snapshot :", snapname)
+print("Checking catalogue:", fofname)
+print("L:", boxsize)
+print("N_DM:", N_DM)
+print("Linking length:", l)
+print("")
+
+####################################################
+
+
+@jit(nopython=True, parallel=True, fastmath=True)
+def nearest(dx, L=boxsize):
+    mask1 = dx > 0.5 * L
+    mask2 = dx < -0.5 * L
+    if np.sum(mask1):
+        dx[mask1] = dx[mask1] - L
+    if np.sum(mask2):
+        dx[mask2] = dx[mask2] + L
+    return dx
+
+
+####################################################
+
+# Verify the content of the catalog
+num_groups = np.size(fof_grp)
+print("Catalog has", num_groups, "groups")
+
+
+def check_fof_size(i):
+    my_grp = fof_grp[i]
+    my_size = fof_size[i]
+
+    mask_gas = grp_gas == my_grp
+    mask_DM = grp_DM == my_grp
+    mask_star = grp_star == my_grp
+
+    total = np.sum(mask_gas) + np.sum(mask_DM) + np.sum(mask_star)
+
+    if total != my_size:
+        print(
+            "Grp",
+            my_grp,
+            "has size=",
+            my_size,
+            "but",
+            total,
+            "particles in the snapshot",
+        )
+        exit()
+
+
+for i in tqdm(range(num_groups)):
+    check_fof_size(i)
+
+print("All group sizes match the particles")
+####################################################
+
+# Verify group masses
+num_groups = np.size(fof_grp)
+print("Catalog has", num_groups, "groups")
+
+
+def check_fof_masses(i):
+    my_grp = fof_grp[i]
+    my_mass = fof_mass[i]
+
+    mask_gas = grp_gas == my_grp
+    mask_DM = grp_DM == my_grp
+    mask_star = grp_star == my_grp
+
+    total = (
+        np.sum(mass_gas[mask_gas])
+        + np.sum(mass_DM[mask_DM])
+        + np.sum(mass_star[mask_star])
+    )
+
+    ratio = total / my_mass
+
+    if ratio > 1.0001 or ratio < 0.9999:
+        print(
+            "Grp",
+            my_grp,
+            "has mass=",
+            my_mass,
+            "but particles in the snapshot have mass",
+            total,
+        )
+        exit()
+
+
+for i in tqdm(range(num_groups)):
+    check_fof_masses(i)
+
+print("All group masses match the particles")
+####################################################
+
+# Test the stand-alone stars
+mask = grp_star == nogrp_grp_id
+num_stars = np.sum(mask)
+print("Found %d stars not in groups" % num_stars)
+my_pos_star = pos_star[mask, :]
+my_ids_star = ids_star[mask]
+my_grp_star = grp_star[mask]
+my_pos_DM = pos_DM[:, :]
+my_ids_DM = ids_DM[:]
+my_grp_DM = grp_DM[:]
+
+# @jit(nopython=True, parallel=True, fastmath=True)
+def check_stand_alone_star(i):
+    pos = my_pos_star[i, :]
+    grp = my_grp_star[i]
+
+    dx = pos[0] - my_pos_DM[:, 0]
+    dy = pos[1] - my_pos_DM[:, 1]
+    dz = pos[2] - my_pos_DM[:, 2]
+
+    dx = nearest(dx)
+    dy = nearest(dy)
+    dz = nearest(dz)
+
+    # Identify the nearest DM particle
+    r2 = dx ** 2 + dy ** 2 + dz ** 2
+    select = np.argmin(r2)
+
+    # If the nearest DM particle is in a group --> mistake
+    target_grp = my_grp_DM[select]
+    if target_grp != nogrp_grp_id and r2[select] < l * l:
+        print("Found a star without group whose nearest DM particle is in a group!")
+        print("Star: id=", my_ids_star[i], "pos=", pos, "grp=", grp)
+        print(
+            "DM: id=",
+            my_ids_DM[select],
+            "pos=",
+            my_pos_DM[select],
+            "grp=",
+            my_grp_DM[select],
+        )
+        print("r=", np.sqrt(r2[select]))
+        # exit()
+
+
+for i in tqdm(range(num_stars)):
+    check_stand_alone_star(i)
+
+print("All stand-alone stars OK!")
+
+####################################################
+
+# Test the stars in groups
+mask = grp_star != nogrp_grp_id
+num_stars = np.sum(mask)
+print("Found %d stars in groups" % num_stars)
+my_pos_star = pos_star[mask, :]
+my_ids_star = ids_star[mask]
+my_grp_star = grp_star[mask]
+my_pos_DM = pos_DM[:, :]
+my_ids_DM = ids_DM[:]
+my_grp_DM = grp_DM[:]
+
+
+@jit(nopython=True, parallel=True, fastmath=True)
+def test_stars_in_group(i):
+    pos = my_pos_star[i, :]
+    grp = my_grp_star[i]
+
+    dx = pos[0] - my_pos_DM[:, 0]
+    dy = pos[1] - my_pos_DM[:, 1]
+    dz = pos[2] - my_pos_DM[:, 2]
+
+    dx = nearest(dx)
+    dy = nearest(dy)
+    dz = nearest(dz)
+
+    # Identify the nearest DM particle
+    r2 = dx ** 2 + dy ** 2 + dz ** 2
+    select = np.argmin(r2)
+
+    # If the nearest DM particle is not in the same group --> mistake
+    target_grp = my_grp_DM[select]
+    if target_grp != grp and r2[select] < l * l:
+        print(
+            "Found a star in a group whose nearest DM particle is in a different group!"
+        )
+        print("Star: id=", my_ids_star[i], "pos=", pos, "grp=", grp)
+        print(
+            "DM: id=", my_ids_DM[select], "pos=", my_pos_DM[select], "grp=", target_grp
+        )
+        print("r=", np.sqrt(r2[select]))
+        # exit()
+
+
+for i in tqdm(range(num_stars)):
+    test_stars_in_group(i)
+
+print("All stars in groups OK!")
+
+####################################################
+
+# Test the stand-alone gas
+mask = grp_gas == nogrp_grp_id
+num_gas = np.sum(mask)
+print("Found %d gas not in groups" % num_gas)
+my_pos_gas = pos_gas[mask, :]
+my_ids_gas = ids_gas[mask]
+my_grp_gas = grp_gas[mask]
+my_pos_DM = pos_DM[:, :]
+my_ids_DM = ids_DM[:]
+my_grp_DM = grp_DM[:]
+
+
+@jit(nopython=True, parallel=True, fastmath=True)
+def test_stand_alone_gas(i):
+    pos = my_pos_gas[i, :]
+    grp = my_grp_gas[i]
+
+    dx = pos[0] - my_pos_DM[:, 0]
+    dy = pos[1] - my_pos_DM[:, 1]
+    dz = pos[2] - my_pos_DM[:, 2]
+
+    dx = nearest(dx)
+    dy = nearest(dy)
+    dz = nearest(dz)
+
+    # Identify the nearest DM particle
+    r2 = dx ** 2 + dy ** 2 + dz ** 2
+    select = np.argmin(r2)
+
+    # If the nearest DM particle is in a group --> mistake
+    target_grp = my_grp_DM[select]
+    if target_grp != nogrp_grp_id and r2[select] < l * l:
+        print("Found a gas without group whose nearest DM particle is in a group!")
+        print("Gas: id=", my_ids_gas[i], "pos=", pos, "grp=", grp)
+        print(
+            "DM: id=",
+            my_ids_DM[select],
+            "pos=",
+            my_pos_DM[select],
+            "grp=",
+            my_grp_DM[select],
+        )
+        print("r=", np.sqrt(r2[select]))
+        # exit()
+
+
+for i in tqdm(range(num_gas)):
+    test_stand_alone_gas(i)
+
+print("All stand-alone gas OK!")
+
+####################################################
+
+# Test the gas in groups
+mask = grp_gas != nogrp_grp_id
+num_gas = np.sum(mask)
+print("Found %d gas in groups" % num_gas)
+my_pos_gas = pos_gas[mask, :]
+my_ids_gas = ids_gas[mask]
+my_grp_gas = grp_gas[mask]
+my_pos_DM = pos_DM[:, :]
+my_ids_DM = ids_DM[:]
+my_grp_DM = grp_DM[:]
+
+
+@jit(nopython=True, parallel=True, fastmath=True)
+def test_gas_in_groups(i):
+    pos = my_pos_gas[i, :]
+    grp = my_grp_gas[i]
+
+    dx = pos[0] - my_pos_DM[:, 0]
+    dy = pos[1] - my_pos_DM[:, 1]
+    dz = pos[2] - my_pos_DM[:, 2]
+
+    dx = nearest(dx)
+    dy = nearest(dy)
+    dz = nearest(dz)
+
+    # Identify the nearest DM particle
+    r2 = dx ** 2 + dy ** 2 + dz ** 2
+    select = np.argmin(r2)
+
+    # If the nearest DM particle is not in the same group --> mistake
+    target_grp = my_grp_DM[select]
+    if target_grp != grp and r2[select] < l * l:
+        print(
+            "Found a gas in a group whose nearest DM particle is in a different group!"
+        )
+        print("Gas: id=", my_ids_gas[i], "pos=", pos, "grp=", grp)
+        print(
+            "DM: id=", my_ids_DM[select], "pos=", my_pos_DM[select], "grp=", target_grp
+        )
+        print("r=", np.sqrt(r2[select]))
+        # exit()
+
+
+for i in tqdm(range(num_gas)):
+    test_gas_in_groups(i)
+
+print("All gas in groups OK!")
+
+####################################################
+
+# Test the stand-alone DM
+mask = grp_DM == nogrp_grp_id
+num_DM = np.sum(mask)
+print("Found %d DM not in groups" % num_DM)
+my_pos_DM = pos_DM[mask, :]
+my_ids_DM = ids_DM[mask]
+my_grp_DM = grp_DM[mask]
+
+
+@jit(nopython=True, parallel=True, fastmath=True)
+def test_stand_alone_DM(i):
+    pos = my_pos_DM[i, :]
+    grp = my_grp_DM[i]
+
+    dx = pos[0] - pos_DM[:, 0]
+    dy = pos[1] - pos_DM[:, 1]
+    dz = pos[2] - pos_DM[:, 2]
+
+    dx = nearest(dx)
+    dy = nearest(dy)
+    dz = nearest(dz)
+
+    # Identify the nearest DM particle
+    r2 = dx ** 2 + dy ** 2 + dz ** 2
+    mask = np.logical_and(r2 < l * l, r2 > 0.0)
+
+    # If the nearest DM particle is in a group --> mistake
+    if not np.all(grp_DM[mask] == nogrp_grp_id):
+        print("Found a DM without group with some DM particle within l in a group!")
+        print("DM:    id=", my_ids_DM[i], "pos=", pos, "grp=", grp)
+        for j in range(np.sum(mask)):
+            if grp_DM[mask][j] != nogrp_grp_id:
+                print(
+                    "Other: id=",
+                    ids_DM[mask][j],
+                    "pos=",
+                    pos_DM[mask, :][j, :],
+                    "grp=",
+                    grp_DM[mask][j],
+                    "r=",
+                    np.sqrt(r2[mask][j]),
+                )
+
+
+for i in tqdm(range(num_DM)):
+    test_stand_alone_DM(i)
+
+print("All stand-alone DM OK!")
+
+####################################################
+
+# Test the DM in groups
+mask = grp_DM != nogrp_grp_id
+num_DM = np.sum(mask)
+print("Found %d DM in groups" % num_DM)
+my_pos_DM = pos_DM[mask, :]
+my_ids_DM = ids_DM[mask]
+my_grp_DM = grp_DM[mask]
+
+
+@jit(nopython=True, parallel=True, fastmath=True)
+def test_DM_in_groups(i):
+    pos = my_pos_DM[i, :]
+    grp = my_grp_DM[i]
+
+    dx = pos[0] - pos_DM[:, 0]
+    dy = pos[1] - pos_DM[:, 1]
+    dz = pos[2] - pos_DM[:, 2]
+
+    dx = nearest(dx)
+    dy = nearest(dy)
+    dz = nearest(dz)
+
+    # Identify the nearest DM particle
+    r2 = dx ** 2 + dy ** 2 + dz ** 2
+    mask = r2 < l * l
+
+    # If the nearest DM particle is not in the same group --> mistake
+    if not np.all(grp_DM[mask] == grp):
+        print(
+            "Found a DM in a group whose DM particles within l are in a different group!"
+        )
+        print("DM:    id=", my_ids_DM[i], "pos=", pos, "grp=", grp)
+        for j in range(np.sum(mask)):
+            if grp_DM[mask][j] != grp:
+                print(
+                    "Other: id=",
+                    ids_DM[mask][j],
+                    "pos=",
+                    pos_DM[mask, :][j, :],
+                    "grp=",
+                    grp_DM[mask][j],
+                    "r=",
+                    np.sqrt(r2[mask][j]),
+                )
+
+
+for i in tqdm(range(num_DM)):
+    test_DM_in_groups(i)
+
+print("All DM in groups OK!")
diff --git a/examples/EAGLE_low_z/EAGLE_6/eagle_6.yml b/examples/EAGLE_low_z/EAGLE_6/eagle_6.yml
index a77e1bb827e8fbe9ad02e92e52bcfda1a25df28d..cf111232f1c20037dad7322e5ce64fad0069fb76 100644
--- a/examples/EAGLE_low_z/EAGLE_6/eagle_6.yml
+++ b/examples/EAGLE_low_z/EAGLE_6/eagle_6.yml
@@ -93,7 +93,9 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.5e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.91        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.005       # Scale-factor ratio between consecutive FoF black hole seeding calls.
-
+  linking_types:  [0, 1, 0, 0, 0, 0, 0]
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]
+  
 # Parameters related to the initial conditions
 InitialConditions:
   file_name:  ./EAGLE_ICs_6.hdf5     # The file to read
diff --git a/examples/GEAR/AgoraCosmo/agora_cosmo.yml b/examples/GEAR/AgoraCosmo/agora_cosmo.yml
index eab8e9b2a829b357a17570b59eecc9b96b2a76e6..9ceadf9a70890a61855b621c282860bee2838d1e 100644
--- a/examples/GEAR/AgoraCosmo/agora_cosmo.yml
+++ b/examples/GEAR/AgoraCosmo/agora_cosmo.yml
@@ -86,14 +86,15 @@ GrackleCooling:
   max_steps: 1000
   convergence_limit: 1e-2
   thermal_time_myr: 5
+  maximal_density_Hpcm3: -1 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
 
 
 GEARStarFormation:
   star_formation_efficiency: 0.01   # star formation efficiency (c_*)
-  maximal_temperature:  3e4         # Upper limit to the temperature of a star forming particle
+  maximal_temperature_K:     3e4    # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   1e-6   # Density threshold (in addition to the pressure floor) in Hydrogen atoms/cm3
   n_stars_per_particle: 1
   min_mass_frac: 0.5
-  density_threshold:   1e-30       # Density threashold (in addition to the pressure floor) in g/cm3
 
 GEARPressureFloor:
   jeans_factor: 8.75
diff --git a/examples/GEAR/AgoraDisk/agora_disk.yml b/examples/GEAR/AgoraDisk/agora_disk.yml
index 2a940fe313647fb8e843b7697a761971fadb6752..c43462371f5d99a2242a4f2a603aaf4d637a7b35 100644
--- a/examples/GEAR/AgoraDisk/agora_disk.yml
+++ b/examples/GEAR/AgoraDisk/agora_disk.yml
@@ -83,14 +83,15 @@ GrackleCooling:
   max_steps: 1000
   convergence_limit: 1e-2
   thermal_time_myr: 0
+  maximal_density_Hpcm3: -1 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
 
 
 GEARStarFormation:
   star_formation_efficiency: 0.01   # star formation efficiency (c_*)
-  maximal_temperature:  1e10         # Upper limit to the temperature of a star forming particle
+  maximal_temperature_K:     1e10   # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   10     # Density threshold in Hydrogen atoms/cm3
   n_stars_per_particle: 1
   min_mass_frac: 0.5
-  density_threshold:   1.67e-23   # Density threashold in g/cm3
 
 
 GEARPressureFloor:
diff --git a/examples/GEAR/ZoomIn/zoom_in.yml b/examples/GEAR/ZoomIn/zoom_in.yml
index bced8bfd87fcbbf96df6f838d84642c4837731e3..d74c248876a615b47e2bd65655a39baf07103cbf 100644
--- a/examples/GEAR/ZoomIn/zoom_in.yml
+++ b/examples/GEAR/ZoomIn/zoom_in.yml
@@ -91,12 +91,13 @@ GrackleCooling:
   max_steps: 1000
   convergence_limit: 1e-2
   thermal_time_myr: 5
+  maximal_density_Hpcm3: -1 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
 
 
 GEARStarFormation:
   star_formation_efficiency: 0.01   # star formation efficiency (c_*)
-  maximal_temperature:  3e4         # Upper limit to the temperature of a star forming particle
-  density_threshold:   1.67e-25    # Density threashold in g/cm3
+  maximal_temperature_K:     3e4    # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   0.1    # Density threshold in Hydrogen atoms/cm3
   n_stars_per_particle: 4
   min_mass_frac: 0.5
 
diff --git a/examples/GravityTests/Hernquist_circularorbit/makeIC.py b/examples/GravityTests/Hernquist_circularorbit/makeIC.py
index 474450f0e23704bfc43730872a978107f28704e9..4c69e7c966f95ca90bce20754c95fce47c45213e 100755
--- a/examples/GravityTests/Hernquist_circularorbit/makeIC.py
+++ b/examples/GravityTests/Hernquist_circularorbit/makeIC.py
@@ -16,9 +16,6 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ################################################################################
-from galpy.potential import NFWPotential
-from galpy.orbit import Orbit
-from galpy.util import bovy_conversion
 import numpy as np
 import matplotlib.pyplot as plt
 from astropy import units
@@ -27,7 +24,7 @@ import h5py as h5
 C = 8.0
 M_200 = 2.0
 N_PARTICLES = 3
-print("Initial conditions written to 'test_nfw.hdf5'")
+print("Initial conditions written to 'circularorbitshernquist.hdf5'")
 
 pos = np.zeros((3, 3))
 pos[0, 2] = 50.0
diff --git a/examples/GravityTests/MWPotential2014_circularorbit/README b/examples/GravityTests/MWPotential2014_circularorbit/README
new file mode 100644
index 0000000000000000000000000000000000000000..056a242ad78337e3ea883351b265782261053232
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_circularorbit/README
@@ -0,0 +1,24 @@
+# Context
+
+This example tests the MWPotential2014, a reference Milky-Way potential implemented on galpy (https://docs.galpy.org). 
+Details of the parameters can be found in galpy's paper, p.7:
+galpy: A Python Library for Galactic Dynamics, Jo Bovy (2015), Astrophys. J. Supp., 216, 29 (<arXiv/1412.3451>)
+
+# How to run this example
+
+In a terminal at the root of the "swiftsim" directory, type:
+
+`./autogen.sh`
+
+Then, configure swift to take into account external potentials. Type:
+
+`./configure --with-ext-potential=MWPotential2014`
+
+Feel free to adapt other configurations parameters depending on your system. Then, build the code by typing:
+
+`make`
+
+Finally, to run this example, open a terminal in the present directory and type:
+
+`./run.sh`
+
diff --git a/examples/GravityTests/MWPotential2014_circularorbit/makeIC.py b/examples/GravityTests/MWPotential2014_circularorbit/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..1f633af58e83bedc550d54974d125bac080a695f
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_circularorbit/makeIC.py
@@ -0,0 +1,75 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+import numpy as np
+import h5py as h5
+
+box_size = 1000.0
+N_PARTICLES = 3
+print("Initial conditions written to 'circular_orbits_MW.hdf5'")
+
+pos = np.zeros((3, 3))
+pos[0, 2] = 5.0
+pos[1, 1] = 5.0
+pos[2, 0] = 30.0
+pos += np.array(
+    [box_size / 2, box_size / 2, box_size / 2]
+)  # shifts the particles to the center of the box
+vel = np.zeros((3, 3))
+vel[0, 0] = 198.5424557586175
+vel[1, 0] = 225.55900735974072
+vel[2, 1] = 188.5272441374569
+
+ids = np.array([1.0, 2.0, 3.0])
+mass = np.array([1.0, 1.0, 1.0]) * 1e-10
+
+# File
+file = h5.File("circular_orbits_MW.hdf5", "w")
+
+# Units
+grp = file.create_group("/Units")
+grp.attrs["Unit length in cgs (U_L)"] = 3.086e21
+grp.attrs["Unit mass in cgs (U_M)"] = 1.98848e43
+grp.attrs["Unit time in cgs (U_t)"] = 3.086e16
+grp.attrs["Unit current in cgs (U_I)"] = 1.0
+grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+# Header
+grp = file.create_group("/Header")
+grp.attrs["BoxSize"] = box_size
+grp.attrs["NumPart_Total"] = [0, N_PARTICLES, 0, 0, 0, 0]
+grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_ThisFile"] = [0, N_PARTICLES, 0, 0, 0, 0]
+grp.attrs["Time"] = 0.0
+grp.attrs["NumFilesPerSnapshot"] = 1
+grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["Dimension"] = 3
+
+# Runtime parameters
+grp = file.create_group("/RuntimePars")
+grp.attrs["PeriodicBoundariesOn"] = 1
+
+# Particle group
+grp1 = file.create_group("/PartType1")
+ds = grp1.create_dataset("Velocities", (N_PARTICLES, 3), "f", data=vel)
+ds = grp1.create_dataset("Masses", (N_PARTICLES,), "f", data=mass)
+ds = grp1.create_dataset("ParticleIDs", (N_PARTICLES,), "L", data=ids)
+ds = grp1.create_dataset("Coordinates", (N_PARTICLES, 3), "d", data=pos)
+
+file.close()
diff --git a/examples/GravityTests/MWPotential2014_circularorbit/makePlots.py b/examples/GravityTests/MWPotential2014_circularorbit/makePlots.py
new file mode 100755
index 0000000000000000000000000000000000000000..e5308720de2dbe9b0dfd02bde2b0c3ed8ab3b436
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_circularorbit/makePlots.py
@@ -0,0 +1,205 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+import numpy as np
+import h5py
+import matplotlib.pyplot as plt
+
+
+#%%Functions
+
+
+def get_positions_and_time(N_snapshots, N_part, output_dir, boxsize):
+    xx = np.zeros((N_part, N_snapshots))
+    yy = np.zeros((N_part, N_snapshots))
+    zz = np.zeros((N_part, N_snapshots))
+    time = np.zeros(N_snapshots)
+    pot = np.zeros((N_part, N_snapshots))
+    for i in range(0, N_snapshots):
+        Data = h5py.File(output_dir + "/output_%04d.hdf5" % i, "r")
+        header = Data["Header"]
+        time[i] = header.attrs["Time"][0]
+        particles = Data["PartType1"]
+        positions = particles["Coordinates"]
+        xx[:, i] = positions[:, 0] - boxsize / 2.0
+        yy[:, i] = positions[:, 1] - boxsize / 2.0
+        zz[:, i] = positions[:, 2] - boxsize / 2.0
+        pot[:, i] = particles["Potentials"][:]
+    return xx, yy, zz, time, pot
+
+
+def plot_orbits(x, y, z, color, save_fig_name_suffix):
+    # Plots the orbits
+    fig, ax = plt.subplots(nrows=1, ncols=3, num=1, figsize=(12, 4.1))
+    fig.suptitle("Orbits", fontsize=15)
+    ax[0].clear()
+    ax[1].clear()
+
+    for i in range(0, N_part):
+        ax[0].plot(x[i, :], y[i, :], color[i])
+
+    ax[0].set_aspect("equal", "box")
+    ax[0].set_xlim([-35, 35])
+    ax[0].set_ylim([-35, 35])
+    ax[0].set_ylabel("y (kpc)")
+    ax[0].set_xlabel("x (kpc)")
+
+    for i in range(0, N_part):
+        ax[1].plot(x[i, :], z[i, :], col[i])
+
+    ax[1].set_aspect("equal", "box")
+    ax[1].set_xlim([-35, 35])
+    ax[1].set_ylim([-35, 35])
+    ax[1].set_ylabel("z (kpc)")
+    ax[1].set_xlabel("x (kpc)")
+    ax[1].legend(
+        [
+            "Particule 1, $R = 5$ kpc",
+            "Particule 2, $R = 5$ kpc",
+            "Particule 3, $R = 30$ kpc",
+        ]
+    )
+
+    for i in range(0, N_part):
+        ax[2].plot(y[i, :], z[i, :], col[i])
+
+    ax[2].set_aspect("equal", "box")
+    ax[2].set_xlim([-35, 35])
+    ax[2].set_ylim([-35, 35])
+    ax[2].set_ylabel("z (kpc)")
+    ax[2].set_xlabel("y (kpc)")
+    plt.tight_layout()
+    plt.savefig("circular_orbits" + save_fig_name_suffix + ".png", bbox_inches="tight")
+    plt.close()
+
+
+def plot_deviation_from_circular_orbits(x, y, z, time, color, save_fig_name_suffix):
+    # Plot of the deviation from circular orbit
+    R_1_th = 5.0  # kpc
+    R_2_th = 5.0  # kpc
+    R_3_th = 30.0  # kpc
+
+    fig2, ax2 = plt.subplots(nrows=1, ncols=2, num=2, figsize=(12, 4.5))
+    fig2.suptitle("Deviation from circular orbit", fontsize=15)
+    ax2[0].clear()
+    ax2[1].clear()
+
+    # Gather the x,y and z components of each particule into one array
+    pos_1 = np.array([x[0, :], y[0, :], z[0, :]])
+    pos_2 = np.array([x[1, :], y[1, :], z[1, :]])
+    pos_3 = np.array([x[2, :], y[2, :], z[2, :]])
+
+    # Compute the radii
+    r_1 = np.linalg.norm(pos_1, axis=0)
+    error_1 = np.abs(r_1 - R_1_th) / R_1_th * 100
+    r_2 = np.linalg.norm(pos_2, axis=0)
+    error_2 = np.abs(r_2 - R_2_th) / R_2_th * 100
+    r_3 = np.linalg.norm(pos_3, axis=0)
+    error_3 = np.abs(r_3 - R_3_th) / R_3_th * 100
+
+    ax2[0].plot(time, error_1, color[0])
+    ax2[1].plot(time, error_2, color[1])
+    ax2[1].plot(time, error_3, color[2])
+    ax2[0].set_ylabel("Deviation (\%)")
+    ax2[0].set_xlabel("Time (Gyr)")
+    ax2[1].set_ylabel("Deviation (\%)")
+    ax2[1].set_xlabel("Time (Gyr)")
+    ax2[0].legend(["Particule 1, $R = 5$ kpc"])
+    ax2[1].legend(["Particule 2, $R = 5$ kpc", "Particule 3, $R = 30$ kpc"])
+
+    plt.tight_layout()
+    plt.savefig("deviation" + save_fig_name_suffix + ".png", bbox_inches="tight")
+    plt.close()
+    return r_1, r_2, r_3
+
+
+def plot_deviation_from_original_data(r_1, r_2, r_3, time, color, save_fig_name_suffix):
+    """Make a comparison with the obtained data and ours to check nothing is broken."""
+    filename = "original_radii.txt"
+    r_1_original, r_2_original, r_3_original = np.loadtxt(filename)
+
+    # Plots the deviation wrt the original data
+    fig3, ax3 = plt.subplots(nrows=1, ncols=3, num=3, figsize=(12, 4.3))
+    fig3.suptitle("Deviation from the original data", fontsize=15)
+    ax3[0].clear()
+    ax3[1].clear()
+    ax3[2].clear()
+
+    error_1 = np.abs(r_1 - r_1_original) / r_1_original * 100
+    error_2 = np.abs(r_2 - r_2_original) / r_2_original * 100
+    error_3 = np.abs(r_3 - r_3_original) / r_3_original * 100
+
+    ax3[0].plot(time, error_1, col[0])
+    ax3[1].plot(time, error_2, col[1])
+    ax3[2].plot(time, error_3, col[2])
+    ax3[0].set_ylabel("Deviation (\%)")
+    ax3[0].set_xlabel("Time (Gyr)")
+    ax3[1].set_ylabel("Deviation (\%)")
+    ax3[1].set_xlabel("Time (Gyr)")
+    ax3[2].set_ylabel("Deviation (\%)")
+    ax3[2].set_xlabel("Time (Gyr)")
+    ax3[0].legend(["Particule 1, $R = 5$ kpc"])
+    ax3[1].legend(["Particule 2, $R = 5$ kpc"])
+    ax3[2].legend(["Particule 3, $R = 30$ kpc"])
+    plt.tight_layout()
+    plt.savefig(
+        "deviation_from_original_data" + save_fig_name_suffix + ".png",
+        bbox_inches="tight",
+    )
+    plt.close()
+
+
+#%%Plots the orbits, the deviation from the circular orbit and the deviation from the original precomputed data
+# Notice that in this examples, the ouputs are set in suitable units in the parameters files.
+
+# General parameters
+N_snapshots = 201
+N_part = 3
+boxsize = 1000.0  # kpc
+col = ["b", "r", "c", "y", "k"]
+
+# First type of units (kpc)
+output_dir = "output_1"
+save_fig_name_suffix = "_simulation_kpc"
+x_1, y_1, z_1, time_1, pot_1 = get_positions_and_time(
+    N_snapshots, N_part, output_dir, boxsize
+)
+plot_orbits(x_1, y_1, z_1, col, save_fig_name_suffix)
+r_11, r_21, r_31 = plot_deviation_from_circular_orbits(
+    x_1, y_1, z_1, time_1, col, save_fig_name_suffix
+)
+plot_deviation_from_original_data(r_11, r_21, r_31, time_1, col, save_fig_name_suffix)
+
+# Second type of units (Mpc) (no need for units conversion to kpc, this is already done by swift in the snapshots)
+output_dir = "output_2"
+save_fig_name_suffix = "_simulation_Mpc"
+x_2, y_2, z_2, time_2, pot_2 = get_positions_and_time(
+    N_snapshots, N_part, output_dir, boxsize
+)
+plot_orbits(x_2, y_2, z_2, col, save_fig_name_suffix)
+r_12, r_22, r_32 = plot_deviation_from_circular_orbits(
+    x_2, y_2, z_2, time_2, col, save_fig_name_suffix
+)
+# plot_deviation_from_original_data(r_12, r_22, r_32, time_2, col, save_fig_name_suffix) #does not make sense since the original data are in kpc, not in Mpc
+
+#%%Saves our data to be the reference ones (precomputed)
+# Uncomment only if corrections of the precomputed data must occur !
+# Original data :  If some corrections occur in the potential default parameters, allows to correct
+# the data
+# filename = "original_radii.txt"
+# np.savetxt(filename, (r_1, r_2, r_3))
diff --git a/examples/GravityTests/MWPotential2014_circularorbit/original_radii.txt b/examples/GravityTests/MWPotential2014_circularorbit/original_radii.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8f2e6a09e86727c1f64aa8cc0c3cb072073b387f
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_circularorbit/original_radii.txt
@@ -0,0 +1,3 @@
+5.000000000000000000e+00 5.001166599459545559e+00 5.020579887129778207e+00 5.100189116397097600e+00 5.299839849332817820e+00 5.578710433997593476e+00 5.783030230255659987e+00 5.827598846292087131e+00 5.682565987909128147e+00 5.357447130085647657e+00 4.912578397139871988e+00 4.497394792994665380e+00 4.397021356282944105e+00 4.775431657567319910e+00 5.300932384346391579e+00 5.731042038011933570e+00 5.965091504370781728e+00 5.970489901937201971e+00 5.752272156326732500e+00 5.356957312236751534e+00 4.906750290982262008e+00 4.657121571444537089e+00 4.714457558888792477e+00 4.933301629890139317e+00 5.176061235968707486e+00 5.362349356536657119e+00 5.461122477067911873e+00 5.485087743207845534e+00 5.485339856546893600e+00 5.523077935189706800e+00 5.537107938749801228e+00 5.464804174270136095e+00 5.285227013585691580e+00 5.020384810243945672e+00 4.743263219255731578e+00 4.592936367545448206e+00 4.749715571294080618e+00 5.194699542807357240e+00 5.642069433457655769e+00 5.933223989683256150e+00 6.005807399351317244e+00 5.844582655179585196e+00 5.470114662333674715e+00 4.956958096079127962e+00 4.496399606097051027e+00 4.436328410208022710e+00 4.769513345769095025e+00 5.203928883967520846e+00 5.560443291869900051e+00 5.758575039733128342e+00 5.777233497154392161e+00 5.634900936928413984e+00 5.399734327667077949e+00 5.204348571913461008e+00 5.108383765639208818e+00 5.057693052077008034e+00 5.015035997691488667e+00 4.970823689765934361e+00 4.947419029227536846e+00 4.995207789090853723e+00 5.182270748184618192e+00 5.494607608495452489e+00 5.759878945928849525e+00 5.871757751107428369e+00 5.788023974259131066e+00 5.507664932242153810e+00 5.073360594415489366e+00 4.604596875155709590e+00 4.356042545916934294e+00 4.609781734172386791e+00 5.121533352837495556e+00 5.594559699714237055e+00 5.895503032313782477e+00 5.976985446160555604e+00 5.833832600072278218e+00 5.499788789299549840e+00 5.068750025511791435e+00 4.754624075584575671e+00 4.723949943832726817e+00 4.875445028387068724e+00 5.081882737105429371e+00 5.259925739770850761e+00 5.372637274737933843e+00 5.425242400507194418e+00 5.460118287161413342e+00 5.536220897651173090e+00 5.595059701109168948e+00 5.560241134615987235e+00 5.401506769780430872e+00 5.130567420218448582e+00 4.809371855699721365e+00 4.569652033714569406e+00 4.612229578661045437e+00 5.019851467780982901e+00 5.505814358798329700e+00 5.865468786224973918e+00 6.016174230579117932e+00 5.931623634176135695e+00 5.622177861123924814e+00 5.141870044592171674e+00 4.638547645705167533e+00 4.428307819955180413e+00 4.654500832829794099e+00 5.055593990419128048e+00 5.427492129376623176e+00 5.668492207620567491e+00 5.744061565586670248e+00 5.662423276901259683e+00 5.476861486048790084e+00 5.299864737421422056e+00 5.202470997364078364e+00 5.134308457492519295e+00 5.058899583759844276e+00 4.970512274614167225e+00 4.896917303931519250e+00 4.898286265130379569e+00 5.054476498797095374e+00 5.384729626924356083e+00 5.707038692608169761e+00 5.888268525975762557e+00 5.871970152607735471e+00 5.647189263247745394e+00 5.241550304616231060e+00 4.747405841925699477e+00 4.378927045831982667e+00 4.474186584819125123e+00 4.940610149752312275e+00 5.439852850044650800e+00 5.799438267533770563e+00 5.953178490303065118e+00 5.885318802304978725e+00 5.618141372733455263e+00 5.224255162563557597e+00 4.875392411458151720e+00 4.768956166135601471e+00 4.850718648821461976e+00 5.009868149351704325e+00 5.165493314050841889e+00 5.278258318859305298e+00 5.348100423409463566e+00 5.410955922599304913e+00 5.522902505871598144e+00 5.630663735706390227e+00 5.642294107886619337e+00 5.517118052537815842e+00 5.256074072268789088e+00 4.908246395925647043e+00 4.590870106450615218e+00 4.510269017110958956e+00 4.841676564404478356e+00 5.347528554683028013e+00 5.768526167155091144e+00 5.995637396520894669e+00 5.989832219840335092e+00 5.751871797307715362e+00 5.320044178608156926e+00 4.806704151149128634e+00 4.474934801273977136e+00 4.576629640621881379e+00 4.921538717217767811e+00 5.290768217179389055e+00 5.562191457746648915e+00 5.686534351951245014e+00 5.661795715468069368e+00 5.528388603164072101e+00 5.380407491001560238e+00 5.294789296099782661e+00 5.223565412511718797e+00 5.127422326009080322e+00 5.001198016563644266e+00 4.875299081804920220e+00 4.818639828265833813e+00 4.925709984592579360e+00 5.252798132458202041e+00 5.624850406729384744e+00 5.875379086292169539e+00 5.930742494670786513e+00 5.769927451901470938e+00 5.407923458724494203e+00 4.913709661908932880e+00 4.459744560941122593e+00 4.382798389554843688e+00 4.768537003581841738e+00 5.272907504565521819e+00 5.680248961594461576e+00 5.900602226243958803e+00 5.906378074467261818e+00 5.709143330590744192e+00 5.364799048397715708e+00 5.008887298284076195e+00 4.843398709646222322e+00 4.859874698091035228e+00 4.965584565534517658e+00 5.086695693063617796e+00 5.185431020385737000e+00 5.259341369321015591e+00 5.340599814904859244e+00 5.482731832086808588e+00 5.640753798791949158e+00 5.705643537489685002e+00 5.624938857189243357e+00
+5.000000000000000000e+00 4.999888359284162753e+00 5.000641684297063350e+00 5.000994447130620024e+00 5.001675693602675388e+00 5.001624794631814197e+00 5.001558065482559989e+00 5.000888414115478575e+00 5.000403424123103235e+00 4.999857363290737489e+00 4.999815117935768605e+00 5.000030078010763823e+00 5.000590428170696988e+00 5.001185604320395939e+00 5.001578829707598928e+00 5.001700786476619420e+00 5.001329335974549650e+00 5.000875735587428927e+00 5.000172435947884964e+00 4.999938404239019363e+00 4.999745387639388028e+00 5.000283699222444866e+00 5.000643388144556667e+00 5.001470334121859374e+00 5.001565799279720004e+00 5.001856736927319247e+00 5.001184047933382892e+00 5.000858585776699172e+00 5.000045524839611844e+00 4.999938201977301766e+00 4.999789233051740389e+00 5.000353429170261599e+00 5.000792119848084027e+00 5.001452348355899247e+00 5.001632422736415506e+00 5.001608535971686642e+00 5.001127158676252904e+00 5.000553905860360970e+00 5.000029329624915420e+00 4.999762162118537745e+00 4.999946677627926661e+00 5.000335136347326070e+00 5.001035783553454017e+00 5.001422330001844330e+00 5.001779305392438424e+00 5.001442290734069651e+00 5.001159754497918719e+00 5.000348863737741567e+00 5.000122347527026889e+00 4.999700132208437786e+00 5.000215353627103454e+00 5.000418901608895794e+00 5.001274128363529137e+00 5.001475223091539135e+00 5.001811379870214402e+00 5.001359505875552891e+00 5.000995546297854588e+00 5.000250370590015869e+00 4.999942233798068791e+00 4.999755726773103426e+00 5.000113010258659507e+00 5.000609015959638803e+00 5.001219673802144605e+00 5.001614354202936319e+00 5.001636066331053954e+00 5.001364334692166125e+00 5.000733606126373409e+00 5.000258441419644484e+00 4.999780701257404480e+00 4.999929042417343261e+00 5.000122602854585097e+00 5.000898089680752889e+00 5.001250786342685828e+00 5.001830340890685456e+00 5.001536553836007926e+00 5.001450960015414893e+00 5.000564462723501968e+00 5.000273841477640424e+00 4.999729946197625452e+00 5.000019708256767004e+00 5.000238197444725863e+00 5.000993609020794928e+00 5.001370698158823735e+00 5.001741598978786563e+00 5.001520071329959016e+00 5.001141309344722785e+00 5.000492961311684859e+00 5.000007097885235119e+00 4.999789471734320756e+00 4.999926978056772775e+00 5.000452501234716074e+00 5.000982517444656494e+00 5.001568938123119423e+00 5.001630494678826366e+00 5.001581127018102535e+00 5.000918624733289519e+00 5.000520523906761383e+00 4.999849635612912735e+00 4.999963116585512957e+00 4.999945908278064621e+00 5.000768878036669918e+00 5.001060851264059082e+00 5.001749243021976099e+00 5.001594604383085674e+00 5.001528201965431464e+00 5.000787308820795474e+00 5.000362783584799686e+00 4.999809889944675234e+00 4.999870251224309392e+00 5.000081273895164991e+00 5.000706925098050704e+00 5.001234607158520085e+00 5.001628277065568895e+00 5.001642902217743547e+00 5.001272369050412614e+00 5.000752316183323387e+00 5.000115144777662834e+00 4.999877063130510280e+00 4.999785278748652750e+00 5.000314975961055453e+00 5.000732923535549546e+00 5.001486668371692978e+00 5.001580864064437471e+00 5.001767526649143925e+00 5.001102524724919896e+00 5.000815405699214899e+00 4.999977098680566279e+00 4.999965430706085456e+00 4.999820211656420987e+00 5.000468706648866224e+00 5.000862674405269992e+00 5.001540997553139967e+00 5.001619721607925229e+00 5.001589058085179396e+00 5.001022878428104335e+00 5.000494347648318794e+00 4.999950437180141094e+00 4.999782011985283603e+00 4.999968771211793950e+00 5.000436397880152484e+00 5.001086192651295725e+00 5.001485312783913173e+00 5.001737933662662172e+00 5.001395316031677751e+00 5.001032550774532126e+00 5.000272753974341455e+00 5.000032632149568279e+00 4.999711660403833413e+00 5.000227368341616518e+00 5.000504073344837153e+00 5.001396382624039738e+00 5.001505342442887248e+00 5.001831185827574799e+00 5.001279357006295001e+00 5.000939059512067075e+00 5.000156498511878489e+00 4.999939419656115547e+00 4.999760777523306388e+00 5.000213242684194981e+00 5.000677422077679068e+00 5.001316266444979952e+00 5.001612758519516255e+00 5.001623006669117188e+00 5.001255205432448392e+00 5.000655856805036770e+00 5.000151079135440213e+00 4.999768706765014059e+00 4.999923501590752828e+00 5.000205665288950385e+00 5.000941154591838078e+00 5.001315826587424240e+00 5.001794596942993110e+00 5.001491317612160259e+00 5.001316595758519057e+00 5.000470387348008749e+00 5.000254580857065534e+00 4.999712075477516393e+00 5.000102976947414568e+00 5.000306391555606744e+00 5.001111012410059509e+00 5.001407461804255661e+00 5.001773556648744012e+00 5.001449382725613013e+00 5.001085296333972252e+00 5.000387772118549456e+00 4.999982482345314061e+00 4.999767868097886314e+00 5.000003634425123522e+00 5.000507888369177145e+00 5.001079803140623170e+00 5.001579362536846318e+00 5.001633342805087423e+00 5.001483416394396642e+00 5.000841451375783286e+00 5.000401977832478195e+00 4.999818075316714072e+00 4.999936405692012364e+00 5.000014185319412441e+00 5.000809312551328212e+00 5.001135437752519231e+00 5.001827481510867202e+00
+3.000000000000000000e+01 2.999779458340697147e+01 2.999913108927551875e+01 2.999822513999371054e+01 2.999869101241313629e+01 2.999908157608862069e+01 2.999867255062164162e+01 3.000035432115671696e+01 2.999906436031323764e+01 3.000130726868057707e+01 2.999985124246509116e+01 3.000120011581939750e+01 3.000101355345713472e+01 3.000145740135981853e+01 3.000252851377030439e+01 3.000205476548366335e+01 3.000437014149219905e+01 3.000296575532702192e+01 3.000506656450175669e+01 3.000416345420759257e+01 3.000531256555992954e+01 3.000561911665013071e+01 3.000580235611645818e+01 3.000730436832045811e+01 3.000650795996002174e+01 3.000919171647245420e+01 3.000740253616102748e+01 3.000908722559980291e+01 3.000846124470268705e+01 3.000913539394623442e+01 3.000966175811804604e+01 3.000931550053518393e+01 3.001098523842369659e+01 3.000961070226721716e+01 3.001169411489794570e+01 3.001000798292042404e+01 3.001105493839978067e+01 3.001049866425505996e+01 3.001050661892987037e+01 3.001107862320722219e+01 3.001004719415356803e+01 3.001174860804977840e+01 3.000967995094207907e+01 3.001106890886319079e+01 3.000941297065721614e+01 3.000977242153797064e+01 3.000925845687551785e+01 3.000859589580250741e+01 3.000923251594434049e+01 3.000755748209316565e+01 3.000935520671618661e+01 3.000667892784183266e+01 3.000748164415950114e+01 3.000598473874775962e+01 3.000580528773574329e+01 3.000550129646441633e+01 3.000435340448430566e+01 3.000525642811016169e+01 3.000315424345881254e+01 3.000455578094075548e+01 3.000223634049040911e+01 3.000270501649730548e+01 3.000162796403852283e+01 3.000117720180458747e+01 3.000135582863928008e+01 2.999999786615864394e+01 3.000144392723029085e+01 2.999919002924884381e+01 3.000046804618323293e+01 2.999877353580113848e+01 2.999916917874825728e+01 2.999876482042754233e+01 2.999828485654238719e+01 2.999917646486512979e+01 2.999782544062704659e+01 3.000001627767003498e+01 2.999779633743827034e+01 2.999911844063658251e+01 2.999819830488571526e+01 2.999865030911109187e+01 2.999902751315119787e+01 2.999860581120435299e+01 3.000027557069040540e+01 2.999897424318053041e+01 3.000120647142659891e+01 2.999974052798087598e+01 3.000108036609949735e+01 3.000088565466134227e+01 3.000132237211635555e+01 3.000238750303375213e+01 3.000190931347448497e+01 3.000422185867911296e+01 3.000281580647389745e+01 3.000491608599069338e+01 3.000401361861365146e+01 3.000516478912791385e+01 3.000547462841453950e+01 3.000566249461711266e+01 3.000717035022314505e+01 3.000638099269628611e+01 3.000907299236889969e+01 3.000729313447100211e+01 3.000898818458806971e+01 3.000837351407666986e+01 3.000905988756514375e+01 3.000959933442729266e+01 3.000926695882656148e+01 3.001095125287651655e+01 3.000959180150701044e+01 3.001169059343623147e+01 3.001002003559164066e+01 3.001108278198048041e+01 3.001054248304392047e+01 3.001056653531087548e+01 3.001115458351709364e+01 3.001013901847990439e+01 3.001185589737429282e+01 3.000980246935861828e+01 3.001120595492282561e+01 3.000956393020903334e+01 3.000993653574142783e+01 3.000943488286652894e+01 3.000878378812352665e+01 3.000943101649924571e+01 3.000776555887522434e+01 3.000957175115255993e+01 3.000690282240585205e+01 3.000771163940339292e+01 3.000621963347996513e+01 3.000604381912500784e+01 3.000574221323055824e+01 3.000459546464529481e+01 3.000549825809542170e+01 3.000339446894059847e+01 3.000479306306118232e+01 3.000246913202072463e+01 3.000293187427373454e+01 3.000184761572847592e+01 3.000138830507564691e+01 3.000155700520485880e+01 3.000018799611447662e+01 3.000162189224056064e+01 2.999935478803817901e+01 3.000061862764304976e+01 2.999890904656334101e+01 2.999928887416733758e+01 2.999886807861885174e+01 2.999837114075570099e+01 2.999924530142823542e+01 2.999787658246655297e+01 3.000004954612104058e+01 2.999781168046782653e+01 2.999911595907233774e+01 2.999817825072545574e+01 2.999861297335620591e+01 2.999897334516337466e+01 2.999853524782260195e+01 3.000018922740121141e+01 2.999887294236322077e+01 3.000109111179866161e+01 2.999961196637686811e+01 3.000093950505366180e+01 3.000073363009094862e+01 3.000116029442814991e+01 3.000221660273533431e+01 3.000173085754470392e+01 3.000403703825239532e+01 3.000262587834090766e+01 3.000472258970311401e+01 3.000381811956868461e+01 3.000496875334294344e+01 3.000527961996388271e+01 3.000546996417365975e+01 3.000698169725789910e+01 3.000619758041846197e+01 3.000889621429398701e+01 3.000712430259497765e+01 3.000882861345186825e+01 3.000822447317817065e+01 3.000892257912635230e+01 3.000947488798068008e+01 3.000915634131698795e+01 3.001085531632530490e+01 3.000951134895363381e+01 3.001162660663587900e+01 3.000997326126640274e+01 3.001105384355043526e+01 3.001053154124043232e+01 3.001057371847517885e+01 3.001118006937515403e+01 3.001018279679330192e+01 3.001191804078371206e+01 3.000988258949455556e+01 3.001130365826893609e+01 3.000967875120074169e+01 3.001006775551087458e+01 3.000958182273279817e+01 3.000894558530716338e+01 3.000960665058126864e+01 3.000795400716476991e+01 3.000977178013717150e+01
diff --git a/examples/GravityTests/MWPotential2014_circularorbit/params_unit_1.yml b/examples/GravityTests/MWPotential2014_circularorbit/params_unit_1.yml
new file mode 100755
index 0000000000000000000000000000000000000000..b7a481af39cf6debc45c780f0a5bc595d1593053
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_circularorbit/params_unit_1.yml
@@ -0,0 +1,53 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+43 # 10^10 Solar masses
+  UnitLength_in_cgs:   3.0856775814913673e+21 # kpc
+  UnitVelocity_in_cgs: 1e5       # 1 km / s in cm/s
+  UnitCurrent_in_cgs:  1         # Amperes
+  UnitTemp_in_cgs:     1         # Kelvin
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:          0.      # The starting time of the simulation (in internal units).
+  time_end:            2.0     # The end time of the simulation (in internal units).
+  dt_min:              1e-10    # The minimal time-step size of the simulation (in internal units).
+  dt_max:              1e0    # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_1/output  # Common part of the name of output files
+  time_first:          0.      # Time of the first output (in internal units)
+  delta_time:          1e-2    # Time difference between consecutive outputs (in internal units)
+  UnitMass_in_cgs:     1.98848e+43
+  UnitLength_in_cgs:   3.086e+21
+  UnitVelocity_in_cgs: 1e5
+  UnitCurrent_in_cgs:  1
+  UnitTemp_in_cgs:     1
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1.0    # Time between statistics output
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          circular_orbits_MW.hdf5 # The file to read
+  periodic:           1
+
+# NFW_MN_PSC potential parameters
+MWPotential2014Potential:
+  useabspos:       0        # 0 -> positions based on centre, 1 -> absolute positions 
+  position:        [0.,0.,0.]    #Centre of the potential with respect to centre of the box
+  timestep_mult:   0.005     # Dimensionless pre-factor for the time-step condition
+  epsilon:         0.001      # Softening size (internal units)
+  #The following parameters are the default ones.
+  # concentration:    9.823403437774843
+  # M_200_Msun:       147.41031542774076e10
+  # H:                127.78254614201471e-2
+  # Mdisk_Msun:       6.8e10
+  # Rdisk_kpc:        3.0
+  # Zdisk_kpc:        0.280
+  # amplitude_Msun_per_kpc3: 1.0e10
+  # r_1_kpc:          1.0
+  # alpha:            1.8
+  # r_c_kpc:          1.9
+  # potential_factors: [0.4367419745056084, 1.002641971008805, 0.022264787598364262]
diff --git a/examples/GravityTests/MWPotential2014_circularorbit/params_unit_2.yml b/examples/GravityTests/MWPotential2014_circularorbit/params_unit_2.yml
new file mode 100755
index 0000000000000000000000000000000000000000..f540c52575a88c7e70ae518c78086dd589665a7f
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_circularorbit/params_unit_2.yml
@@ -0,0 +1,52 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+33 # 10^10 Solar masses
+  UnitLength_in_cgs:   3.0856775814913673e+24 # Mpc
+  UnitVelocity_in_cgs: 1e5       # 1 km / s in cm/s
+  UnitCurrent_in_cgs:  1         # Amperes
+  UnitTemp_in_cgs:     1         # Kelvin
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:          0.      # The starting time of the simulation (in internal units).
+  time_end:            2.0e-3     # The end time of the simulation (in internal units).
+  dt_min:              1e-13    # The minimal time-step size of the simulation (in internal units).
+  dt_max:              1e-3    # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_2/output  # Common part of the name of output files
+  time_first:          0.      # Time of the first output (in internal units)
+  delta_time:          1e-5    # Time difference between consecutive outputs (in internal units)
+  UnitMass_in_cgs:     1.98848e+43
+  UnitLength_in_cgs:   3.086e+21
+  UnitVelocity_in_cgs: 1e5
+  UnitCurrent_in_cgs:  1
+  UnitTemp_in_cgs:     1
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1.0    # Time between statistics output
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          circular_orbits_MW.hdf5 # The file to read
+  periodic:           1
+
+# NFW_MN_PSC potential parameters
+MWPotential2014Potential:
+  useabspos:       0        # 0 -> positions based on centre, 1 -> absolute positions 
+  position:        [0.,0.,0.]    #Centre of the potential with respect to centre of the box
+  timestep_mult:   0.0005     # Dimensionless pre-factor for the time-step condition
+  epsilon:         0.001e-3      # Softening size (internal units)
+  #The following parameters are the default ones.
+  # concentration:    9.823403437774843
+  # M_200_Msun:       147.41031542774076e10
+  # H:                127.78254614201471e-2
+  # Mdisk_Msun:       6.8e10
+  # Rdisk_kpc:        3.0
+  # Zdisk_kpc:        0.280
+  # amplitude_Msun_per_kpc3: 1e10
+  # r_1_kpc:          1.0
+  # alpha:            1.8
+  # r_c_kpc:          1.9
+  # potential_factors: [0.4367419745056084, 1.002641971008805, 0.022264787598364262]
diff --git a/examples/GravityTests/MWPotential2014_circularorbit/run.sh b/examples/GravityTests/MWPotential2014_circularorbit/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a2821996a9cfde87bfdaaff36df9bd576a083241
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_circularorbit/run.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+
+#Creates a directory for the outputs
+DIR=output_1 #First test of units conversion
+if [ -d "$DIR" ];
+then
+    echo "$DIR directory exists. Its content will be removed."
+    rm $DIR/output_*
+else
+    echo "$DIR directory does not exists. It will be created."
+    mkdir $DIR
+fi
+
+DIR=output_2 #Second test of units conversion
+if [ -d "$DIR" ];
+then
+    echo "$DIR directory exists. Its content will be removed."
+    rm $DIR/output_*
+else
+    echo "$DIR directory does not exists. It will be created."
+    mkdir $DIR
+fi
+
+#Clears the previous figures
+echo "Clearing existing figures."
+if [ -f "circular_orbits_simulation_kpc.png" ];
+then
+    rm circular_orbits_simulation_kpc.png
+fi
+
+if [ -f "circular_orbits_simulation_Mpc.png" ];
+then
+    rm circular_orbits_simulation_Mpc.png
+fi
+
+if [ -f "deviation_simulation_kpc.png" ];
+then
+    rm deviation_simulation_kpc.png
+fi
+if [ -f "deviation_simulation_Mpc.png" ];
+then
+    rm deviation_simulation_Mpc.png
+fi
+
+if [ -f "deviation_from_original_data_simulation_kpc.png" ];
+then
+    rm deviation_from_original_data_simulation_kpc.png
+fi
+
+if [ -f "deviation_from_original_data_simulation_Mpc.png" ];
+then
+    rm deviation_from_original_data_simulation_Mpc.png
+fi
+
+#Clears the IC file
+if [ -f "circular_orbits_MW.hdf5" ];
+then
+    rm circular_orbits_MW.hdf5
+fi
+
+
+#Generates the initial conditions
+echo "Generate initial conditions for circular orbits"
+if command -v python3 &>/dev/null; then
+    python3 makeIC.py
+else
+    python3 makeIC.py
+fi
+
+#Runs the simulation
+# self gravity G, external potential g, hydro s, threads t and high verbosity v
+echo "Starting the simulation with the first type of units (kpc)... Look at output_1.log for the simulation details."
+../../../swift --external-gravity --threads=8 params_unit_1.yml 2>&1 > output_1.log
+echo "Simulation ended."
+
+echo "Starting the simulation with the second type of units (Mpc)... Look at output_2.log for the simulation details."
+../../../swift --external-gravity --threads=8 params_unit_2.yml 2>&1 > output_2.log
+echo "Simulation ended."
+
+#Saves the plots
+echo "Save plots of the circular orbits and of the errors"
+if command -v python3 &>/dev/null; then
+    python3 makePlots.py
+else 
+    python3 makePlots.py
+fi
diff --git a/examples/GravityTests/MWPotential2014_df/README b/examples/GravityTests/MWPotential2014_df/README
new file mode 100644
index 0000000000000000000000000000000000000000..01a869ddbcd914529a34aee628de2d4c5525bbba
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_df/README
@@ -0,0 +1,28 @@
+# Context
+
+This example tests the dynamical friction implemented in the MWPotential2014.
+It compares the orbit predicted by SWIFT with an orbit computed with pNbody, using:
+
+`orbits_integration_MW --t_forward 10 --position 0.1 0.1 100 --velocity 80 0 0 --dynamical_friction -o orbit.csv`
+
+# How to run this example
+
+In a terminal at the root of the "swiftsim" directory, type:
+
+`./autogen.sh`
+
+Then, configure swift to take into account external potentials. Type:
+
+`./configure --with-ext-potential=MWPotential2014`
+
+Feel free to adapt other configurations parameters depending on your system. Then, build the code by typing:
+
+`make`
+
+Finally, to run this example, open a terminal in the present directory and type:
+
+`./run.sh`
+
+In this last command run Swift run on two different parameter files, params_unit_1.yml and params_unit_2.yml.
+Those two sets of parameters only differ by the choice of the internal units. This allow to test the correct 
+units conversion of all parameters involved.
diff --git a/examples/GravityTests/MWPotential2014_df/makeIC.py b/examples/GravityTests/MWPotential2014_df/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..edfd426c3d7f7ed4412d5c35abadb1ecf0617b3d
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_df/makeIC.py
@@ -0,0 +1,79 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+import numpy as np
+import h5py as h5
+
+box_size = 1000.0
+N_PARTICLES = 1
+print("Initial conditions written to 'IC.hdf5'")
+
+pos = np.zeros((1, 3))
+pos[0, 0] = 0.1
+pos[0, 1] = 0.1
+pos[0, 2] = 100
+
+
+# pos += np.array(
+#    [box_size / 2, box_size / 2, box_size / 2]
+# )  # shifts the particles to the center of the box
+
+vel = np.zeros((1, 3))
+vel[0, 0] = 80
+vel[0, 1] = 0
+vel[0, 2] = 0
+
+
+ids = np.array([1.0])
+mass = np.array([1.0]) * 1e-10
+
+# File
+file = h5.File("IC.hdf5", "w")
+
+# Units
+grp = file.create_group("/Units")
+grp.attrs["Unit length in cgs (U_L)"] = 3.086e21
+grp.attrs["Unit mass in cgs (U_M)"] = 1.98848e43
+grp.attrs["Unit time in cgs (U_t)"] = 3.086e16
+grp.attrs["Unit current in cgs (U_I)"] = 1.0
+grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+# Header
+grp = file.create_group("/Header")
+grp.attrs["BoxSize"] = box_size
+grp.attrs["NumPart_Total"] = [0, N_PARTICLES, 0, 0, 0, 0]
+grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_ThisFile"] = [0, N_PARTICLES, 0, 0, 0, 0]
+grp.attrs["Time"] = 0.0
+grp.attrs["NumFilesPerSnapshot"] = 1
+grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["Dimension"] = 3
+
+# Runtime parameters
+grp = file.create_group("/RuntimePars")
+grp.attrs["PeriodicBoundariesOn"] = 1
+
+# Particle group
+grp1 = file.create_group("/PartType1")
+ds = grp1.create_dataset("Velocities", (N_PARTICLES, 3), "f", data=vel)
+ds = grp1.create_dataset("Masses", (N_PARTICLES,), "f", data=mass)
+ds = grp1.create_dataset("ParticleIDs", (N_PARTICLES,), "L", data=ids)
+ds = grp1.create_dataset("Coordinates", (N_PARTICLES, 3), "d", data=pos)
+
+file.close()
diff --git a/examples/GravityTests/MWPotential2014_df/makePlots.py b/examples/GravityTests/MWPotential2014_df/makePlots.py
new file mode 100755
index 0000000000000000000000000000000000000000..7cda8aa43e7d0da91ebdc61ecc57ddec74a1eef2
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_df/makePlots.py
@@ -0,0 +1,134 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Darwin Roduit (yves.revaz@.epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+import numpy as np
+import h5py
+import matplotlib.pyplot as plt
+
+
+#%%Functions
+
+
+def get_positions_and_time(N_snapshots, N_part, output_dir, boxsize):
+    xx = np.zeros((N_part, N_snapshots))
+    yy = np.zeros((N_part, N_snapshots))
+    zz = np.zeros((N_part, N_snapshots))
+    time = np.zeros(N_snapshots)
+    pot = np.zeros((N_part, N_snapshots))
+    for i in range(0, N_snapshots):
+        Data = h5py.File(output_dir + "/output_%04d.hdf5" % i, "r")
+        header = Data["Header"]
+        time[i] = header.attrs["Time"][0]
+        particles = Data["PartType1"]
+        positions = particles["Coordinates"]
+        xx[:, i] = positions[:, 0] - boxsize / 2.0
+        yy[:, i] = positions[:, 1] - boxsize / 2.0
+        zz[:, i] = positions[:, 2] - boxsize / 2.0
+        pot[:, i] = particles["Potentials"][:]
+    return xx, yy, zz, time, pot
+
+
+def plot_orbits(x, y, z, t, color, save_fig_name_suffix):
+    # Plots the orbits
+    fig, ax = plt.subplots(nrows=1, ncols=4, num=1, figsize=(12, 4.1))
+    fig.suptitle("Orbits", fontsize=15)
+    ax[0].clear()
+    ax[1].clear()
+
+    for i in range(0, N_part):
+        ax[0].plot(x[i, :], y[i, :], color[i])
+
+    ax[0].set_aspect("equal", "box")
+    ax[0].set_xlim([-300, 300])
+    ax[0].set_ylim([-300, 300])
+    ax[0].set_ylabel("y (kpc)")
+    ax[0].set_xlabel("x (kpc)")
+
+    for i in range(0, N_part):
+        ax[1].plot(x[i, :], z[i, :], col[i], label="SWIFT solution")
+
+    ax[1].set_aspect("equal", "box")
+    ax[1].set_xlim([-100, 100])
+    ax[1].set_ylim([-100, 100])
+    ax[1].set_ylabel("z (kpc)")
+    ax[1].set_xlabel("x (kpc)")
+
+    for i in range(0, N_part):
+        ax[2].plot(y[i, :], z[i, :], col[i])
+
+    ax[2].set_aspect("equal", "box")
+    ax[2].set_xlim([-100, 100])
+    ax[2].set_ylim([-100, 100])
+    ax[2].set_ylabel("z (kpc)")
+    ax[2].set_xlabel("y (kpc)")
+    plt.tight_layout()
+
+    for i in range(0, N_part):
+        ax[3].plot(t, np.sqrt(x[i, :] ** 2 + y[i, :] ** 2 + z[i, :] ** 2), col[i])
+
+    ax[3].set_aspect("auto", "box")
+    ax[3].set_ylim([0, 100])
+    ax[3].set_ylabel("r (kpc)")
+    ax[3].set_xlabel("t (kpc)")
+    plt.tight_layout()
+
+    # add the reference orbit
+    data = np.genfromtxt("orbit.csv", delimiter=",", skip_header=1)
+    t = data[:, 0]
+    x = data[:, 1]
+    y = data[:, 2]
+    z = data[:, 3]
+
+    r = np.sqrt(x ** 2 + y ** 2 + z ** 2)
+
+    ax[0].plot(x, y, "grey", alpha=0.5, lw=5)
+    ax[1].plot(x, z, "grey", alpha=0.5, lw=5, label="pNbody solution")
+    ax[2].plot(y, z, "grey", alpha=0.5, lw=5)
+    ax[3].plot(t, r, "grey", alpha=0.5, lw=5)
+
+    ax[1].legend()
+
+    plt.savefig("orbits" + save_fig_name_suffix + ".png", bbox_inches="tight")
+    plt.close()
+
+
+#%%Plots the orbits, the deviation from the circular orbit and the deviation from the original precomputed data
+# Notice that in this examples, the ouputs are set in suitable units in the parameters files.
+
+# General parameters
+N_snapshots = 1001
+N_part = 1
+boxsize = 1000.0  # kpc
+col = ["b", "r", "c", "y", "k"]
+
+# First type of units (kpc)
+output_dir = "output_1"
+save_fig_name_suffix = "_simulation_kpc"
+x_1, y_1, z_1, time_1, pot_1 = get_positions_and_time(
+    N_snapshots, N_part, output_dir, boxsize
+)
+plot_orbits(x_1, y_1, z_1, time_1, col, save_fig_name_suffix)
+
+
+# Second type of units (Mpc) (no need for units conversion to kpc, this is already done by swift in the snapshots)
+output_dir = "output_2"
+save_fig_name_suffix = "_simulation_Mpc"
+x_2, y_2, z_2, time_2, pot_2 = get_positions_and_time(
+    N_snapshots, N_part, output_dir, boxsize
+)
+plot_orbits(x_2, y_2, z_2, time_2, col, save_fig_name_suffix)
diff --git a/examples/GravityTests/MWPotential2014_df/params_unit_1.yml b/examples/GravityTests/MWPotential2014_df/params_unit_1.yml
new file mode 100755
index 0000000000000000000000000000000000000000..ebc0b108e5d5a3fe2a4f94a51a953deee36e6768
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_df/params_unit_1.yml
@@ -0,0 +1,66 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+43 # 10^10 Solar masses
+  UnitLength_in_cgs:   3.0856775814913673e+21 # kpc
+  UnitVelocity_in_cgs: 1e5       # 1 km / s in cm/s
+  UnitCurrent_in_cgs:  1         # Amperes
+  UnitTemp_in_cgs:     1         # Kelvin
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:          0.      # The starting time of the simulation (in internal units).
+  time_end:            10.0     # The end time of the simulation (in internal units).
+  dt_min:              1e-10    # The minimal time-step size of the simulation (in internal units).
+  dt_max:              1e0    # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output           # Common part of the name of output files
+  subdir:              output_1         # (Optional) Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+  time_first:          0.      # Time of the first output (in internal units)
+  delta_time:          1e-2    # Time difference between consecutive outputs (in internal units)
+  UnitMass_in_cgs:     1.98848e+43
+  UnitLength_in_cgs:   3.086e+21
+  UnitVelocity_in_cgs: 1e5
+  UnitCurrent_in_cgs:  1
+  UnitTemp_in_cgs:     1
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1.0    # Time between statistics output
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          IC.hdf5 # The file to read
+  shift:              [500,500,500]
+  periodic:           0
+
+# NFW_MN_PSC potential parameters
+MWPotential2014Potential:
+  useabspos:       0                   # 0 -> positions based on centre, 1 -> absolute positions 
+  position:        [0.,0.,0.]          #Centre of the potential with respect to centre of the box
+  timestep_mult:   0.0005              # Dimensionless pre-factor for the time-step condition
+  epsilon:         0.001e-3            # Softening size (internal units)
+  with_dynamical_friction:1            # Are we running with dynamical friction ? 0 -> no, 1 -> yes
+  df_lnLambda:        5.0              # Coulomb logarithm
+  df_satellite_mass_in_Msun: 1e10      # Satellite mass in solar mass
+  df_core_radius_in_kpc: 10            # Radius below which the dynamical friction vanishes.  
+  df_timestep_mult:0.1                 # Dimensionless pre-factor for the time-step condition for the dynamical friction force
+  df_polyfit_coeffs00: -2.96536595e-31 # Polynomial fit coefficient for the velocity dispersion model (order 16)
+  df_polyfit_coeffs01:  8.88944631e-28 # Polynomial fit coefficient for the velocity dispersion model (order 15)
+  df_polyfit_coeffs02: -1.18280578e-24 # Polynomial fit coefficient for the velocity dispersion model (order 14)
+  df_polyfit_coeffs03:  9.29479457e-22 # Polynomial fit coefficient for the velocity dispersion model (order 13)
+  df_polyfit_coeffs04: -4.82805265e-19 # Polynomial fit coefficient for the velocity dispersion model (order 12)
+  df_polyfit_coeffs05:  1.75460211e-16 # Polynomial fit coefficient for the velocity dispersion model (order 11)
+  df_polyfit_coeffs06: -4.59976540e-14 # Polynomial fit coefficient for the velocity dispersion model (order 10)
+  df_polyfit_coeffs07:  8.83166045e-12 # Polynomial fit coefficient for the velocity dispersion model (order 9)
+  df_polyfit_coeffs08: -1.24747700e-09 # Polynomial fit coefficient for the velocity dispersion model (order 8)
+  df_polyfit_coeffs09:  1.29060404e-07 # Polynomial fit coefficient for the velocity dispersion model (order 7)
+  df_polyfit_coeffs10: -9.65315026e-06 # Polynomial fit coefficient for the velocity dispersion model (order 6)
+  df_polyfit_coeffs11:  5.10187806e-04 # Polynomial fit coefficient for the velocity dispersion model (order 5)
+  df_polyfit_coeffs12: -1.83800281e-02 # Polynomial fit coefficient for the velocity dispersion model (order 4)
+  df_polyfit_coeffs13:  4.26501444e-01 # Polynomial fit coefficient for the velocity dispersion model (order 3)
+  df_polyfit_coeffs14: -5.78038064e+00 # Polynomial fit coefficient for the velocity dispersion model (order 2)
+  df_polyfit_coeffs15:  3.57956721e+01 # Polynomial fit coefficient for the velocity dispersion model (order 1)
+  df_polyfit_coeffs16:  1.85478908e+02 # Polynomial fit coefficient for the velocity dispersion model (order 0)
+  
+
diff --git a/examples/GravityTests/MWPotential2014_df/params_unit_2.yml b/examples/GravityTests/MWPotential2014_df/params_unit_2.yml
new file mode 100755
index 0000000000000000000000000000000000000000..cad87d2e4358d7c71beb05a0373ae6e916575312
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_df/params_unit_2.yml
@@ -0,0 +1,63 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+33 # 10^10 Solar masses
+  UnitLength_in_cgs:   3.0856775814913673e+24 # Mpc
+  UnitVelocity_in_cgs: 1e5       # 1 km / s in cm/s
+  UnitCurrent_in_cgs:  1         # Amperes
+  UnitTemp_in_cgs:     1         # Kelvin
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:          0.      # The starting time of the simulation (in internal units).
+  time_end:            10.0e-3     # The end time of the simulation (in internal units).
+  dt_min:              1e-13    # The minimal time-step size of the simulation (in internal units).
+  dt_max:              1e-3    # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output           # Common part of the name of output files
+  subdir:              output_2         # (Optional) Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).  time_first:          0.      # Time of the first output (in internal units)
+  delta_time:          1e-5    # Time difference between consecutive outputs (in internal units)
+  UnitMass_in_cgs:     1.98848e+43
+  UnitLength_in_cgs:   3.086e+21
+  UnitVelocity_in_cgs: 1e5
+  UnitCurrent_in_cgs:  1
+  UnitTemp_in_cgs:     1
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1.0    # Time between statistics output
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          IC.hdf5 # The file to read
+  shift:              [0.5,0.5,0.5]
+  periodic:           0
+  
+# NFW_MN_PSC potential parameters
+MWPotential2014Potential:
+  useabspos:       0                 # 0 -> positions based on centre, 1 -> absolute positions 
+  position:        [0.,0.,0.]        #Centre of the potential with respect to centre of the box
+  timestep_mult:   0.0005            # Dimensionless pre-factor for the time-step condition
+  epsilon:         0.001e-3          # Softening size (internal units)
+  with_dynamical_friction:1          # Are we running with dynamical friction ? 0 -> no, 1 -> yes
+  df_lnLambda:        5.0              # Coulomb logarithm
+  df_satellite_mass_in_Msun: 1e10      # Satellite mass in solar mass
+  df_core_radius_in_kpc: 10            # Radius below which the dynamical friction vanishes.  
+  df_timestep_mult:0.1                 # Dimensionless pre-factor for the time-step condition for the dynamical friction force
+  df_polyfit_coeffs00: -2.96536595e-31 # Polynomial fit coefficient for the velocity dispersion model (order 16)
+  df_polyfit_coeffs01:  8.88944631e-28 # Polynomial fit coefficient for the velocity dispersion model (order 15)
+  df_polyfit_coeffs02: -1.18280578e-24 # Polynomial fit coefficient for the velocity dispersion model (order 14)
+  df_polyfit_coeffs03:  9.29479457e-22 # Polynomial fit coefficient for the velocity dispersion model (order 13)
+  df_polyfit_coeffs04: -4.82805265e-19 # Polynomial fit coefficient for the velocity dispersion model (order 12)
+  df_polyfit_coeffs05:  1.75460211e-16 # Polynomial fit coefficient for the velocity dispersion model (order 11)
+  df_polyfit_coeffs06: -4.59976540e-14 # Polynomial fit coefficient for the velocity dispersion model (order 10)
+  df_polyfit_coeffs07:  8.83166045e-12 # Polynomial fit coefficient for the velocity dispersion model (order 9)
+  df_polyfit_coeffs08: -1.24747700e-09 # Polynomial fit coefficient for the velocity dispersion model (order 8)
+  df_polyfit_coeffs09:  1.29060404e-07 # Polynomial fit coefficient for the velocity dispersion model (order 7)
+  df_polyfit_coeffs10: -9.65315026e-06 # Polynomial fit coefficient for the velocity dispersion model (order 6)
+  df_polyfit_coeffs11:  5.10187806e-04 # Polynomial fit coefficient for the velocity dispersion model (order 5)
+  df_polyfit_coeffs12: -1.83800281e-02 # Polynomial fit coefficient for the velocity dispersion model (order 4)
+  df_polyfit_coeffs13:  4.26501444e-01 # Polynomial fit coefficient for the velocity dispersion model (order 3)
+  df_polyfit_coeffs14: -5.78038064e+00 # Polynomial fit coefficient for the velocity dispersion model (order 2)
+  df_polyfit_coeffs15:  3.57956721e+01 # Polynomial fit coefficient for the velocity dispersion model (order 1)
+  df_polyfit_coeffs16:  1.85478908e+02 # Polynomial fit coefficient for the velocity dispersion model (order 0)
diff --git a/examples/GravityTests/MWPotential2014_df/run.sh b/examples/GravityTests/MWPotential2014_df/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..dec50890d1c71bd8b404e93a6319b780396ee6ac
--- /dev/null
+++ b/examples/GravityTests/MWPotential2014_df/run.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+
+
+#Clears the previous figures
+echo "Clearing existing figures."
+if [ -f "orbits_simulation_kpc.png" ];
+then
+    rm orbits_simulation_kpc.png
+fi
+
+if [ -f "orbits_simulation_Mpc.png" ];
+then
+    rm orbits_simulation_Mpc.png
+fi
+
+if [ ! -e orbit.csv ]
+then
+    echo "Fetching solution..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ReferenceSolutions/MWPotential_2014/orbit.csv
+fi
+
+
+#Clears the IC file
+if [ -f "circular_orbits_MW.hdf5" ];
+then
+    rm circular_orbits_MW.hdf5
+fi
+
+
+#Generates the initial conditions
+echo "Generate initial conditions for circular orbits"
+if command -v python3 &>/dev/null; then
+    python3 makeIC.py
+else
+    python3 makeIC.py
+fi
+
+#Runs the simulation
+# self gravity G, external potential g, hydro s, threads t and high verbosity v
+echo "Starting the simulation with the first type of units (kpc)... Look at output_1.log for the simulation details."
+../../../swift --external-gravity --threads=8 params_unit_1.yml 2>&1 > output_1.log
+echo "Simulation ended."
+
+echo "Starting the simulation with the second type of units (Mpc)... Look at output_2.log for the simulation details."
+../../../swift --external-gravity --threads=8 params_unit_2.yml 2>&1 > output_2.log
+echo "Simulation ended."
+
+#Saves the plots
+echo "Save plots of orbits."
+if command -v python3 &>/dev/null; then
+    python3 makePlots.py
+else 
+    python3 makePlots.py
+fi
diff --git a/examples/HydroTests/EvrardCollapse_3D/plotEnergy.py b/examples/HydroTests/EvrardCollapse_3D/plotEnergy.py
new file mode 100644
index 0000000000000000000000000000000000000000..290da76a10fea1b303ff38b46d65dff7b93196fe
--- /dev/null
+++ b/examples/HydroTests/EvrardCollapse_3D/plotEnergy.py
@@ -0,0 +1,58 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+import numpy as np
+from scipy import stats
+import sys
+
+plt.style.use("../../../tools/stylesheets/mnras.mplstyle")
+
+data = np.loadtxt("statistics.txt")
+t = data[:, 1]
+E_kin = data[:, 13]
+E_int = data[:, 14]
+E_pot = data[:, 15]
+
+E_tot = E_kin + E_int + E_pot
+
+plt.figure(figsize=(5, 5))
+
+plt.subplot(211)
+plt.plot(t, E_tot, "k", label="total")
+plt.plot(t, E_int, label="internal")
+plt.plot(t, E_kin, label="kinetic")
+plt.plot(t, E_pot, label="potential")
+
+plt.legend(loc="upper left")
+
+plt.ylabel("Energy")
+plt.xlabel("Time")
+
+plt.subplot(212)
+plt.plot(t, E_tot / E_tot[0], "k")
+
+plt.ylim(0.98, 1.02)
+plt.ylabel("Total energy w.r.t. t=0")
+plt.xlabel("Time")
+
+plt.savefig("Energy.png")
diff --git a/examples/HydroTests/NFW_Hydrostatic/NFW_Hydrostatic.yml b/examples/HydroTests/NFW_Hydrostatic/NFW_Hydrostatic.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5e31dd03d6bc21c8d8429ab375c83e0a2df38546
--- /dev/null
+++ b/examples/HydroTests/NFW_Hydrostatic/NFW_Hydrostatic.yml
@@ -0,0 +1,58 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98848e43    # 10^10 M_sun in grams
+  UnitLength_in_cgs:   3.08567758e21 # kpc in centimeters
+  UnitVelocity_in_cgs: 1e5           # km/s in centimeters per second
+  UnitCurrent_in_cgs:  1             # Amperes
+  UnitTemp_in_cgs:     1             # Kelvin
+
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   1.0  # The end time of the simulation (in internal units).
+  dt_min:     1e-10 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     0.1  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            snapshot # Common part of the name of output files
+  time_first:          0.       # Time of the first output (in internal units)
+  delta_time:          2e-2     # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1e-3 # Time between statistics output
+
+# Parameters for the self-gravity scheme
+Gravity:
+  eta:                       0.05    # Constant dimensionless multiplier for time integration.
+  MAC:                       geometric
+  theta_cr:                  0.7     
+  comoving_DM_softening:     0.01 # Comoving softening length (in internal units).
+  max_physical_DM_softening: 0.01    # Physical softening length (in internal units).
+  comoving_baryon_softening:     0.01 # Comoving softening length (in internal units).
+  max_physical_baryon_softening: 0.01    # Physical softening length (in internal units).
+  
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./nfw.hdf5     # The file to read
+  periodic:   0                     # Non-periodic BCs
+  shift:    [0,0,0]   # Centre the box
+
+NFWPotential:
+  useabspos:       0              # 0 -> positions based on centre, 1 -> absolute positions
+  position:        [0.,0.,0.]     # Location of centre of isothermal potential with respect to centre of the box (if 0) otherwise absolute (if 1) (internal units)
+  M_200:            9.5          # M200 of the galaxy disk
+  h:               0.72          # reduced Hubble constant (value does not specify the used units!)
+  concentration:   17.0            # concentration of the Halo
+  diskfraction:    0.15          # Disk mass fraction (here this is the gas)
+  bulgefraction:   0.0            # Bulge mass fraction
+  timestep_mult:   0.01           # Dimensionless pre-factor for the time-step condition, basically determines the fraction of the orbital time we use to do the time integration
+  epsilon:         0.001            # Softening size (internal units)
diff --git a/examples/HydroTests/NFW_Hydrostatic/README b/examples/HydroTests/NFW_Hydrostatic/README
new file mode 100644
index 0000000000000000000000000000000000000000..17655f5c5c83a3bd0894caa79c6d11e36451d67e
--- /dev/null
+++ b/examples/HydroTests/NFW_Hydrostatic/README
@@ -0,0 +1,13 @@
+
+Evolve gas at the static equilibrium in an NFW potential for several dynamical times.
+This test allows to compare the impact of different hydro solvers on the initial 
+hydrostatic equilibrium.
+
+To run SWIFT configured with the sphenix hydro solver:
+  ./configure --with-ext-potential=nfw --with-hydro=sphenix  
+  
+To run SWIFT configured with the gadget2 hydro solver:
+  ./configure --with-ext-potential=nfw --with-hydro=gadget2 --disable-hand-vec   
+
+To run SWIFT configured with the gizmo (MFV) hydro solver:
+  ./configure --with-ext-potential=nfw --with-hydro=gizmo-mfv --with-riemann-solver=exact    
diff --git a/examples/HydroTests/NFW_Hydrostatic/getICs.sh b/examples/HydroTests/NFW_Hydrostatic/getICs.sh
new file mode 100755
index 0000000000000000000000000000000000000000..4f132dc2c82b01e80a0e051137d4bf52c3b91fe5
--- /dev/null
+++ b/examples/HydroTests/NFW_Hydrostatic/getICs.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/NFW_Hydrostatic/nfw.hdf5
+
diff --git a/examples/HydroTests/NFW_Hydrostatic/plotGasDensityProfile.py b/examples/HydroTests/NFW_Hydrostatic/plotGasDensityProfile.py
new file mode 100755
index 0000000000000000000000000000000000000000..beb677f5127f98626b3dbd2e8f544bbb78c8de56
--- /dev/null
+++ b/examples/HydroTests/NFW_Hydrostatic/plotGasDensityProfile.py
@@ -0,0 +1,126 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Yves Revaz (yves.revaz@epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+import numpy as np
+from glob import glob
+import h5py
+
+
+plt.style.use("../../../tools/stylesheets/mnras.mplstyle")
+
+MyrInSec = 31557600000000.0
+gcm3InAcc = 1 / 5.978637406556783e23
+
+
+def ComputeDensity(snap):
+
+    # Read the initial state of the gas
+    f = h5py.File(snap, "r")
+
+    # Read the units parameters from the snapshot
+    units = f["InternalCodeUnits"]
+    unit_mass = units.attrs["Unit mass in cgs (U_M)"]
+    unit_length = units.attrs["Unit length in cgs (U_L)"]
+    unit_time = units.attrs["Unit time in cgs (U_t)"]
+    unit_density = unit_mass / unit_length ** 3
+
+    # Header
+    header = f["Header"]
+    BoxSize = header.attrs["BoxSize"]
+    Time = header.attrs["Time"]
+
+    # Read data
+    pos = f["/PartType0/Coordinates"][:]
+    rho = f["/PartType0/Densities"][:]
+    mass = f["/PartType0/Masses"][:]
+    ids = f["/PartType0/ParticleIDs"][:]
+
+    # Center the model and compute particle radius
+    pos = pos - BoxSize / 2
+    x = pos[:, 0]
+    y = pos[:, 1]
+    z = pos[:, 2]
+    r = np.sqrt(x * x + y * y + z * z)
+    n = len(r)
+
+    # sort particles according to their distance
+    idx = np.argsort(r)
+    r = r[idx]
+    mass = mass[idx]
+    ids = ids[idx]
+
+    nparts_per_bin = 100
+
+    # loop over bins containing nparts_per_bin particles
+    idx = np.arange(n)
+
+    # bins radius and mass
+    rs_beg = np.array([])
+    rs_end = np.array([])
+    ms = np.array([])
+
+    i = 0
+
+    while i + nparts_per_bin < n:
+
+        rs_beg = np.concatenate((rs_beg, [r[i]]))
+        rs_end = np.concatenate((rs_end, [r[i + nparts_per_bin]]))
+        m_this_bin = np.sum(mass[i : i + nparts_per_bin])
+        ms = np.concatenate((ms, [np.sum(m_this_bin)]))
+
+        # shift
+        i = i + nparts_per_bin
+
+    # compute density
+    vol = 4 / 3 * np.pi * (rs_end ** 3 - rs_beg ** 3)
+    rho = ms / vol
+
+    # compute radius, we use the mean
+    rs = 0.5 * (rs_beg + rs_end)
+
+    # convert rho to acc
+    rho = rho * unit_density / gcm3InAcc
+
+    # convert time to Myr
+    Time = Time * unit_time / MyrInSec
+
+    return rs, rho, Time
+
+
+# Do the plot
+
+plt.figure()
+
+rs, rho, Time = ComputeDensity("snapshot_0000.hdf5")
+plt.plot(rs, rho, c="b", label=r"$t=%5.1f\,\rm{[Myr]}$" % Time, lw=1)
+
+rs, rho, Time = ComputeDensity("snapshot_0050.hdf5")
+plt.plot(rs, rho, c="r", label=r"$t=%5.1f\,\rm{[Myr]}$" % Time, lw=1)
+
+plt.loglog()
+
+plt.legend()
+plt.xlabel("${\\rm{Radius~[kpc]}}$", labelpad=0)
+plt.ylabel("${\\rm{Density~[atom/cm^3]}}$", labelpad=0)
+
+
+plt.savefig("GasDensityProfile.png", dpi=200)
diff --git a/examples/HydroTests/NFW_Hydrostatic/run.sh b/examples/HydroTests/NFW_Hydrostatic/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..713957f30caaa90782902fcccab4d44b2ba366e4
--- /dev/null
+++ b/examples/HydroTests/NFW_Hydrostatic/run.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# Generate the initial conditions if they are not present.
+if [ ! -e nfw.hdf5 ]
+then
+    echo "Fetching initial conditions file for the example..."
+    ./getICs.sh
+fi
+
+
+# Run SWIFT
+../../../swift --hydro --external-gravity --self-gravity --threads=14  NFW_Hydrostatic.yml
+
+# Compute gas density profile
+python3  plotGasDensityProfile.py
diff --git a/examples/IdealisedCluster/IdealisedCluster_M13/idealised_cluster_M13.yml b/examples/IdealisedCluster/IdealisedCluster_M13/idealised_cluster_M13.yml
index 501961c070c5e00f22e339c6d2961430942c1a8a..6c853b16f3806ae58df19460a10224411340b92e 100644
--- a/examples/IdealisedCluster/IdealisedCluster_M13/idealised_cluster_M13.yml
+++ b/examples/IdealisedCluster/IdealisedCluster_M13/idealised_cluster_M13.yml
@@ -233,4 +233,79 @@ EAGLEAGN:
   merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
   minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
 
-  
+# Spin and jet AGN model (Husko et al. 2022)
+SPINJETAGN:
+  subgrid_seed_mass_Msun:             1.0e4           # Black hole subgrid mass at creation time in solar masses.
+  use_multi_phase_bondi:              0               # Compute Bondi rates per neighbour particle?
+  use_subgrid_bondi:                  0               # Compute Bondi rates using the subgrid extrapolation of the gas properties around the BH?
+  with_angmom_limiter:                0               # Are we applying the Rosas-Guevara et al. (2015) viscous time-scale reduction term?
+  viscous_alpha:                      1e6             # Normalisation constant of the viscous time-scale in the accretion reduction term
+  with_boost_factor:                  0               # Are we using the model from Booth & Schaye (2009)?
+  boost_alpha_only:                   0               # If using the boost factor, are we using a constant boost only?
+  boost_alpha:                        1.              # Lowest value for the accretion effeciency for the Booth & Schaye 2009 accretion model.
+  boost_beta:                         2.              # Slope of the power law for the Booth & Schaye 2009 model, set beta to zero for constant alpha models.
+  boost_n_h_star_H_p_cm3:             0.1             # Normalization of the power law for the Booth & Schaye 2009 model in cgs (cm^-3).
+  with_fixed_T_near_EoS:              0               # Are we using a fixed temperature to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term?
+  fixed_T_above_EoS_dex:              0.3             # Distance above the entropy floor for which we use a fixed sound-speed
+  fixed_T_near_EoS_K:                 8000            # Fixed temperature assumed to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term
+  radiative_efficiency:               0.1             # Fraction of the accreted mass that gets radiated.
+  use_nibbling:                       1               # Continuously transfer small amounts of mass from all gas neighbours to a black hole [1] or stochastically swallow whole gas particles [0]?
+  min_gas_mass_for_nibbling_Msun:     9e5             # Minimum mass for a gas particle to be nibbled from [M_Sun]. Only used if use_nibbling is 1.
+  max_eddington_fraction:             1.              # Maximal allowed accretion rate in units of the Eddington rate.
+  eddington_fraction_for_recording:   0.1             # Record the last time BHs reached an Eddington ratio above this threshold.
+  coupling_efficiency:                0.1             # Fraction of the radiated energy that couples to the gas in feedback events.
+  AGN_feedback_model:                 MinimumDistance # Feedback modes: Random, Isotropic, MinimumDistance, MinimumDensity
+  AGN_use_deterministic_feedback:     1               # Deterministic (reservoir) [1] or stochastic [0] AGN feedback?
+  use_variable_delta_T:               1               # Switch to enable adaptive calculation of AGN dT [1], rather than using a constant value [0].
+  AGN_with_locally_adaptive_delta_T:  1               # Switch to enable additional dependence of AGN dT on local gas density and temperature (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_norm:              3e8             # Normalisation temperature of AGN dT scaling with BH subgrid mass [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_reference:         1e8             # BH subgrid mass at which the normalisation temperature set above applies [M_Sun] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_exponent:          0.666667        # Power-law index of AGN dT scaling with BH subgrid mass (only used if use_variable_delta_T is 1).
+  AGN_delta_T_crit_factor:            1.0             # Multiple of critical dT for numerical efficiency (Dalla Vecchia & Schaye 2012) to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_background_factor:      0.0             # Multiple of local gas temperature to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_min:                    1e7             # Minimum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_max:                    3e9             # Maximum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_K:                      3.16228e8       # Change in temperature to apply to the gas particle in an AGN feedback event [K] (used if use_variable_delta_T is 0 or AGN_use_nheat_with_fixed_dT is 1 AND to initialise the BHs).
+  AGN_heating_temperature_model:      Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported.
+  delta_T_xi:                         1.              # The numerical multiplier by which the heating temperature formula is scaled, if 'AGN_heating_temperature_model' is 'Local'. If a value of 1 is used, the formulas are used as derived, i.e. they are not rescaled.
+  AGN_use_nheat_with_fixed_dT:        0               # Switch to use the constant AGN dT, rather than the adaptive one, for calculating the energy reservoir threshold.
+  AGN_use_adaptive_energy_reservoir_threshold: 0      # Switch to calculate an adaptive AGN energy reservoir threshold.
+  AGN_num_ngb_to_heat:                1.              # Target number of gas neighbours to heat in an AGN feedback event (only used if AGN_use_adaptive_energy_reservoir_threshold is 0).
+  max_reposition_mass:                1e20            # Maximal BH mass considered for BH repositioning in solar masses (large number implies we always reposition).
+  max_reposition_distance_ratio:      3.0             # Maximal distance a BH can be repositioned, in units of the softening length.
+  with_reposition_velocity_threshold: 0               # Should we only reposition to particles that move slowly w.r.t. the black hole?
+  max_reposition_velocity_ratio:      0.5             # Maximal velocity offset of a particle to reposition a BH to, in units of the ambient sound speed of the BH. Only meaningful if with_reposition_velocity_threshold is 1.
+  min_reposition_velocity_threshold: -1.0             # Minimal value of the velocity threshold for repositioning [km/s], set to < 0 for no effect. Only meaningful if with_reposition_velocity_threshold is 1.
+  set_reposition_speed:               0               # Should we reposition black holes with (at most) a prescribed speed towards the potential minimum?
+  with_potential_correction:          1               # Should the BH's own contribution to the potential be removed from the neighbour's potentials when looking for repositioning targets.
+  threshold_major_merger:             0.333           # Mass ratio threshold to consider a BH merger as 'major'
+  threshold_minor_merger:             0.1             # Mass ratio threshold to consider a BH merger as 'minor'
+  merger_threshold_type:              DynamicalEscapeVelocity  # Type of velocity threshold for BH mergers ('CircularVelocity', 'EscapeVelocity', 'DynamicalEscapeVelocity').
+  merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
+  minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
+  include_jets:                       1               # Global switch whether to include jet feedback [1] or not [0].
+  turn_off_radiative_feedback:        0               # Global switch whether to turn off radiative (thermal) feedback [1] or not [0]. This should only be used if 'include_jets' is set to 1, since we want feedback in some form or another.
+  alpha_acc:                          0.2             # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between the thin and thick disk, as dot(m) = 0.2 * alpha^2.
+  mdot_crit_ADAF:                     0.01            # The transition normalized accretion rate (Eddington ratio) at which the disc goes from thick (low accretion rates) to thin (high accretion rates). The feedback also changes from kinetic jets to thermal isotropic, respectively.
+  seed_spin:                          0.01            # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
+  AGN_jet_velocity_model:             Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
+  v_jet_km_p_s:                       5000.           # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
+  opening_angle_in_degrees:           7.5             # The half-opening angle of the jet in degrees. Should use values < 15 unless for tests.
+  N_jet:                              2               # Target number of particles to kick as part of a single jet feedback event. Should be a multiple of 2 to ensure approximate momentum conservation (we always kick particles in pairs, one from each 'side' of the BH, relative to the spin vector).
+  AGN_jet_feedback_model:             MinimumDistance # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
+  eps_f_jet:                          1.              # Coupling efficiency for jet feedback. No reason to expect this to be less than 1.
+  fix_jet_efficiency:                 0               # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0].
+  jet_efficiency:                     0.1             # The constant jet efficiency used if 'fix_jet_efficiency' is set to 1.
+  fix_jet_direction:                  0               # Global switch whether to fix the jet direction to be along the z-axis, instead of along the spin vector.
+  accretion_efficiency_mode:          Constant        # How the accretion efficiencies are calculated for the thick accretion disc. If 'Constant', the value of 'accretion_efficiency_thick' will be used. If 'Variable', the accretion efficiency will scale with Eddington ratio.
+  accretion_efficiency_thick:         0.01            # The accretion efficiency (suppression factor of the accretion rate) to use in the thick disc (ADAF), to represent the effects of subgrid ADIOS winds that take away most of the mass flowing through the accretion disc.
+  accretion_efficiency_slim:          1               # The constant accretion efficiency to use in the slim disc, at super-Eddington rates.
+  fix_radiative_efficiency:           0               # Global switch whether to fix the radiative efficiency to a particular value [1], or use a spin-dependant formula [0]. 
+  radiative_efficiency:               0.1             # The constant jet efficiency used if 'fix_radiative_efficiency' is set to 1. Otherwise, this value is used to define the Eddington accretion rate.
+  TD_region:                          B               # How to treat the subgrid accretion disk if it is thin, according to the Shakura & Sunyaev (1973) model. If set to B, region b will be used. If set to C, region c will be used.
+  include_GRMHD_spindown:             1               # Whether to include high jet spindown rates from GRMHD simulations [1], or use an analytical formula that assumes extraction of energy from the rotational mass/energy of the BH.
+  delta_ADAF:                         0.2             # Electron heating parameter, which controls the strength of radiative feedback in thick disks. Should be between 0.1 and 0.5. This parameter is only used if turn_off_secondary_feedback is set to 0.
+  include_slim_disk:                  0               # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
+  use_jets_in_thin_disc:              1               # Whether to use jets alongside radiation in the thin disc at moderate Eddington ratios.
+  use_ADIOS_winds:                    0               # Whether to include ADIOS winds in the thick disc as thermal isotropic feedback (same channel as thin disc quasar feedback, but with a different efficiency). 
+  slim_disc_wind_factor:              0               # The relative efficiency of slim disc winds at super-Eddington rates. If '1', full winds will be used, while '0' will lead to no winds. Any value in between those can also be used. The wind is implemented in the thermal isotropic feedback channel.
diff --git a/examples/IdealisedCluster/IdealisedCluster_M135/idealised_cluster_M135.yml b/examples/IdealisedCluster/IdealisedCluster_M135/idealised_cluster_M135.yml
index 0ebd235b2572ba805b436069da715e951a5feb00..7a0771e0da226dfe9f226972f337c4015357bee7 100644
--- a/examples/IdealisedCluster/IdealisedCluster_M135/idealised_cluster_M135.yml
+++ b/examples/IdealisedCluster/IdealisedCluster_M135/idealised_cluster_M135.yml
@@ -233,4 +233,79 @@ EAGLEAGN:
   merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
   minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
 
-  
+# Spin and jet AGN model (Husko et al. 2022)
+SPINJETAGN:
+  subgrid_seed_mass_Msun:             1.0e4           # Black hole subgrid mass at creation time in solar masses.
+  use_multi_phase_bondi:              0               # Compute Bondi rates per neighbour particle?
+  use_subgrid_bondi:                  0               # Compute Bondi rates using the subgrid extrapolation of the gas properties around the BH?
+  with_angmom_limiter:                0               # Are we applying the Rosas-Guevara et al. (2015) viscous time-scale reduction term?
+  viscous_alpha:                      1e6             # Normalisation constant of the viscous time-scale in the accretion reduction term
+  with_boost_factor:                  0               # Are we using the model from Booth & Schaye (2009)?
+  boost_alpha_only:                   0               # If using the boost factor, are we using a constant boost only?
+  boost_alpha:                        1.              # Lowest value for the accretion effeciency for the Booth & Schaye 2009 accretion model.
+  boost_beta:                         2.              # Slope of the power law for the Booth & Schaye 2009 model, set beta to zero for constant alpha models.
+  boost_n_h_star_H_p_cm3:             0.1             # Normalization of the power law for the Booth & Schaye 2009 model in cgs (cm^-3).
+  with_fixed_T_near_EoS:              0               # Are we using a fixed temperature to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term?
+  fixed_T_above_EoS_dex:              0.3             # Distance above the entropy floor for which we use a fixed sound-speed
+  fixed_T_near_EoS_K:                 8000            # Fixed temperature assumed to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term
+  radiative_efficiency:               0.1             # Fraction of the accreted mass that gets radiated.
+  use_nibbling:                       1               # Continuously transfer small amounts of mass from all gas neighbours to a black hole [1] or stochastically swallow whole gas particles [0]?
+  min_gas_mass_for_nibbling_Msun:     9e5             # Minimum mass for a gas particle to be nibbled from [M_Sun]. Only used if use_nibbling is 1.
+  max_eddington_fraction:             1.              # Maximal allowed accretion rate in units of the Eddington rate.
+  eddington_fraction_for_recording:   0.1             # Record the last time BHs reached an Eddington ratio above this threshold.
+  coupling_efficiency:                0.1             # Fraction of the radiated energy that couples to the gas in feedback events.
+  AGN_feedback_model:                 MinimumDistance # Feedback modes: Random, Isotropic, MinimumDistance, MinimumDensity
+  AGN_use_deterministic_feedback:     1               # Deterministic (reservoir) [1] or stochastic [0] AGN feedback?
+  use_variable_delta_T:               1               # Switch to enable adaptive calculation of AGN dT [1], rather than using a constant value [0].
+  AGN_with_locally_adaptive_delta_T:  1               # Switch to enable additional dependence of AGN dT on local gas density and temperature (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_norm:              3e8             # Normalisation temperature of AGN dT scaling with BH subgrid mass [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_reference:         1e8             # BH subgrid mass at which the normalisation temperature set above applies [M_Sun] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_exponent:          0.666667        # Power-law index of AGN dT scaling with BH subgrid mass (only used if use_variable_delta_T is 1).
+  AGN_delta_T_crit_factor:            1.0             # Multiple of critical dT for numerical efficiency (Dalla Vecchia & Schaye 2012) to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_background_factor:      0.0             # Multiple of local gas temperature to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_min:                    1e7             # Minimum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_max:                    3e9             # Maximum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_K:                      3.16228e8       # Change in temperature to apply to the gas particle in an AGN feedback event [K] (used if use_variable_delta_T is 0 or AGN_use_nheat_with_fixed_dT is 1 AND to initialise the BHs).
+  AGN_heating_temperature_model:      Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported.
+  delta_T_xi:                         1.              # The numerical multiplier by which the heating temperature formula is scaled, if 'AGN_heating_temperature_model' is 'Local'. If a value of 1 is used, the formulas are used as derived, i.e. they are not rescaled.
+  AGN_use_nheat_with_fixed_dT:        0               # Switch to use the constant AGN dT, rather than the adaptive one, for calculating the energy reservoir threshold.
+  AGN_use_adaptive_energy_reservoir_threshold: 0      # Switch to calculate an adaptive AGN energy reservoir threshold.
+  AGN_num_ngb_to_heat:                1.              # Target number of gas neighbours to heat in an AGN feedback event (only used if AGN_use_adaptive_energy_reservoir_threshold is 0).
+  max_reposition_mass:                1e20            # Maximal BH mass considered for BH repositioning in solar masses (large number implies we always reposition).
+  max_reposition_distance_ratio:      3.0             # Maximal distance a BH can be repositioned, in units of the softening length.
+  with_reposition_velocity_threshold: 0               # Should we only reposition to particles that move slowly w.r.t. the black hole?
+  max_reposition_velocity_ratio:      0.5             # Maximal velocity offset of a particle to reposition a BH to, in units of the ambient sound speed of the BH. Only meaningful if with_reposition_velocity_threshold is 1.
+  min_reposition_velocity_threshold: -1.0             # Minimal value of the velocity threshold for repositioning [km/s], set to < 0 for no effect. Only meaningful if with_reposition_velocity_threshold is 1.
+  set_reposition_speed:               0               # Should we reposition black holes with (at most) a prescribed speed towards the potential minimum?
+  with_potential_correction:          1               # Should the BH's own contribution to the potential be removed from the neighbour's potentials when looking for repositioning targets.
+  threshold_major_merger:             0.333           # Mass ratio threshold to consider a BH merger as 'major'
+  threshold_minor_merger:             0.1             # Mass ratio threshold to consider a BH merger as 'minor'
+  merger_threshold_type:              DynamicalEscapeVelocity  # Type of velocity threshold for BH mergers ('CircularVelocity', 'EscapeVelocity', 'DynamicalEscapeVelocity').
+  merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
+  minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
+  include_jets:                       1               # Global switch whether to include jet feedback [1] or not [0].
+  turn_off_radiative_feedback:        0               # Global switch whether to turn off radiative (thermal) feedback [1] or not [0]. This should only be used if 'include_jets' is set to 1, since we want feedback in some form or another.
+  alpha_acc:                          0.2             # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between the thin and thick disk, as dot(m) = 0.2 * alpha^2.
+  mdot_crit_ADAF:                     0.01            # The transition normalized accretion rate (Eddington ratio) at which the disc goes from thick (low accretion rates) to thin (high accretion rates). The feedback also changes from kinetic jets to thermal isotropic, respectively.
+  seed_spin:                          0.01            # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
+  AGN_jet_velocity_model:             Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
+  v_jet_km_p_s:                       5000.           # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
+  opening_angle_in_degrees:           7.5             # The half-opening angle of the jet in degrees. Should use values < 15 unless for tests.
+  N_jet:                              2               # Target number of particles to kick as part of a single jet feedback event. Should be a multiple of 2 to ensure approximate momentum conservation (we always kick particles in pairs, one from each 'side' of the BH, relative to the spin vector).
+  AGN_jet_feedback_model:             MinimumDistance # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
+  eps_f_jet:                          1.              # Coupling efficiency for jet feedback. No reason to expect this to be less than 1.
+  fix_jet_efficiency:                 0               # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0].
+  jet_efficiency:                     0.1             # The constant jet efficiency used if 'fix_jet_efficiency' is set to 1.
+  fix_jet_direction:                  0               # Global switch whether to fix the jet direction to be along the z-axis, instead of along the spin vector.
+  accretion_efficiency_mode:          Constant        # How the accretion efficiencies are calculated for the thick accretion disc. If 'Constant', the value of 'accretion_efficiency_thick' will be used. If 'Variable', the accretion efficiency will scale with Eddington ratio.
+  accretion_efficiency_thick:         0.01            # The accretion efficiency (suppression factor of the accretion rate) to use in the thick disc (ADAF), to represent the effects of subgrid ADIOS winds that take away most of the mass flowing through the accretion disc.
+  accretion_efficiency_slim:          1               # The constant accretion efficiency to use in the slim disc, at super-Eddington rates.
+  fix_radiative_efficiency:           0               # Global switch whether to fix the radiative efficiency to a particular value [1], or use a spin-dependant formula [0]. 
+  radiative_efficiency:               0.1             # The constant jet efficiency used if 'fix_radiative_efficiency' is set to 1. Otherwise, this value is used to define the Eddington accretion rate.
+  TD_region:                          B               # How to treat the subgrid accretion disk if it is thin, according to the Shakura & Sunyaev (1973) model. If set to B, region b will be used. If set to C, region c will be used.
+  include_GRMHD_spindown:             1               # Whether to include high jet spindown rates from GRMHD simulations [1], or use an analytical formula that assumes extraction of energy from the rotational mass/energy of the BH.
+  delta_ADAF:                         0.2             # Electron heating parameter, which controls the strength of radiative feedback in thick disks. Should be between 0.1 and 0.5. This parameter is only used if turn_off_secondary_feedback is set to 0.
+  include_slim_disk:                  0               # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
+  use_jets_in_thin_disc:              1               # Whether to use jets alongside radiation in the thin disc at moderate Eddington ratios.
+  use_ADIOS_winds:                    0               # Whether to include ADIOS winds in the thick disc as thermal isotropic feedback (same channel as thin disc quasar feedback, but with a different efficiency). 
+  slim_disc_wind_factor:              0               # The relative efficiency of slim disc winds at super-Eddington rates. If '1', full winds will be used, while '0' will lead to no winds. Any value in between those can also be used. The wind is implemented in the thermal isotropic feedback channel.
diff --git a/examples/IdealisedCluster/IdealisedCluster_M14/idealised_cluster_M14.yml b/examples/IdealisedCluster/IdealisedCluster_M14/idealised_cluster_M14.yml
index 742a9e50a4f70cd35fc9d54084c0501120962782..f184bf45774f54dbdc483a0465363ed128d7bd51 100644
--- a/examples/IdealisedCluster/IdealisedCluster_M14/idealised_cluster_M14.yml
+++ b/examples/IdealisedCluster/IdealisedCluster_M14/idealised_cluster_M14.yml
@@ -233,4 +233,79 @@ EAGLEAGN:
   merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
   minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
 
-  
+# Spin and jet AGN model (Husko et al. 2022)
+SPINJETAGN:
+  subgrid_seed_mass_Msun:             1.0e4           # Black hole subgrid mass at creation time in solar masses.
+  use_multi_phase_bondi:              0               # Compute Bondi rates per neighbour particle?
+  use_subgrid_bondi:                  0               # Compute Bondi rates using the subgrid extrapolation of the gas properties around the BH?
+  with_angmom_limiter:                0               # Are we applying the Rosas-Guevara et al. (2015) viscous time-scale reduction term?
+  viscous_alpha:                      1e6             # Normalisation constant of the viscous time-scale in the accretion reduction term
+  with_boost_factor:                  0               # Are we using the model from Booth & Schaye (2009)?
+  boost_alpha_only:                   0               # If using the boost factor, are we using a constant boost only?
+  boost_alpha:                        1.              # Lowest value for the accretion effeciency for the Booth & Schaye 2009 accretion model.
+  boost_beta:                         2.              # Slope of the power law for the Booth & Schaye 2009 model, set beta to zero for constant alpha models.
+  boost_n_h_star_H_p_cm3:             0.1             # Normalization of the power law for the Booth & Schaye 2009 model in cgs (cm^-3).
+  with_fixed_T_near_EoS:              0               # Are we using a fixed temperature to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term?
+  fixed_T_above_EoS_dex:              0.3             # Distance above the entropy floor for which we use a fixed sound-speed
+  fixed_T_near_EoS_K:                 8000            # Fixed temperature assumed to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term
+  radiative_efficiency:               0.1             # Fraction of the accreted mass that gets radiated.
+  use_nibbling:                       1               # Continuously transfer small amounts of mass from all gas neighbours to a black hole [1] or stochastically swallow whole gas particles [0]?
+  min_gas_mass_for_nibbling_Msun:     9e5             # Minimum mass for a gas particle to be nibbled from [M_Sun]. Only used if use_nibbling is 1.
+  max_eddington_fraction:             1.              # Maximal allowed accretion rate in units of the Eddington rate.
+  eddington_fraction_for_recording:   0.1             # Record the last time BHs reached an Eddington ratio above this threshold.
+  coupling_efficiency:                0.1             # Fraction of the radiated energy that couples to the gas in feedback events.
+  AGN_feedback_model:                 MinimumDistance # Feedback modes: Random, Isotropic, MinimumDistance, MinimumDensity
+  AGN_use_deterministic_feedback:     1               # Deterministic (reservoir) [1] or stochastic [0] AGN feedback?
+  use_variable_delta_T:               1               # Switch to enable adaptive calculation of AGN dT [1], rather than using a constant value [0].
+  AGN_with_locally_adaptive_delta_T:  1               # Switch to enable additional dependence of AGN dT on local gas density and temperature (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_norm:              3e8             # Normalisation temperature of AGN dT scaling with BH subgrid mass [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_reference:         1e8             # BH subgrid mass at which the normalisation temperature set above applies [M_Sun] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_exponent:          0.666667        # Power-law index of AGN dT scaling with BH subgrid mass (only used if use_variable_delta_T is 1).
+  AGN_delta_T_crit_factor:            1.0             # Multiple of critical dT for numerical efficiency (Dalla Vecchia & Schaye 2012) to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_background_factor:      0.0             # Multiple of local gas temperature to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_min:                    1e7             # Minimum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_max:                    3e9             # Maximum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_K:                      3.16228e8       # Change in temperature to apply to the gas particle in an AGN feedback event [K] (used if use_variable_delta_T is 0 or AGN_use_nheat_with_fixed_dT is 1 AND to initialise the BHs).
+  AGN_heating_temperature_model:      Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported.
+  delta_T_xi:                         1.              # The numerical multiplier by which the heating temperature formula is scaled, if 'AGN_heating_temperature_model' is 'Local'. If a value of 1 is used, the formulas are used as derived, i.e. they are not rescaled.
+  AGN_use_nheat_with_fixed_dT:        0               # Switch to use the constant AGN dT, rather than the adaptive one, for calculating the energy reservoir threshold.
+  AGN_use_adaptive_energy_reservoir_threshold: 0      # Switch to calculate an adaptive AGN energy reservoir threshold.
+  AGN_num_ngb_to_heat:                1.              # Target number of gas neighbours to heat in an AGN feedback event (only used if AGN_use_adaptive_energy_reservoir_threshold is 0).
+  max_reposition_mass:                1e20            # Maximal BH mass considered for BH repositioning in solar masses (large number implies we always reposition).
+  max_reposition_distance_ratio:      3.0             # Maximal distance a BH can be repositioned, in units of the softening length.
+  with_reposition_velocity_threshold: 0               # Should we only reposition to particles that move slowly w.r.t. the black hole?
+  max_reposition_velocity_ratio:      0.5             # Maximal velocity offset of a particle to reposition a BH to, in units of the ambient sound speed of the BH. Only meaningful if with_reposition_velocity_threshold is 1.
+  min_reposition_velocity_threshold: -1.0             # Minimal value of the velocity threshold for repositioning [km/s], set to < 0 for no effect. Only meaningful if with_reposition_velocity_threshold is 1.
+  set_reposition_speed:               0               # Should we reposition black holes with (at most) a prescribed speed towards the potential minimum?
+  with_potential_correction:          1               # Should the BH's own contribution to the potential be removed from the neighbour's potentials when looking for repositioning targets.
+  threshold_major_merger:             0.333           # Mass ratio threshold to consider a BH merger as 'major'
+  threshold_minor_merger:             0.1             # Mass ratio threshold to consider a BH merger as 'minor'
+  merger_threshold_type:              DynamicalEscapeVelocity  # Type of velocity threshold for BH mergers ('CircularVelocity', 'EscapeVelocity', 'DynamicalEscapeVelocity').
+  merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
+  minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
+  include_jets:                       1               # Global switch whether to include jet feedback [1] or not [0].
+  turn_off_radiative_feedback:        0               # Global switch whether to turn off radiative (thermal) feedback [1] or not [0]. This should only be used if 'include_jets' is set to 1, since we want feedback in some form or another.
+  alpha_acc:                          0.2             # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between the thin and thick disk, as dot(m) = 0.2 * alpha^2.
+  mdot_crit_ADAF:                     0.01            # The transition normalized accretion rate (Eddington ratio) at which the disc goes from thick (low accretion rates) to thin (high accretion rates). The feedback also changes from kinetic jets to thermal isotropic, respectively.
+  seed_spin:                          0.01            # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
+  AGN_jet_velocity_model:             Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
+  v_jet_km_p_s:                       5000.           # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
+  opening_angle_in_degrees:           7.5             # The half-opening angle of the jet in degrees. Should use values < 15 unless for tests.
+  N_jet:                              2               # Target number of particles to kick as part of a single jet feedback event. Should be a multiple of 2 to ensure approximate momentum conservation (we always kick particles in pairs, one from each 'side' of the BH, relative to the spin vector).
+  AGN_jet_feedback_model:             MinimumDistance # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
+  eps_f_jet:                          1.              # Coupling efficiency for jet feedback. No reason to expect this to be less than 1.
+  fix_jet_efficiency:                 0               # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0].
+  jet_efficiency:                     0.1             # The constant jet efficiency used if 'fix_jet_efficiency' is set to 1.
+  fix_jet_direction:                  0               # Global switch whether to fix the jet direction to be along the z-axis, instead of along the spin vector.
+  accretion_efficiency_mode:          Constant        # How the accretion efficiencies are calculated for the thick accretion disc. If 'Constant', the value of 'accretion_efficiency_thick' will be used. If 'Variable', the accretion efficiency will scale with Eddington ratio.
+  accretion_efficiency_thick:         0.01            # The accretion efficiency (suppression factor of the accretion rate) to use in the thick disc (ADAF), to represent the effects of subgrid ADIOS winds that take away most of the mass flowing through the accretion disc.
+  accretion_efficiency_slim:          1               # The constant accretion efficiency to use in the slim disc, at super-Eddington rates.
+  fix_radiative_efficiency:           0               # Global switch whether to fix the radiative efficiency to a particular value [1], or use a spin-dependant formula [0]. 
+  radiative_efficiency:               0.1             # The constant jet efficiency used if 'fix_radiative_efficiency' is set to 1. Otherwise, this value is used to define the Eddington accretion rate.
+  TD_region:                          B               # How to treat the subgrid accretion disk if it is thin, according to the Shakura & Sunyaev (1973) model. If set to B, region b will be used. If set to C, region c will be used.
+  include_GRMHD_spindown:             1               # Whether to include high jet spindown rates from GRMHD simulations [1], or use an analytical formula that assumes extraction of energy from the rotational mass/energy of the BH.
+  delta_ADAF:                         0.2             # Electron heating parameter, which controls the strength of radiative feedback in thick disks. Should be between 0.1 and 0.5. This parameter is only used if turn_off_secondary_feedback is set to 0.
+  include_slim_disk:                  0               # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
+  use_jets_in_thin_disc:              1               # Whether to use jets alongside radiation in the thin disc at moderate Eddington ratios.
+  use_ADIOS_winds:                    0               # Whether to include ADIOS winds in the thick disc as thermal isotropic feedback (same channel as thin disc quasar feedback, but with a different efficiency). 
+  slim_disc_wind_factor:              0               # The relative efficiency of slim disc winds at super-Eddington rates. If '1', full winds will be used, while '0' will lead to no winds. Any value in between those can also be used. The wind is implemented in the thermal isotropic feedback channel.
\ No newline at end of file
diff --git a/examples/IdealisedCluster/IdealisedCluster_M15/idealised_cluster_M15.yml b/examples/IdealisedCluster/IdealisedCluster_M15/idealised_cluster_M15.yml
index b5b69a1f81151f79e338f6f9ea93a14d248c06e7..dd55de086101b5bf9c7a208ec0d01cb5698163fc 100644
--- a/examples/IdealisedCluster/IdealisedCluster_M15/idealised_cluster_M15.yml
+++ b/examples/IdealisedCluster/IdealisedCluster_M15/idealised_cluster_M15.yml
@@ -232,3 +232,80 @@ EAGLEAGN:
   merger_threshold_type:              DynamicalEscapeVelocity  # Type of velocity threshold for BH mergers ('CircularVelocity', 'EscapeVelocity', 'DynamicalEscapeVelocity').
   merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
   minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
+
+# Spin and jet AGN model (Husko et al. 2022)
+SPINJETAGN:
+  subgrid_seed_mass_Msun:             1.0e4           # Black hole subgrid mass at creation time in solar masses.
+  use_multi_phase_bondi:              0               # Compute Bondi rates per neighbour particle?
+  use_subgrid_bondi:                  0               # Compute Bondi rates using the subgrid extrapolation of the gas properties around the BH?
+  with_angmom_limiter:                0               # Are we applying the Rosas-Guevara et al. (2015) viscous time-scale reduction term?
+  viscous_alpha:                      1e6             # Normalisation constant of the viscous time-scale in the accretion reduction term
+  with_boost_factor:                  0               # Are we using the model from Booth & Schaye (2009)?
+  boost_alpha_only:                   0               # If using the boost factor, are we using a constant boost only?
+  boost_alpha:                        1.              # Lowest value for the accretion effeciency for the Booth & Schaye 2009 accretion model.
+  boost_beta:                         2.              # Slope of the power law for the Booth & Schaye 2009 model, set beta to zero for constant alpha models.
+  boost_n_h_star_H_p_cm3:             0.1             # Normalization of the power law for the Booth & Schaye 2009 model in cgs (cm^-3).
+  with_fixed_T_near_EoS:              0               # Are we using a fixed temperature to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term?
+  fixed_T_above_EoS_dex:              0.3             # Distance above the entropy floor for which we use a fixed sound-speed
+  fixed_T_near_EoS_K:                 8000            # Fixed temperature assumed to compute the sound-speed of gas on the entropy floor in the Bondy-Hoyle accretion term
+  radiative_efficiency:               0.1             # Fraction of the accreted mass that gets radiated.
+  use_nibbling:                       1               # Continuously transfer small amounts of mass from all gas neighbours to a black hole [1] or stochastically swallow whole gas particles [0]?
+  min_gas_mass_for_nibbling_Msun:     9e5             # Minimum mass for a gas particle to be nibbled from [M_Sun]. Only used if use_nibbling is 1.
+  max_eddington_fraction:             1.              # Maximal allowed accretion rate in units of the Eddington rate.
+  eddington_fraction_for_recording:   0.1             # Record the last time BHs reached an Eddington ratio above this threshold.
+  coupling_efficiency:                0.1             # Fraction of the radiated energy that couples to the gas in feedback events.
+  AGN_feedback_model:                 MinimumDistance # Feedback modes: Random, Isotropic, MinimumDistance, MinimumDensity
+  AGN_use_deterministic_feedback:     1               # Deterministic (reservoir) [1] or stochastic [0] AGN feedback?
+  use_variable_delta_T:               1               # Switch to enable adaptive calculation of AGN dT [1], rather than using a constant value [0].
+  AGN_with_locally_adaptive_delta_T:  1               # Switch to enable additional dependence of AGN dT on local gas density and temperature (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_norm:              3e8             # Normalisation temperature of AGN dT scaling with BH subgrid mass [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_reference:         1e8             # BH subgrid mass at which the normalisation temperature set above applies [M_Sun] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_mass_exponent:          0.666667        # Power-law index of AGN dT scaling with BH subgrid mass (only used if use_variable_delta_T is 1).
+  AGN_delta_T_crit_factor:            1.0             # Multiple of critical dT for numerical efficiency (Dalla Vecchia & Schaye 2012) to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_background_factor:      0.0             # Multiple of local gas temperature to use as dT floor (only used if use_variable_delta_T and AGN_with_locally_adaptive_delta_T are both 1).
+  AGN_delta_T_min:                    1e7             # Minimum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_max:                    3e9             # Maximum allowed value of AGN dT [K] (only used if use_variable_delta_T is 1).
+  AGN_delta_T_K:                      3.16228e8       # Change in temperature to apply to the gas particle in an AGN feedback event [K] (used if use_variable_delta_T is 0 or AGN_use_nheat_with_fixed_dT is 1 AND to initialise the BHs).
+  AGN_heating_temperature_model:      Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported.
+  delta_T_xi:                         1.              # The numerical multiplier by which the heating temperature formula is scaled, if 'AGN_heating_temperature_model' is 'Local'. If a value of 1 is used, the formulas are used as derived, i.e. they are not rescaled.
+  AGN_use_nheat_with_fixed_dT:        0               # Switch to use the constant AGN dT, rather than the adaptive one, for calculating the energy reservoir threshold.
+  AGN_use_adaptive_energy_reservoir_threshold: 0      # Switch to calculate an adaptive AGN energy reservoir threshold.
+  AGN_num_ngb_to_heat:                1.              # Target number of gas neighbours to heat in an AGN feedback event (only used if AGN_use_adaptive_energy_reservoir_threshold is 0).
+  max_reposition_mass:                1e20            # Maximal BH mass considered for BH repositioning in solar masses (large number implies we always reposition).
+  max_reposition_distance_ratio:      3.0             # Maximal distance a BH can be repositioned, in units of the softening length.
+  with_reposition_velocity_threshold: 0               # Should we only reposition to particles that move slowly w.r.t. the black hole?
+  max_reposition_velocity_ratio:      0.5             # Maximal velocity offset of a particle to reposition a BH to, in units of the ambient sound speed of the BH. Only meaningful if with_reposition_velocity_threshold is 1.
+  min_reposition_velocity_threshold: -1.0             # Minimal value of the velocity threshold for repositioning [km/s], set to < 0 for no effect. Only meaningful if with_reposition_velocity_threshold is 1.
+  set_reposition_speed:               0               # Should we reposition black holes with (at most) a prescribed speed towards the potential minimum?
+  with_potential_correction:          1               # Should the BH's own contribution to the potential be removed from the neighbour's potentials when looking for repositioning targets.
+  threshold_major_merger:             0.333           # Mass ratio threshold to consider a BH merger as 'major'
+  threshold_minor_merger:             0.1             # Mass ratio threshold to consider a BH merger as 'minor'
+  merger_threshold_type:              DynamicalEscapeVelocity  # Type of velocity threshold for BH mergers ('CircularVelocity', 'EscapeVelocity', 'DynamicalEscapeVelocity').
+  merger_max_distance_ratio:          3.0             # Maximal distance over which two BHs can merge, in units of the softening length.
+  minimum_timestep_Myr:               0.1             # Minimum of the accretion-limited time-step length.
+  include_jets:                       1               # Global switch whether to include jet feedback [1] or not [0].
+  turn_off_radiative_feedback:        0               # Global switch whether to turn off radiative (thermal) feedback [1] or not [0]. This should only be used if 'include_jets' is set to 1, since we want feedback in some form or another.
+  alpha_acc:                          0.2             # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between the thin and thick disk, as dot(m) = 0.2 * alpha^2.
+  mdot_crit_ADAF:                     0.01            # The transition normalized accretion rate (Eddington ratio) at which the disc goes from thick (low accretion rates) to thin (high accretion rates). The feedback also changes from kinetic jets to thermal isotropic, respectively.
+  seed_spin:                          0.01            # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
+  AGN_jet_velocity_model:             Constant        # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
+  v_jet_km_p_s:                       5000.           # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
+  opening_angle_in_degrees:           7.5             # The half-opening angle of the jet in degrees. Should use values < 15 unless for tests.
+  N_jet:                              2               # Target number of particles to kick as part of a single jet feedback event. Should be a multiple of 2 to ensure approximate momentum conservation (we always kick particles in pairs, one from each 'side' of the BH, relative to the spin vector).
+  AGN_jet_feedback_model:             MinimumDistance # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
+  eps_f_jet:                          1.              # Coupling efficiency for jet feedback. No reason to expect this to be less than 1.
+  fix_jet_efficiency:                 0               # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0].
+  jet_efficiency:                     0.1             # The constant jet efficiency used if 'fix_jet_efficiency' is set to 1.
+  fix_jet_direction:                  0               # Global switch whether to fix the jet direction to be along the z-axis, instead of along the spin vector.
+  accretion_efficiency_mode:          Constant        # How the accretion efficiencies are calculated for the thick accretion disc. If 'Constant', the value of 'accretion_efficiency_thick' will be used. If 'Variable', the accretion efficiency will scale with Eddington ratio.
+  accretion_efficiency_thick:         0.01            # The accretion efficiency (suppression factor of the accretion rate) to use in the thick disc (ADAF), to represent the effects of subgrid ADIOS winds that take away most of the mass flowing through the accretion disc.
+  accretion_efficiency_slim:          1               # The constant accretion efficiency to use in the slim disc, at super-Eddington rates.
+  fix_radiative_efficiency:           0               # Global switch whether to fix the radiative efficiency to a particular value [1], or use a spin-dependant formula [0]. 
+  radiative_efficiency:               0.1             # The constant jet efficiency used if 'fix_radiative_efficiency' is set to 1. Otherwise, this value is used to define the Eddington accretion rate.
+  TD_region:                          B               # How to treat the subgrid accretion disk if it is thin, according to the Shakura & Sunyaev (1973) model. If set to B, region b will be used. If set to C, region c will be used.
+  include_GRMHD_spindown:             1               # Whether to include high jet spindown rates from GRMHD simulations [1], or use an analytical formula that assumes extraction of energy from the rotational mass/energy of the BH.
+  delta_ADAF:                         0.2             # Electron heating parameter, which controls the strength of radiative feedback in thick disks. Should be between 0.1 and 0.5. This parameter is only used if turn_off_secondary_feedback is set to 0.
+  include_slim_disk:                  0               # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
+  use_jets_in_thin_disc:              1               # Whether to use jets alongside radiation in the thin disc at moderate Eddington ratios.
+  use_ADIOS_winds:                    0               # Whether to include ADIOS winds in the thick disc as thermal isotropic feedback (same channel as thin disc quasar feedback, but with a different efficiency). 
+  slim_disc_wind_factor:              0               # The relative efficiency of slim disc winds at super-Eddington rates. If '1', full winds will be used, while '0' will lead to no winds. Any value in between those can also be used. The wind is implemented in the thermal isotropic feedback channel.
\ No newline at end of file
diff --git a/examples/IdealisedCluster/README b/examples/IdealisedCluster/README
index 18689194da63f0ccba8bd627bae226b41d82a39c..17ebf3bd43a8654c0ca72d1038934b94a0e7cfc5 100644
--- a/examples/IdealisedCluster/README
+++ b/examples/IdealisedCluster/README
@@ -15,11 +15,14 @@ different resolution and different central temperatures.
 
 The code should be configured using a NFW profile and the desired subgrid
 models:
-./configure --with-ext-potential=nfw --enable-fixed-boundary-particles=2
+./configure --with-subgrid=x --with-ext-potential=nfw 
+--enable-fixed-boundary-particles=2
+where x is EAGLE if the desired subgrid model is similar to that used in
+Nobels et al. (2022) and SPIN_JET_EAGLE for the model used in Husko et al. 
+(2022), which has jet AGN feedback instead of thermal isotropic feedback.
 
-The first argument sets the external potential to an NFW profile and the 
-second argument pins the supermassive BH of the simulation to the centre
-of the halo.
+The remaining arguments set the external potential to an NFW profile and pin
+the supermassive BH of the simulation to the centre of the halo.
 
 These ICs are ideal for testing galaxy formation subgrid model 
 implementation.
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_feedback/getIC.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_feedback/getIC.sh
index 32195a97b154e849eacd781ddbe98f59ecf48311..227fcebe73fcf67aab9838f9eb332d32abc1e953 100755
--- a/examples/IsolatedGalaxy/IsolatedGalaxy_feedback/getIC.sh
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_feedback/getIC.sh
@@ -1,3 +1,7 @@
 #!/bin/bash 
 
 wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolatedGalaxies/lowres8.hdf5
+
+# Initial conditions from Nobels et al. arXiv:2309.13750
+#wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolatedGalaxies_COLIBRE/M5_disk.hdf5
+#wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolatedGalaxies_COLIBRE/M6_disk.hdf5
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/README b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/README
new file mode 100644
index 0000000000000000000000000000000000000000..cd91ab37288a9737480f760a87e0bfbd7810c239
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/README
@@ -0,0 +1,18 @@
+We show here how to run such initial conditions with the AGORA model.
+
+To generate the initial conditions locally, if pNbody is installed, run:
+
+../makeDisk.py
+
+If you don't have access to pNbody, you can simply skip this step. The initial conditions will automatically 
+be downloaded when launching the run script (see below).
+
+
+To run this example with the AGORA model, SWIFT must be configured with the following options:
+
+./configure --with-feedback=AGORA --with-chemistry=AGORA  --with-cooling=grackle_0  --with-pressure-floor=GEAR --with-stars=GEAR --with-star-formation=GEAR  --with-grackle=${GRACKLE_ROOT}
+
+To start the simulation with the AGORA model:
+
+./run.sh
+
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/getGrackleCoolingTable.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/getGrackleCoolingTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e3eb106240709c80151a48625567d2cd78e5f568
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/getGrackleCoolingTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012.h5
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/params.yml b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/params.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8218a822d63a7f20f4d382b9656f17e974dc890c
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/params.yml
@@ -0,0 +1,94 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98848e43    # 10^10 M_sun in grams
+  UnitLength_in_cgs:   3.08567758e21 # kpc in centimeters
+  UnitVelocity_in_cgs: 1e5           # km/s in centimeters per second
+  UnitCurrent_in_cgs:  1             # Amperes
+  UnitTemp_in_cgs:     1             # Kelvin
+
+Scheduler:
+  max_top_level_cells: 16
+  cell_extra_gparts:         100         # (Optional) Number of spare gparts per top-level allocated at rebuild time for on-the-fly creation.
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   0.1   # The end time of the simulation (in internal units).
+  dt_min:     1e-10 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     0.1  # The maximal time-step size of the simulation (in internal units).
+  max_dt_RMS_factor: 0.25  # (Optional) Dimensionless factor for the maximal displacement allowed based on the RMS velocities.
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir:              snap  # snapshot directory
+  basename:            snapshot # Common part of the name of output files
+  time_first:          0.    # Time of the first output (in internal units)
+  delta_time:          1e-2  # Time difference between consecutive outputs (in internal units)
+  compression:         4
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1e-3 # Time between statistics output
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:          0.025               # Constant dimensionless multiplier for time integration.
+  max_physical_baryon_softening: 0.05  # Physical softening length (in internal units)
+  max_physical_DM_softening: 0.05  # Physical softening length (in internal units)
+  
+# Parameters for the hydrodynamics scheme
+SPH:  
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+  h_max:                 10.      # (Optional) Maximal allowed smoothing length in internal units. Defaults to FLT_MAX if unspecified.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./galaxy_multi_component.hdf5     # The file to read
+  periodic:   0                                 # Non-periodic BCs
+  shift:    [750,750,750]                       # Centre the box
+
+# Cooling with Grackle 2.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5 # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 1 # Enable or not the UV background
+  redshift: 0 # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 1 # Enable or not the metal cooling
+  provide_volumetric_heating_rates: 0 # User provide volumetric heating rates
+  provide_specific_heating_rates: 0 # User provide specific heating rates
+  self_shielding_method: -1 # Grackle (<= 3) or Gear self shielding method
+  self_shielding_threshold_atom_per_cm3: 1e10  # Required only with GEAR's self shielding. Density threshold of the self shielding
+  max_steps: 1000
+  convergence_limit: 1e-2
+  thermal_time_myr: 0
+  maximal_density_Hpcm3: -1   # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+GEARStarFormation:
+  star_formation_mode: agora        # default or agora
+  star_formation_efficiency: 0.01   # star formation efficiency (c_*)
+  maximal_temperature_K:     1e10   # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   10     # Density threshold in Hydrogen atoms/cm3
+  n_stars_per_particle: 1
+  min_mass_frac: 0.5
+
+GEARPressureFloor:
+  jeans_factor: 10
+
+AGORAFeedback:
+  energy_in_erg_per_CCSN: 1e51
+  supernovae_efficiency: 1
+  supernovae_explosion_time_myr: 5 # In Myr
+  ccsne_per_solar_mass : 0.010989 # 1/91
+  ejected_mass_in_solar_mass_per_CCSN : 14.8
+  ejected_Fe_mass_in_solar_mass_per_CCSN : 2.63
+  ejected_metal_mass_in_solar_mass_per_CCSN : 2.63
+
+AGORAChemistry:
+  initial_metallicity: 1          # if less than 0, read the metallicity from the snapshot (1 means solar abundance)
+  solar_abundance_Metals: 0.02  
+  scale_initial_metallicity: 1    # scale the initial metallicity using solar_abundance_Metals ?
+
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/run.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..8191e66aeb83caca765afd5dc177699268f0679e
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/AGORA/run.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+if [ ! -e galaxy_multi_component.hdf5 ]
+then
+    echo "Fetching initial conditions to run the example..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolatedGalaxies/galaxy_multi_component.hdf5
+fi
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ./getGrackleCoolingTable.sh
+fi
+
+
+printf "Running simulation..."
+
+../../../../swift --hydro --stars --star-formation --self-gravity --feedback --cooling --threads=14 params.yml 2>&1 | tee output.log 
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/README b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/README
new file mode 100644
index 0000000000000000000000000000000000000000..bb34801d2d1c8390ad9fc29fd78012bb697f6e32
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/README
@@ -0,0 +1,17 @@
+We show here how to run such initial conditions with the EAGLE model.
+
+To generate the initial conditions locally, if pNbody is installed, run:
+
+../makeDisk.py
+
+If you don't have access to pNbody, you can simply skip this step. The initial conditions will automatically 
+be downloaded when launching the run script (see below).
+
+To run this example with the EAGLE model, SWIFT must be configured with the following options:
+
+./configure --with-chemistry=EAGLE  --with-cooling=EAGLE  --with-stars=EAGLE --with-star-formation=EAGLE --with-feedback=EAGLE --with-tracers=EAGLE
+
+To start the simulation with the EAGLE model:
+
+./run.sh
+
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getEagleCoolingTable.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getEagleCoolingTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5cfd93ef0f4603e40b7675f3f2c254b2250f699f
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getEagleCoolingTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/EAGLE/coolingtables.tar.gz
+tar -xf coolingtables.tar.gz 
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getEaglePhotometryTable.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getEaglePhotometryTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ee9c3b422f19518612416da0913b162fd4a120ff
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getEaglePhotometryTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/YieldTables/EAGLE/photometry.tar.gz
+tar -xf photometry.tar.gz
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getPS2020CoolingTables.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getPS2020CoolingTables.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0945dc565a4c8b1723b21e685b03369e782cd3be
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getPS2020CoolingTables.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/COLIBRE/UV_dust1_CR1_G1_shield1.hdf5
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getYieldTable.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getYieldTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..26eef020cab82acee2c80e88089df1790b281eab
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/getYieldTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/YieldTables/EAGLE/yieldtables.tar.gz
+tar -xf yieldtables.tar.gz
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/params.yml b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/params.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1754835b8746d27e2a9b21ef1dfbec98d27105a0
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/params.yml
@@ -0,0 +1,172 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98848e43    # 10^10 M_sun in grams
+  UnitLength_in_cgs:   3.08567758e21 # kpc in centimeters
+  UnitVelocity_in_cgs: 1e5           # km/s in centimeters per second
+  UnitCurrent_in_cgs:  1             # Amperes
+  UnitTemp_in_cgs:     1             # Kelvin
+
+Scheduler:
+  max_top_level_cells: 16
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   0.1   # The end time of the simulation (in internal units).
+  dt_min:     1e-10 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     0.1  # The maximal time-step size of the simulation (in internal units).
+  max_dt_RMS_factor: 0.25  # (Optional) Dimensionless factor for the maximal displacement allowed based on the RMS velocities.
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir:              snap  # snapshot directory
+  basename:            snapshot # Common part of the name of output files
+  time_first:          0.    # Time of the first output (in internal units)
+  delta_time:          1e-2  # Time difference between consecutive outputs (in internal units)
+  compression:         4
+  recording_triggers_part: [-1, -1]   # Not recording as we have many snapshots
+  recording_triggers_bpart: [-1, -1]  # Not recording as we have many snapshots 
+  
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1e-3 # Time between statistics output
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:          0.025               # Constant dimensionless multiplier for time integration.
+  max_physical_baryon_softening: 0.05  # Physical softening length (in internal units)
+  max_physical_DM_softening: 0.05  # Physical softening length (in internal units)
+  
+# Parameters for the hydrodynamics scheme
+SPH:  
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+  h_max:                 10.      # (Optional) Maximal allowed smoothing length in internal units. Defaults to FLT_MAX if unspecified.
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./galaxy_multi_component.hdf5     # The file to read
+  periodic:   0                                 # Non-periodic BCs
+  shift:    [750,750,750]                       # Centre the box
+
+# Parameters for the stars neighbour search
+Stars:
+  resolution_eta:        1.1642  # Target smoothing length in units of the mean inter-particle separation
+  h_tolerance:           7e-3
+  overwrite_birth_time:    1     # Make sure the stars in the ICs do not do any feedback
+  birth_time:             -1.    # by setting all of their birth times to -1  
+  timestep_age_threshold_Myr: 10.   # Age at which stars switch from young to old (in Mega-years).
+  max_timestep_young_Myr:     0.1   # Maximal time-step length of young stars (in Mega-years).
+  max_timestep_old_Myr:       1.0   # Maximal time-step length of old stars (in Mega-years).
+  luminosity_filename:   ./photometry
+
+# Standard EAGLE cooling options
+EAGLECooling:
+  dir_name:                ./coolingtables/  # Location of the Wiersma+09 cooling tables
+  H_reion_z:               7.5               # Redshift of Hydrogen re-ionization
+  H_reion_eV_p_H:          2.0               # Energy inject by Hydrogen re-ionization in electron-volt per Hydrogen atom
+  He_reion_z_centre:       3.5               # Redshift of the centre of the Helium re-ionization Gaussian
+  He_reion_z_sigma:        0.5               # Spread in redshift of the  Helium re-ionization Gaussian
+  He_reion_eV_p_H:         2.0               # Energy inject by Helium re-ionization in electron-volt per Hydrogen atom
+
+# PS2020 cooling parameters
+PS2020Cooling:
+  dir_name:                ./UV_dust1_CR1_G1_shield1.hdf5 # Location of the Ploeckinger+20 cooling tables
+  H_reion_z:               7.5               # Redshift of Hydrogen re-ionization (Planck 2018)
+  H_reion_eV_p_H:          2.0
+  He_reion_z_centre:       3.5               # Redshift of the centre of the Helium re-ionization Gaussian
+  He_reion_z_sigma:        0.5               # Spread in redshift of the  Helium re-ionization Gaussian
+  He_reion_eV_p_H:         2.0               # Energy inject by Helium re-ionization in electron-volt per Hydrogen atom
+  delta_logTEOS_subgrid_properties: 0.3      # delta log T above the EOS below which the subgrid properties use Teq assumption
+  rapid_cooling_threshold:          0.333333 # Switch to rapid cooling regime for dt / t_cool above this threshold.
+
+# Use solar abundances
+EAGLEChemistry:
+  init_abundance_metal:     0.0129   
+  init_abundance_Hydrogen:  0.7065   
+  init_abundance_Helium:    0.2806   
+  init_abundance_Carbon:    0.00207  
+  init_abundance_Nitrogen:  0.000836 
+  init_abundance_Oxygen:    0.00549  
+  init_abundance_Neon:      0.00141  
+  init_abundance_Magnesium: 0.000591 
+  init_abundance_Silicon:   0.000683 
+  init_abundance_Iron:      0.0011   
+ 
+# EAGLE star formation parameters
+EAGLEStarFormation:
+  SF_threshold:                      Zdep         # Zdep (Schaye 2004) or Subgrid
+  SF_model:                          PressureLaw  # PressureLaw (Schaye et al. 2008) or SchmidtLaw
+  KS_normalisation:                  1.515e-4     # The normalization of the Kennicutt-Schmidt law in Msun / kpc^2 / yr.
+  KS_exponent:                       1.4          # The exponent of the Kennicutt-Schmidt law.
+  min_over_density:                  100.0        # The over-density above which star-formation is allowed.
+  KS_high_density_threshold_H_p_cm3: 1e3          # Hydrogen number density above which the Kennicut-Schmidt law changes slope in Hydrogen atoms per cm^3.
+  KS_high_density_exponent:          2.0          # Slope of the Kennicut-Schmidt law above the high-density threshold.
+  EOS_entropy_margin_dex:            0.3          # When using Z-based SF threshold, logarithm base 10 of the maximal entropy above the EOS at which stars can form.
+  threshold_norm_H_p_cm3:            0.1          # When using Z-based SF threshold, normalisation of the metal-dependant density threshold for star formation in Hydrogen atoms per cm^3.
+  threshold_Z0:                      0.002        # When using Z-based SF threshold, reference metallicity (metal mass fraction) for the metal-dependant threshold for star formation.
+  threshold_slope:                   -0.64        # When using Z-based SF threshold, slope of the metal-dependant star formation threshold
+  threshold_max_density_H_p_cm3:     10.0         # When using Z-based SF threshold, maximal density of the metal-dependant density threshold for star formation in Hydrogen atoms per cm^3.
+  threshold_temperature1_K:          1000         # When using subgrid-based SF threshold, subgrid temperature below which gas is star-forming.
+  threshold_temperature2_K:          31622        # When using subgrid-based SF threshold, subgrid temperature below which gas is star-forming if also above the density limit.
+  threshold_number_density_H_p_cm3:  10           # When using subgrid-based SF threshold, subgrid number density above which gas is star-forming if also below the second temperature limit.
+  
+# Parameters for the EAGLE "equation of state"
+EAGLEEntropyFloor:
+  Jeans_density_threshold_H_p_cm3: 1e-4      # Physical density above which the EAGLE Jeans limiter entropy floor kicks in expressed in Hydrogen atoms per cm^3.
+  Jeans_over_density_threshold:    10.       # Overdensity above which the EAGLE Jeans limiter entropy floor can kick in.
+  Jeans_temperature_norm_K:        800       # Temperature of the EAGLE Jeans limiter entropy floor at the density threshold expressed in Kelvin.
+  Jeans_gamma_effective:           1.3333333 # Slope the of the EAGLE Jeans limiter entropy floor
+  Cool_density_threshold_H_p_cm3: 1e-5       # Physical density above which the EAGLE Cool limiter entropy floor kicks in expressed in Hydrogen atoms per cm^3.
+  Cool_over_density_threshold:    10.        # Overdensity above which the EAGLE Cool limiter entropy floor can kick in.
+  Cool_temperature_norm_K:        10.        # Temperature of the EAGLE Cool limiter entropy floor at the density threshold expressed in Kelvin. (NOTE: This is below the min T and hence this floor does nothing)
+  Cool_gamma_effective:           1.         # Slope the of the EAGLE Cool limiter entropy floor
+
+# EAGLE feedback model with constant feedback energy fraction
+EAGLEFeedback:
+  use_SNII_feedback:                    1               # Global switch for SNII thermal (stochastic) feedback.
+  use_SNIa_feedback:                    1               # Global switch for SNIa thermal (continuous) feedback.
+  use_AGB_enrichment:                   1               # Global switch for enrichement from AGB stars.
+  use_SNII_enrichment:                  1               # Global switch for enrichement from SNII stars.
+  use_SNIa_enrichment:                  1               # Global switch for enrichement from SNIa stars.
+  filename:                             ./yieldtables/  # Path to the directory containing the EAGLE yield tables.
+  IMF_min_mass_Msun:                    0.1             # Minimal stellar mass considered for the Chabrier IMF in solar masses.
+  IMF_max_mass_Msun:                  100.0             # Maximal stellar mass considered for the Chabrier IMF in solar masses.
+  SNII_min_mass_Msun:                   8.0             # Minimal mass considered for SNII stars in solar masses.
+  SNII_max_mass_Msun:                 100.0             # Maximal mass considered for SNII stars in solar masses.
+  SNII_feedback_model:                  MinimumDistance # Feedback modes: Random, Isotropic, MinimumDistance, MinimumDensity
+  SNII_sampled_delay:                   1               # Sample the SNII lifetimes to do feedback.
+  SNII_delta_T_K:                       3.16228e7       # Change in temperature to apply to the gas particle in a SNII thermal feedback event in Kelvin.
+  SNII_delta_v_km_p_s:                  200             # Velocity kick applied by the stars when doing SNII feedback (in km/s).
+  SNII_energy_erg:                      1.0e51          # Energy of one SNII explosion in ergs.
+  SNII_energy_fraction_function:        EAGLE           # Type of functional form to use for scaling the energy fraction with density and metallicity ('EAGLE', 'Separable', or 'Independent').
+  SNII_energy_fraction_min:             1.0             # Minimal fraction of energy applied in a SNII feedback event.
+  SNII_energy_fraction_max:             1.0             # Maximal fraction of energy applied in a SNII feedback event.
+  SNII_energy_fraction_Z_0:             0.0012663729    # Pivot point for the metallicity dependance of the SNII energy fraction (metal mass fraction).
+  SNII_energy_fraction_n_0_H_p_cm3:     1.4588          # Pivot point for the birth density dependance of the SNII energy fraction in cm^-3.
+  SNII_energy_fraction_n_Z:             0.8686          # Power-law for the metallicity dependance of the SNII energy fraction.
+  SNII_energy_fraction_n_n:             0.8686          # Power-law for the birth density dependance of the SNII energy fraction.
+  SNII_energy_fraction_use_birth_density: 0             # Are we using the density at birth to compute f_E or at feedback time?
+  SNII_energy_fraction_use_birth_metallicity: 0         # Are we using the metallicity at birth to compuote f_E or at feedback time?
+  SNIa_DTD:                             Exponential     # Functional form of the SNIa delay time distribution.
+  SNIa_DTD_delay_Gyr:                   0.04            # Stellar age after which SNIa start in Gyr (40 Myr corresponds to stars ~ 8 Msun).
+  SNIa_DTD_exp_timescale_Gyr:           2.0             # Time-scale of the exponential decay of the SNIa rates in Gyr.
+  SNIa_DTD_exp_norm_p_Msun:             0.002           # Normalisation of the SNIa rates in inverse solar masses.
+  SNIa_energy_erg:                     1.0e51           # Energy of one SNIa explosion in ergs.
+  AGB_ejecta_velocity_km_p_s:          10.0             # Velocity of the AGB ejectas in km/s.
+  stellar_evolution_age_cut_Gyr:        0.1             # Age in Gyr above which the enrichment is downsampled.
+  stellar_evolution_sampling_rate:       10             # Number of time-steps in-between two enrichment events for a star above the age threshold.
+  SNII_yield_factor_Hydrogen:           1.0             # (Optional) Correction factor to apply to the Hydrogen yield from the SNII channel.
+  SNII_yield_factor_Helium:             1.0             # (Optional) Correction factor to apply to the Helium yield from the SNII channel.
+  SNII_yield_factor_Carbon:             0.5             # (Optional) Correction factor to apply to the Carbon yield from the SNII channel.
+  SNII_yield_factor_Nitrogen:           1.0             # (Optional) Correction factor to apply to the Nitrogen yield from the SNII channel.
+  SNII_yield_factor_Oxygen:             1.0             # (Optional) Correction factor to apply to the Oxygen yield from the SNII channel.
+  SNII_yield_factor_Neon:               1.0             # (Optional) Correction factor to apply to the Neon yield from the SNII channel.
+  SNII_yield_factor_Magnesium:          2.0             # (Optional) Correction factor to apply to the Magnesium yield from the SNII channel.
+  SNII_yield_factor_Silicon:            1.0             # (Optional) Correction factor to apply to the Silicon yield from the SNII channel.
+  SNII_yield_factor_Iron:               0.5             # (Optional) Correction factor to apply to the Iron yield from the SNII channel.
+
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/run.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..24d9f80f4c46f16e3a57b86008086659657f07a1
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/EAGLE/run.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+if [ ! -e galaxy_multi_component.hdf5 ]
+then
+    echo "Fetching initial conditions to run the example..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolatedGalaxies/galaxy_multi_component.hdf5
+fi
+
+if [ ! -e coolingtables ] 
+then     
+    echo "Fetching EAGLE cooling tables for the isolated galaxy example..."
+    ./getEagleCoolingTable.sh
+fi
+
+if [ ! -e yieldtables ] 
+then     
+    echo "Fetching EAGLE stellar yield tables for the isolated galaxy example..."
+    ./getYieldTable.sh
+fi
+
+if [ ! -e photometry ]
+then
+    echo "Fetching EAGLE photometry tables..."
+    ./getEaglePhotometryTable.sh
+fi
+
+
+
+printf "Running simulation..."
+
+../../../../swift --threads=14 --feedback --self-gravity --stars --star-formation --cooling --hydro --limiter --sync params.yml 2>&1 | tee output.log
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/README b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/README
new file mode 100644
index 0000000000000000000000000000000000000000..452eb5ee93856176add2ad7100039a86166dbf5c
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/README
@@ -0,0 +1,25 @@
+We show here how to run such initial conditions with the GEAR model.
+
+To generate the initial conditions locally, if pNbody is installed, run:
+
+../makeDisk.py
+
+If you don't have access to pNbody, you can simply skip this step. The initial conditions will automatically 
+be downloaded when launching the run script (see below).
+
+
+To run this example with the GEAR model, SWIFT must be configured with the following options:
+
+./configure --with-subgrid=GEAR --with-grackle=${GRACKLE_ROOT}
+
+To start the simulation with the GEAR model:
+
+./run.sh
+
+# Sink particles
+
+Sink particles are already included in the option `--with-subgrid=GEAR`.
+
+To start the simulation with the sink particles, type:
+
+./run_sink.sh
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/getChemistryTable.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/getChemistryTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b10fd23964158ee7a38d352dbd0ddd9beb7bdd77
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/getChemistryTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/FeedbackTables/POPIIsw.h5
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/getGrackleCoolingTable.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/getGrackleCoolingTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e3eb106240709c80151a48625567d2cd78e5f568
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/getGrackleCoolingTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012.h5
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/params.yml b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/params.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e5a251acf6377a9e1a380399d229c0754a7c54f2
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/params.yml
@@ -0,0 +1,122 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98848e43    # 10^10 M_sun in grams
+  UnitLength_in_cgs:   3.08567758e21 # kpc in centimeters
+  UnitVelocity_in_cgs: 1e5           # km/s in centimeters per second
+  UnitCurrent_in_cgs:  1             # Amperes
+  UnitTemp_in_cgs:     1             # Kelvin
+
+Scheduler:
+  max_top_level_cells: 16
+  cell_extra_gparts:   1000      # (Optional) Number of spare gparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sinks:    1000      # (Optional) Number of spare sinks per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sparts:   1000      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   0.1   # The end time of the simulation (in internal units).
+  dt_min:     1e-10 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     0.1  # The maximal time-step size of the simulation (in internal units).
+  max_dt_RMS_factor: 0.25  # (Optional) Dimensionless factor for the maximal displacement allowed based on the RMS velocities.
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir:              snap  # snapshot directory
+  basename:            snapshot # Common part of the name of output files
+  time_first:          0.    # Time of the first output (in internal units)
+  delta_time:          1e-2  # Time difference between consecutive outputs (in internal units)
+  compression:         4
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          1e-3 # Time between statistics output
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:          0.025               # Constant dimensionless multiplier for time integration.
+  max_physical_baryon_softening: 0.05  # Physical softening length (in internal units)
+  max_physical_DM_softening: 0.05  # Physical softening length (in internal units)
+  
+# Parameters for the hydrodynamics scheme
+SPH:  
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+  h_max:                 10.      # (Optional) Maximal allowed smoothing length in internal units. Defaults to FLT_MAX if unspecified.
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./galaxy_multi_component.hdf5     # The file to read
+  periodic:   0                                 # Non-periodic BCs
+  shift:    [750,750,750]                       # Centre the box
+
+# Cooling with Grackle 2.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5 # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 1 # Enable or not the UV background
+  redshift: 0 # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 1 # Enable or not the metal cooling
+  provide_volumetric_heating_rates: 0 # User provide volumetric heating rates
+  provide_specific_heating_rates: 0 # User provide specific heating rates
+  self_shielding_method: -1 # Grackle (<= 3) or Gear self shielding method
+  self_shielding_threshold_atom_per_cm3: 0.007  # Required only with GEAR's self shielding. Density threshold of the self shielding
+  max_steps: 1000
+  convergence_limit: 1e-2
+  thermal_time_myr: 5
+  maximal_density_Hpcm3: -1 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+GEARStarFormation:
+  star_formation_efficiency: 0.01     # star formation efficiency (c_*)
+  maximal_temperature_K:     3e4      # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   0.1      # Density threshold in Hydrogen atoms/cm3
+  n_stars_per_particle: 4
+  min_mass_frac: 0.5
+
+GEARSink:
+  use_fixed_cut_off_radius: 1                 # Are we using a fixed cutoff radius? If we are, in GEAR the cutoff radius is fixed at the value specified below, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+  cut_off_radius: 5e-3                        # Cut off radius of all the sinks in internal units. Ignored if use_fixed_cut_off_radius is 0. 
+  f_acc: 0.1
+  temperature_threshold_K: 1e4               # Max temperature (in K) for forming a sink when density_threshold_Hpcm3 <= density <= maximal_density_threshold_Hpcm3.
+  density_threshold_Hpcm3: 1e0               # Minimum gas density (in Hydrogen atoms/cm3) required to form a sink particle.
+  maximal_density_threshold_Hpcm3: 1e5       # If the gas density exceeds this value (in Hydrogen atoms/cm3), a sink forms regardless of temperature if all other criteria are passed.
+  stellar_particle_mass_Msun: 1e5            # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_Msun: 30             # Minimal mass of stars represented by discrete particles, in solar mass
+  stellar_particle_mass_first_stars_Msun: 1e5 # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_first_stars_Msun: 30  # Minimal mass of stars represented by discrete particles, in solar mass
+  star_spawning_sigma_factor: 0.5             # Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+  sink_formation_contracting_gas_criterion: 1     # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+  sink_formation_smoothing_length_criterion: 0    # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+  sink_formation_jeans_instability_criterion: 1   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+  sink_formation_bound_state_criterion: 1         # (Optional) Activate the bound state check for sink formation. (Default: 1)
+  sink_formation_overlapping_sink_criterion: 1    # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+  disable_sink_formation: 0                       # (Optional) Disable sink formation. (Default: 0)
+
+  # Timesteps parameters
+  CFL_condition:                        0.5       # Courant-Friedrich-Levy condition for time integration.
+  timestep_age_threshold_unlimited_Myr: 100.      # (Optional) Age above which sinks have no time-step restriction any more (in Mega-years). Defaults to 0.
+  timestep_age_threshold_Myr:           25.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_young_Myr:                1.       # (Optional) Maximal time-step length of young sinks (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_old_Myr:                  5.       # (Optional) Maximal time-step length of old sinks (in Mega-years). Defaults to FLT_MAX.
+  n_IMF:                                 2        # (Optional) Number of times the IMF mass can be swallowed in a single timestep. (Default: FLTM_MAX)
+
+
+GEARPressureFloor:
+  jeans_factor: 10
+
+GEARFeedback:
+  supernovae_energy_erg: 1e51
+  supernovae_efficiency: 0.1
+  yields_table: POPIIsw.h5
+  discrete_yields: 1
+  yields_table_first_stars: POPIIsw.h5          # Table containing the yields of the first stars.
+  metallicity_max_first_stars: -5                          # Maximal metallicity (in mass fraction) for a first star (-1 to deactivate).
+  elements: [Fe, Mg, O, C, Al, Ca, Ba, Zn, Eu]             # Elements to read in the yields table. The number of element should be one less than the number of elements (N) requested during the configuration (--with-chemistry=GEAR_N).
+
+GEARChemistry:
+  initial_metallicity: 1
+  scale_initial_metallicity: 1
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/run.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e2cab84f9432afbfb4990aa2184cd1e32814dd14
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/run.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+if [ ! -e galaxy_multi_component.hdf5 ]
+then
+    echo "Fetching initial conditions to run the example..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolatedGalaxies/galaxy_multi_component.hdf5
+fi
+
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ./getGrackleCoolingTable.sh
+fi
+
+
+if [ ! -e POPIIsw.h5 ]
+then
+    echo "Fetching the chemistry tables..."
+    ./getChemistryTable.sh
+fi
+
+printf "Running simulation..."
+
+../../../../swift --hydro --stars --star-formation --self-gravity --feedback --cooling --threads=14 params.yml 2>&1 | tee output.log 
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/run_sink.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/run_sink.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f3b7b9fa73582e73ad499029eddb53cb46453617
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR/run_sink.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+if [ ! -e galaxy_multi_component.hdf5 ]
+then
+    echo "Fetching initial conditions to run the example..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolatedGalaxies/galaxy_multi_component.hdf5
+fi
+
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ./getGrackleCoolingTable.sh
+fi
+
+
+if [ ! -e POPIIsw.h5 ]
+then
+    echo "Fetching the chemistry tables..."
+    ./getChemistryTable.sh
+fi
+
+printf "Running simulation with sink particles..."
+
+../../../../swift --hydro --stars --sinks --self-gravity --feedback --cooling --threads=14 params.yml 2>&1 | tee output.log
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/README b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/README
new file mode 100644
index 0000000000000000000000000000000000000000..3e90356706ef5c6fcd5f81902600aa72afc60731
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/README
@@ -0,0 +1,13 @@
+These examples show how to run Swift on initial conditions of a multi-component 
+isolated galactic disk generated with the pNbody code (http://lastro.epfl.ch/projects/pNbody/).
+
+In the current example, the galactic disk is composed of an exponential disk, a Plummer bulge,
+a Miyamoto-Nagai gaseous disk all three embedded in an NFW halo.
+The systems is put at equilibrium by solving the Jeans equations.
+
+AGORA  : folder containing information on how to run with the AGORA sub-grid model
+EAGLE  : folder containing information on how to run with the EAGLE sub-grid model
+GEAR   : folder containing information on how to run with the GEAR  sub-grid model
+
+makeDisk.py : script to generate the ICs with pNbody 
+If pNbody is not installed ICs will be downloaded when running the examples
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/makeDisk.py b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/makeDisk.py
new file mode 100755
index 0000000000000000000000000000000000000000..39d4c9419a51219c58c82c73652b351f8b9cbdd4
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_multi_component/makeDisk.py
@@ -0,0 +1,601 @@
+#!/usr/bin/env python3
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Yves Revaz (yves.revaz@epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+
+from pNbody import *
+from pNbody import ic
+from astropy import units as u
+from astropy import constants as c
+from pNbody import thermodyn, ctes
+
+#################################################################
+# Units
+#################################################################
+
+UnitLength_in_cm = 3.085e21
+UnitMass_in_g = 4.435693e44
+UnitVelocity_in_cm_per_s = 97824708.2699
+
+k = (
+    c.k_B.to(u.g * u.cm ** 2 / u.s ** 2 / u.K)
+    / UnitVelocity_in_cm_per_s ** 2
+    / UnitMass_in_g
+).value  # Boltzman constant   in code units
+m_p = (c.m_p.to(u.g) / UnitMass_in_g).value  # proton mass         in code units
+
+kmstoCodeUnits = (1 / (UnitVelocity_in_cm_per_s * u.cm / u.s).to(u.km / u.s)).value
+MsoltoCodeUnits = 1 / (UnitMass_in_g * u.g).to(u.M_sun).value
+
+#################################################################
+# model parameters
+#################################################################
+
+
+m_ref = 200000.0  # mass of gas particles, in solar mass
+nf = 1  # particle number multiplicative factor (used to reduce noise)
+hydro = 1  # 1=gas is treated with SPH, 0=gas will be collision-less
+
+
+# mass ratio between components
+fm_gas = 1.0
+fm_disk = 1.0
+fm_bulge = 1.0
+fm_halo = 5.0
+
+# Stellar disk
+if fm_disk == 0:
+    M_disk = 0
+else:
+    M_disk = 2e10 * MsoltoCodeUnits  # Stellar disk total mass
+Hr_disk = 2.0
+Hz_disk = 0.3
+fr_disk = 10.0
+fz_disk = 10.0
+toomre_disk = 2.0
+
+# Dark halo
+if fm_halo == 0:
+    M_halo = 0
+else:
+    M_halo = 100e10 * MsoltoCodeUnits  # Dark halo total mass
+Hr_halo = 50.0  # for plummer=30, for nfw, determined rmax
+Rs_halo = 15.0  # for nfw
+dR_halo = 0.25
+fr_halo = 3.0
+
+# Bulge
+if fm_bulge == 0:
+    M_bulge = 0
+else:
+    M_bulge = 0.4e10 * MsoltoCodeUnits  # Bulge total mass
+Hr_bulge = 1.0
+fr_bulge = 5.0
+
+# Gas disk
+if fm_gas == 0:
+    M_gas = 0
+else:
+    M_gas = 0.5e10 * MsoltoCodeUnits  # Gas disk total mass
+Hz_gas = 0.3
+Hr_gas = 4.0
+Rf = 40.0
+Hr_gas = Hr_gas - Hz_gas
+rmax_gas = 10 * Hr_gas
+zmax_gas = 3 * Hz_gas
+sigmavel_gas = 10 * kmstoCodeUnits  # km/s to code units
+T_gas = 100  # K
+
+
+# estimation of the gravitational softening length
+eps = 0.05 / (m_ref / 200000) ** (1 / 3.0)
+
+m_ref = m_ref * MsoltoCodeUnits  # to code units
+
+
+#################################################################
+# parameters for the velocities
+#################################################################
+
+ErrTolTheta = 0.5
+AdaptativeSoftenning = False
+
+
+###################################
+# spherical components
+###################################
+
+
+# grid parameters halo
+stats_name_halo = "stats_halo.dmp"
+grmin_halo = 0  # grid minimal radius
+grmax_halo = Hr_halo * fr_halo * 1.05  # grid maximal radius
+nr_halo = 64  # number of radial bins
+eps_halo = eps
+# grid bins functions
+rc_halo = Hr_halo / 5.0
+
+
+def g_halo(r):
+    return np.log(r / rc_halo + 1.0)
+
+
+def gm_halo(r):
+    return rc_halo * (np.exp(r) - 1.0)
+
+
+# grid parameters bulge
+stats_name_bulge = "stats_bulge.dmp"
+grmin_bulge = 0  # grid minimal radius
+grmax_bulge = Hr_bulge * fr_bulge * 1.05  # grid maximal radius
+nr_bulge = 64  # number of radial bins
+eps_bulge = eps
+# grid bins functions
+rc_bulge = Hr_halo
+
+
+def g_bulge(r):
+    return np.log(r / rc_bulge + 1.0)
+
+
+def gm_bulge(r):
+    return rc_bulge * (np.exp(r) - 1.0)
+
+
+###################################
+# cylindrical components
+###################################
+
+# grid parameters disk
+stats_name_disk = "stats_disk.dmp"
+grmin_disk = 0.0  # minimal grid radius
+grmax_disk = Hr_disk * fr_disk  # maximal grid radius
+gzmin_disk = -Hz_disk * fz_disk  # minimal grid z
+gzmax_disk = Hz_disk * fz_disk  # maximal grid z
+nr_disk = 32  # number of bins in r
+nt_disk = 2  # number of bins in t
+nz_disk = 64 + 1  # number of bins in z
+# for an even value of nz, the potential is computed at z=0
+# for an odd  value of nz, the density   is computed at z=0
+eps_disk = eps
+# grid bins functions
+rc_disk = 3.0
+
+
+def g_disk(r):
+    return np.log(r / rc_disk + 1.0)
+
+
+def gm_disk(r):
+    return rc_disk * (np.exp(r) - 1.0)
+
+
+mode_sigma_z = {"name": "jeans", "param": None}
+mode_sigma_r = {"name": "isothropic", "param": 2}
+mode_sigma_p = {"name": "epicyclic_approximation", "param": None}
+params_disk = [mode_sigma_z, mode_sigma_r, mode_sigma_p]
+
+
+# grid parameters gas
+stats_name_gas = "stats_gas.dmp"
+grmin_gas = 0.0  # minimal grid radius
+grmax_gas = rmax_gas * 1.05  # maximal grid radius
+gzmin_gas = -zmax_gas * 1.05  # minimal grid z
+gzmax_gas = zmax_gas * 1.05  # maximal grid z
+nr_gas = 32  # number of bins in r
+nt_gas = 2  # number of bins in t
+nz_gas = 64 + 1  # number of bins in z
+# for an even value of nz, the potential is computed at z=0
+# for an odd  value of nz, the density   is computed at z=0
+eps_gas = eps
+rc_gas = 3.0
+# grid bins functions
+def g_gas(r):
+    return np.log(r / rc_gas + 1.0)
+
+
+def gm_gas(r):
+    return rc_gas * (np.exp(r) - 1.0)
+
+
+mode_sigma_z = {"name": "jeans", "param": None}
+mode_sigma_r = {"name": "constant", "param": sigmavel_gas}
+mode_sigma_p = {"name": "epicyclic_approximation", "param": None}
+params_gas = [mode_sigma_z, mode_sigma_r, mode_sigma_p]
+
+
+#################################################################
+# compute the number of particles for each component
+#################################################################
+
+
+# here we give explicitly the mass of the gas particles
+m = m_ref
+
+if fm_gas == 0:
+    N_gas = 0
+else:
+    N_gas = int(M_gas / (m * fm_gas))
+
+if fm_disk == 0:
+    N_disk = 0
+else:
+    N_disk = int(M_disk / (m * fm_disk))
+
+if fm_bulge == 0:
+    N_bulge = 0
+else:
+    N_bulge = int(M_bulge / (m * fm_bulge))
+
+if fm_halo == 0:
+    N_halo = 0
+else:
+    N_halo = int(M_halo / (m * fm_halo))
+
+print("N_gas   = %d" % N_gas)
+print("N_disk  = %d" % N_disk)
+print("N_bulge = %d" % N_bulge)
+print("N_halo  = %d" % N_halo)
+print("----------------------------")
+print("N_tot   = %d" % (N_gas + N_disk + N_bulge + N_halo))
+print("----------------------------")
+
+
+print()
+if N_gas > 0:
+    print("m_gas   = %g Msol" % ((M_gas / N_gas) / MsoltoCodeUnits))
+
+if N_disk > 0:
+    print("m_disk  = %g Msol" % ((M_disk / N_disk) / MsoltoCodeUnits))
+
+if N_bulge > 0:
+    print("m_bulge = %g Msol" % ((M_bulge / N_bulge) / MsoltoCodeUnits))
+
+if N_halo > 0:
+    print("m_halo  = %g Msol" % ((M_halo / N_halo) / MsoltoCodeUnits))
+print()
+
+
+if nf > 1:
+    N_gas = int(nf * N_gas)
+    N_disk = int(nf * N_disk)
+    N_bulge = int(nf * N_bulge)
+    N_halo = int(nf * N_halo)
+
+
+#################################################################
+# generate models
+#################################################################
+
+
+#####################
+# exponnential disk
+#####################
+
+nb_disk = None
+if M_disk != 0.0:
+    print("generating disk...")
+    nb_disk = ic.expd(
+        N_disk,
+        Hr_disk,
+        Hz_disk,
+        fr_disk * Hr_disk,
+        fz_disk * Hz_disk,
+        irand=0,
+        ftype="gh5",
+    )
+    nb_disk.set_tpe("disk")
+    nb_disk.mass = (M_disk / N_disk) * np.ones(nb_disk.nbody).astype(np.float32)
+    nb_disk.rename("disk.dat")
+    nb_disk.write()
+
+
+#####################
+# halo
+#####################
+
+
+nb_halo = None
+if M_halo != 0.0:
+    print("generating halo...")
+    nb_halo = ic.nfw(N_halo, Rs_halo, fr_halo * Hr_halo, dR_halo, ftype="gh5")
+    nb_halo.set_tpe("halo")
+    nb_halo.mass = (M_halo / N_halo) * np.ones(nb_halo.nbody).astype(np.float32)
+    nb_halo.rename("halo.dat")
+    nb_halo.write()
+
+
+#####################
+# bulge
+#####################
+
+nb_bulge = None
+if M_bulge != 0.0:
+    print("generating bulge...")
+    nb_bulge = ic.plummer(
+        N_bulge, 1, 1, 1, Hr_bulge, fr_bulge * Hr_bulge, vel="no", ftype="gh5"
+    )
+    nb_bulge.set_tpe("bulge")
+    nb_bulge.mass = (M_bulge / N_bulge) * np.ones(nb_bulge.nbody).astype(np.float32)
+    nb_bulge.rename("bulge.dat")
+    nb_bulge.write()
+
+#####################
+# gas disk
+#####################
+
+nb_gas = None
+if M_gas != 0.0:
+    print("generating gas...")
+    nb_gas = ic.miyamoto_nagai(
+        N_gas, Hr_gas, Hz_gas, rmax_gas, zmax_gas, irand=-2, ftype="gh5"
+    )
+    nb_gas.set_tpe("gas")
+    nb_gas.mass = (M_gas / N_gas) * np.ones(nb_gas.nbody).astype(np.float32)
+    nb_gas.rename("gas.dat")
+    nb_gas.write()
+
+
+###############################################################
+# merge all components
+###############################################################
+
+nb = None
+
+if nb_disk is not None:
+    if nb is None:
+        nb = nb_disk
+    else:
+        nb = nb + nb_disk
+
+if nb_halo is not None:
+    if nb is None:
+        nb = nb_halo
+    else:
+        nb = nb + nb_halo
+
+if nb_bulge is not None:
+    if nb is None:
+        nb = nb_bulge
+    else:
+        nb = nb + nb_bulge
+
+if nb_gas is not None:
+    if nb is None:
+        nb = nb_gas
+    else:
+        nb = nb + nb_gas
+
+# save particles without velocities
+nb.write("snapnf.hdf5")
+
+###############################################################
+# compute velocities
+###############################################################
+
+
+if nb_disk is not None:
+    print("------------------------")
+    print("disk velocities...")
+    print("------------------------")
+
+    nb_disk, phi, stats_disk = nb.Get_Velocities_From_Cylindrical_Grid(
+        select="disk",
+        disk=("disk", "gas"),
+        eps=eps_disk,
+        nR=nr_disk,
+        nz=nz_disk,
+        nt=nt_disk,
+        Rmax=grmax_disk,
+        zmin=gzmin_disk,
+        zmax=gzmax_disk,
+        params=params_disk,
+        Phi=None,
+        g=g_disk,
+        gm=gm_disk,
+        ErrTolTheta=ErrTolTheta,
+        AdaptativeSoftenning=AdaptativeSoftenning,
+    )
+    iofunc.write_dmp(stats_name_disk, stats_disk)
+
+    r = stats_disk["R"]
+    z = stats_disk["z"]
+    dr = r[1] - r[0]
+    dz = z[nz_disk // 2 + 1] - z[nz_disk // 2]
+
+    print("disk : Delta R :", dr, "=", dr // eps_disk, "eps")
+    print("disk : Delta z :", dz, "=", dz // eps_disk, "eps")
+
+    # reduce if needed
+    if nf > 1:
+        nb_disk = nb_disk.reduc(nf, mass=True)
+
+
+if nb_gas is not None:
+    print("------------------------")
+    print("gas velocities...")
+    print("------------------------")
+    nb_gas, phi, stats_gas = nb.Get_Velocities_From_Cylindrical_Grid(
+        select="gas",
+        disk=("disk", "gas"),
+        eps=eps_gas,
+        nR=nr_gas,
+        nz=nz_gas,
+        nt=nt_gas,
+        Rmax=grmax_gas,
+        zmin=gzmin_gas,
+        zmax=gzmax_gas,
+        params=params_gas,
+        Phi=None,
+        g=g_gas,
+        gm=gm_gas,
+        ErrTolTheta=ErrTolTheta,
+        AdaptativeSoftenning=AdaptativeSoftenning,
+    )
+    iofunc.write_dmp(stats_name_gas, stats_gas)
+
+    r = stats_gas["R"]
+    z = stats_gas["z"]
+    dr = r[1] - r[0]
+    dz = z[nz_gas // 2 + 1] - z[nz_gas // 2]
+    print("gas   : Delta R :", dr, "=", dr / eps_gas, "eps")
+    print("gas   : Delta z :", dz, "=", dz / eps_gas, "eps")
+
+    # reduce if needed
+    if nf > 1:
+        nb_gas = nb_gas.reduc(nf, mass=True)
+
+
+if nb_bulge is not None:
+    print("------------------------")
+    print("bulge velocities...")
+    print("------------------------")
+    nb_bulge, phi, stats_bulge = nb.Get_Velocities_From_Spherical_Grid(
+        select="bulge",
+        eps=eps_bulge,
+        nr=nr_bulge,
+        rmax=grmax_bulge,
+        phi=None,
+        g=g_bulge,
+        gm=gm_bulge,
+        UseTree=True,
+        ErrTolTheta=ErrTolTheta,
+    )
+    iofunc.write_dmp(stats_name_bulge, stats_bulge)
+
+    r = stats_bulge["r"]
+    dr = r[1] - r[0]
+    print("bulge : Delta r :", dr, "=", dr / eps_bulge, "eps")
+
+    # reduce if needed
+    if nf > 1:
+        nb_bulge = nb_bulge.reduc(nf, mass=True)
+
+
+if nb_halo is not None:
+    print("------------------------")
+    print("halo velocities...")
+    print("------------------------")
+    nb_halo, phi, stats_halo = nb.Get_Velocities_From_Spherical_Grid(
+        select="halo",
+        eps=eps_halo,
+        nr=nr_halo,
+        rmax=grmax_halo,
+        phi=None,
+        g=g_halo,
+        gm=gm_halo,
+        UseTree=True,
+        ErrTolTheta=ErrTolTheta,
+    )
+    iofunc.write_dmp(stats_name_halo, stats_halo)
+
+    r = stats_halo["r"]
+    dr = r[1] - r[0]
+    print("halo : Delta r :", dr, "=", dr / eps_halo, "eps")
+
+    # reduce if needed
+    if nf > 1:
+        nb_halo = nb_halo.reduc(nf, mass=True)
+
+
+###############################################################
+# sum the different components and save the final model
+###############################################################
+
+nb = None
+
+if nb_disk is not None:
+    if nb is None:
+        nb = nb_disk
+    else:
+        nb = nb + nb_disk
+
+if nb_halo is not None:
+    if nb is None:
+        nb = nb_halo
+    else:
+        nb = nb + nb_halo
+
+if nb_bulge is not None:
+    if nb is None:
+        nb = nb_bulge
+    else:
+        nb = nb + nb_bulge
+
+if nb_gas is not None:
+    if nb is None:
+        nb = nb_gas
+    else:
+        nb = nb + nb_gas
+
+
+# reorganize components
+nb1 = nb.select("halo")
+nb1.set_tpe("halo_1")  # position expected by swift
+
+nb2 = nb.select("disk")
+nb2.set_tpe("stars_1")  # position expected by swift
+
+nb3 = nb.select("bulge")
+nb3.set_tpe("stars_1")  # position expected by swift
+
+nb4 = nb.select("gas")
+
+if hydro == 0:
+    nb4.set_tpe("stars_1")
+
+nb = nb1 + nb2 + nb3 + nb4
+
+# convert to swift
+nb = nb.set_ftype(ftype="swift")
+
+
+# add units
+np.UnitLength_in_cm = UnitLength_in_cm
+nb.UnitMass_in_g = UnitMass_in_g
+nb.UnitVelocity_in_cm_per_s = UnitVelocity_in_cm_per_s
+nb.Unit_time_in_cgs = UnitLength_in_cm / UnitVelocity_in_cm_per_s
+
+nb.boxsize = 150 * 10
+nb.rsp_init = np.ones(nb.nbody) * eps
+nb.birth_time_init = -1 * np.ones(nb.nbody)
+
+if hydro:
+    gamma = 5 / 3.0
+    xi = 0.76
+    ionisation = 0
+    mu = thermodyn.MeanWeight(xi, ionisation)
+    mumh = m_p * mu
+    nb.u_init = T_gas / (gamma - 1.0) * k / mumh * np.ones(nb.nbody)
+
+# save model
+nb.rename("galaxy_multi_component.hdf5")
+nb.write()
+
+#%%
+# Add the StellarParticleType attribute to the dataset
+import h5py as h5
+import numpy as np
+
+nb_star = nb.select("stars")
+N_star = np.sum(nb_star.npart)
+star_tpe = 2  # Single population stars
+star_type = np.ones(N_star) * star_tpe
+
+with h5.File("galaxy_multi_component.hdf5", "r+") as f:
+    f["PartType4"].create_dataset("StellarParticleType", data=star_type)
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_sink/README b/examples/IsolatedGalaxy/IsolatedGalaxy_sink/README
new file mode 100644
index 0000000000000000000000000000000000000000..131bff5820a3d07a3ad136c2096a74348ceee445
--- /dev/null
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_sink/README
@@ -0,0 +1,31 @@
+# Intro
+
+This example is an idealised galaxy with a geasous disk, a stellar bulge and disc. The dark matter halo is model using a external Hernquist potential. This example allows the use of sink particles for star formation.
+
+# Configure
+
+To run this example with GEAR model,
+
+```
+./configure --with-ext-potential=hernquist --with-chemistry=GEAR_10 --with-cooling=grackle_0 --with-stars=GEAR --with-star-formation=GEAR --with-feedback=GEAR --with-sink=GEAR --with-kernel=wendland-C2 --with-adaptive-softening --with-grackle=path/to/grackle
+```
+
+and then
+
+`make -j`
+
+You can remove the adaptive softening. In this case, you may need to change the default `max_physical_baryon_softening` value.
+
+Other sink models can be probed by changing swift configuration and adding the relevant parameters in params.yml.
+
+# ICs
+
+The run.sh script can download the initial conditions at different resolutions by changing `filename=...`. Do not forget to update the name in isolated_galaxy.yml.
+
+# Run
+
+Type `run.sh`, and let's go!
+
+# Changing the cooling
+
+You can change grackle mode to 1,2, or 3 if you want. You may also want to change the simulation end time.
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_sink/isolated_galaxy.yml b/examples/IsolatedGalaxy/IsolatedGalaxy_sink/isolated_galaxy.yml
index 91d66212f64feca5f2ed3ad8ab869c57cb36551c..c35f58e4a68c9b419c254d89c32092e92aacfac9 100644
--- a/examples/IsolatedGalaxy/IsolatedGalaxy_sink/isolated_galaxy.yml
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_sink/isolated_galaxy.yml
@@ -43,6 +43,7 @@ Statistics:
 InitialConditions:
   file_name:          lowres8.hdf5  # The file to read
   periodic:                    0    # Are we running with periodic ICs?
+  stars_smoothing_length:     5e-2  # (Optional) Set the smoothing length of all the stars to this value (disabled by default).
 
 # Parameters for the hydrodynamics scheme
 SPH:
@@ -87,4 +88,41 @@ DefaultSink:
   cut_off_radius: 0.1      # Cut off radius of all the sinks in internal units.
 
 GEARSink:
-  cut_off_radius: 0.1      # Cut off radius of all the sinks in internal units.
+  use_fixed_cut_off_radius: 1                 # Are we using a fixed cutoff radius? If we are, in GEAR the cutoff radius is fixed at the value specified below, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+  cut_off_radius: 5e-3                        # Cut off radius of all the sinks in internal units. Ignored if use_fixed_cut_off_radius is 0. 
+  f_acc: 1e-2
+  temperature_threshold_K: 5e3               # Max temperature (in K) for forming a sink when density_threshold_Hpcm3 <= density <= maximal_density_threshold_Hpcm3.
+  density_threshold_Hpcm3: 1e0               # Minimum gas density (in Hydrogen atoms/cm3) required to form a sink particle.
+  maximal_density_threshold_Hpcm3: 1e5       # If the gas density exceeds this value (in Hydrogen atoms/cm3), a sink forms regardless of temperature if all other criteria are passed.
+  stellar_particle_mass_Msun: 1e5            # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_Msun: 30             # Minimal mass of stars represented by discrete particles, in solar mass
+  stellar_particle_mass_first_stars_Msun: 1e5 # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_first_stars_Msun: 30  # Minimal mass of stars represented by discrete particles, in solar mass
+  star_spawning_sigma_factor: 0.5             # Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+  sink_formation_contracting_gas_criterion: 1     # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+  sink_formation_smoothing_length_criterion: 0    # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+  sink_formation_jeans_instability_criterion: 1   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+  sink_formation_bound_state_criterion: 1         # (Optional) Activate the bound state check for sink formation. (Default: 1)
+  sink_formation_overlapping_sink_criterion: 1    # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+  disable_sink_formation: 0                       # (Optional) Disable sink formation. (Default: 0)
+
+  # Timesteps parameters
+  CFL_condition:                        0.5       # Courant-Friedrich-Levy condition for time integration.
+  timestep_age_threshold_unlimited_Myr: 100.      # (Optional) Age above which sinks have no time-step restriction any more (in Mega-years). Defaults to 0.
+  timestep_age_threshold_Myr:           25.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_young_Myr:               0.5       # (Optional) Maximal time-step length of young sinks (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_old_Myr:                  1.       # (Optional) Maximal time-step length of old sinks (in Mega-years). Defaults to FLT_MAX.
+  n_IMF:                                 2.       # (Optional) Number of times the IMF mass can be swallowed in a single timestep. (Default: FLTM_MAX)
+
+GEARFeedback:
+  supernovae_energy_erg: 1e51
+  supernovae_efficiency: 0.1
+  yields_table: POPIIsw.h5
+  discrete_yields: 1
+  yields_table_first_stars: POPIIsw.h5          # Table containing the yields of the first stars.
+  metallicity_max_first_stars: -5                          # Maximal metallicity (in mass fraction) for a first star (-1 to deactivate).
+  elements: [Fe, Mg, O, C, Al, Ca, Ba, Zn, Eu]             # Elements to read in the yields table. The number of element should be one less than the number of elements (N) requested during the configuration (--with-chemistry=GEAR_N).
+
+GEARChemistry:
+  initial_metallicity: 0
+  scale_initial_metallicity: 0
diff --git a/examples/IsolatedGalaxy/IsolatedGalaxy_sink/run.sh b/examples/IsolatedGalaxy/IsolatedGalaxy_sink/run.sh
index d23ced0a634a913b111df4b22444726a8b3f7564..1e5ad37da5c50892cfbd5b932e4a75e0fed62f26 100755
--- a/examples/IsolatedGalaxy/IsolatedGalaxy_sink/run.sh
+++ b/examples/IsolatedGalaxy/IsolatedGalaxy_sink/run.sh
@@ -8,10 +8,20 @@ filename=lowres8.hdf5
 # filename=lowres512.hdf5
 # filename=highres6.hdf5
 
+# make run.sh fail if a subcommand fails
+set -e
+
 if [ ! -e $filename ]
 then
     echo "Fetching initial conditons for the isolated galaxy with an external potential ..."
     ./getIC.sh $filename
 fi
 
-../../../swift --hydro --sinks --stars --external-gravity --self-gravity --threads=8 isolated_galaxy.yml 2>&1 | tee output.log
+if [ ! -e POPIIsw.h5 ]
+then
+    echo "Fetching the chemistry tables..."
+    ./getChemistryTable.sh
+fi
+
+
+../../../swift --hydro --sinks --feedback --stars --external-gravity --self-gravity --threads=8 isolated_galaxy.yml 2>&1 | tee output.log
diff --git a/examples/IsolateddSph/IsolateddSph_core/README b/examples/IsolateddSph/IsolateddSph_core/README
new file mode 100644
index 0000000000000000000000000000000000000000..c55b76a4a2a41ead21e4002fdc0cada75471312f
--- /dev/null
+++ b/examples/IsolateddSph/IsolateddSph_core/README
@@ -0,0 +1,18 @@
+We show here how to run such initial conditions with Swift.
+
+To generate the initial conditions locally, if pNbody is installed, run:
+
+ic_gen_2_slopes+plummer -q --Rmin 1e-3 --Rmax 50  --M200 6.0e9 --c 30  --alpha 0 --beta 3 --Mtot 1e7 -a 0.5 --ptype1 4 --ptype2 1  --mass1 8000 --mass2  8000   -t swift -o dSph_core.hdf5
+
+If you don't have access to pNbody, you can simply skip this step. The initial conditions will automatically 
+be downloaded when launching the run script (see below).
+
+
+To run this example, SWIFT must be configured with the following options:
+
+./configure
+
+To start the simulation:
+
+./run.sh
+
diff --git a/examples/IsolateddSph/IsolateddSph_core/params.yml b/examples/IsolateddSph/IsolateddSph_core/params.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e6c9b793a8133a294b35a2e25bfec9d32e5d7ffa
--- /dev/null
+++ b/examples/IsolateddSph/IsolateddSph_core/params.yml
@@ -0,0 +1,41 @@
+#Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988e+43 # 10^10 Solar masses
+  UnitLength_in_cgs:   3.086e+21 # kpc
+  UnitVelocity_in_cgs: 1e5       # km / s
+  UnitCurrent_in_cgs:  1         # Amperes
+  UnitTemp_in_cgs:     1         # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin:          0.      # The starting time of the simulation (in internal units).
+  time_end:            0.1     # The end time of the simulation (in internal units).
+  dt_min:              1e-11   # The minimal time-step size of the simulation (in internal units).
+  dt_max:              1e-3    # The maximal time-step size of the simulation (in internal units).
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:                           0.025     # Constant dimensionless multiplier for time integration.
+  max_physical_DM_softening:     0.025     # Physical softening length (in internal units).
+  max_physical_baryon_softening: 0.025     # Physical softening length (in internal units).
+  rebuild_frequency:             0.00001   # Tree rebuild frequency
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir:              snap
+  basename:            snapshot  # Common part of the name of output files
+  time_first:          0.        # Time of the first output (in internal units)
+  delta_time:          .01       # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          2e-1    # Time between statistics output
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          dSph_core.hdf5 # The file to read
+  shift:              [100,100,100]
+  periodic:           0
diff --git a/examples/IsolateddSph/IsolateddSph_core/run.sh b/examples/IsolateddSph/IsolateddSph_core/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..158684f23065f05a791ff9e84d3d0cd71e22687d
--- /dev/null
+++ b/examples/IsolateddSph/IsolateddSph_core/run.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+if [ ! -e dSph_core.hdf5 ]
+then
+    echo "Fetching initial conditions to run the example..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolateddSph/dSph_core.hdf5
+fi
+
+printf "Running simulation..."
+
+../../../swift --stars --self-gravity --threads=14 params.yml 2>&1 | tee output.log 
diff --git a/examples/IsolateddSph/IsolateddSph_cups/README b/examples/IsolateddSph/IsolateddSph_cups/README
new file mode 100644
index 0000000000000000000000000000000000000000..646f5e8e80b8d8ad9a81437dc86bfb8fb60be3d6
--- /dev/null
+++ b/examples/IsolateddSph/IsolateddSph_cups/README
@@ -0,0 +1,18 @@
+We show here how to run such initial conditions with Swift.
+
+To generate the initial conditions locally, if pNbody is installed, run:
+
+ic_gen_2_slopes+plummer -q --Rmin 1e-3 --Rmax 50  --M200 5.0e9 --c 12  --alpha 1 --beta 3 --Mtot 1e7 -a 0.5 --ptype1 4 --ptype2 1  --mass1 8000 --mass2  8000   -t swift -o dSph_cusp.hdf5
+
+If you don't have access to pNbody, you can simply skip this step. The initial conditions will automatically 
+be downloaded when launching the run script (see below).
+
+
+To run this example, SWIFT must be configured with the following options:
+
+./configure
+
+To start the simulation:
+
+./run.sh
+
diff --git a/examples/IsolateddSph/IsolateddSph_cups/params.yml b/examples/IsolateddSph/IsolateddSph_cups/params.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f5cf87b5d41d13eb07fbe525723015d6445054b7
--- /dev/null
+++ b/examples/IsolateddSph/IsolateddSph_cups/params.yml
@@ -0,0 +1,41 @@
+#Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988e+43 # 10^10 Solar masses
+  UnitLength_in_cgs:   3.086e+21 # kpc
+  UnitVelocity_in_cgs: 1e5       # km / s
+  UnitCurrent_in_cgs:  1         # Amperes
+  UnitTemp_in_cgs:     1         # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin:          0.      # The starting time of the simulation (in internal units).
+  time_end:            0.1     # The end time of the simulation (in internal units).
+  dt_min:              1e-11   # The minimal time-step size of the simulation (in internal units).
+  dt_max:              1e-3    # The maximal time-step size of the simulation (in internal units).
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:                           0.025     # Constant dimensionless multiplier for time integration.
+  max_physical_DM_softening:     0.025     # Physical softening length (in internal units).
+  max_physical_baryon_softening: 0.025     # Physical softening length (in internal units).
+  rebuild_frequency:             0.00001   # Tree rebuild frequency
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir:              snap
+  basename:            snapshot  # Common part of the name of output files
+  time_first:          0.        # Time of the first output (in internal units)
+  delta_time:          .01       # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          2e-1    # Time between statistics output
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          dSph_cusp.hdf5 # The file to read
+  shift:              [100,100,100]
+  periodic:           0
diff --git a/examples/IsolateddSph/IsolateddSph_cups/run.sh b/examples/IsolateddSph/IsolateddSph_cups/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..af1d314664c9341299932216b57169c02c987b94
--- /dev/null
+++ b/examples/IsolateddSph/IsolateddSph_cups/run.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+if [ ! -e dSph_cusp.hdf5 ]
+then
+    echo "Fetching initial conditions to run the example..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/IsolateddSph/dSph_cusp.hdf5
+fi
+
+printf "Running simulation..."
+
+../../../swift --stars --self-gravity --threads=14 params.yml 2>&1 | tee output.log 
diff --git a/examples/IsolateddSph/README b/examples/IsolateddSph/README
new file mode 100644
index 0000000000000000000000000000000000000000..765ffc4e667a1a0fb797fbdda8bdc1d239fb6fd0
--- /dev/null
+++ b/examples/IsolateddSph/README
@@ -0,0 +1,9 @@
+These examples show how to run Swift on initial conditions of a two-component (gravity-only)
+dwarf model generated with the pNbody code (http://lastro.epfl.ch/projects/pNbody/).
+
+In the current example, the dwarf galaxy is composed of a Plummer spheroid 
+embedded in a cuspy or cored halo.
+The systems is put at equilibrium by sampling a distribution function obtained from the Eddington equation.
+
+IsolateddSph_cusp  : folder containing information on how to run with the cuspy dark halo
+IsolateddSph_core  : folder containing information on how to run with the cored dark halo
diff --git a/examples/Planetary/DemoImpact/README.md b/examples/Planetary/DemoImpact/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8f8b9f279a2f0921cc2a817af294e28caf15b994
--- /dev/null
+++ b/examples/Planetary/DemoImpact/README.md
@@ -0,0 +1,31 @@
+A demo planetary simulation of a giant impact, using the initial conditions
+created by the DemoImpactInitCond example with SEAGen and WoMa [1,2].
+
+The scenario is a so-called canonical-like Moon-forming impact of a ~Mars-mass
+body onto the proto-Earth, at the mutual escape speed, with an impact angle of
+45 degrees, set up as described in [3].
+
+The initial conditions can be created via the DemoImpactInitCond example, see
+its README.md and the WoMa documentation for more details. Or the premade
+initial conditions can be downloaded directly with `get_init_cond.sh`.
+
+The default resolution is ~10^5 particles, set by the `N` variables at the start
+of the scripts. Note that here the simulation is cut short with an end time of
+just 15 h, for testing. Note also that this type of graze-and-merge collision is
+notoriously chaotic, so minor changes to the initial conditions can have large
+effects on the outcome, and a higher resolution than the default for this demo
+is required for convergence [3].
+
+If you have just run the initial settling simulations, then please ensure you
+have configured and compiled SWIFT without the fixed-entropy setting before
+running an impact simulation. You may find it convenient to rename your SWIFT
+executables according to their configuration.
+
+[1] Kegerreis et al. 2019, MNRAS 487:4
+[2] Ruiz-Bonilla et al. 2021, MNRAS 500:3
+[3] Kegerreis et al. 2022, ApJL 937:2 L40
+
+
+This example requires the code to be configured to use the planetary
+hydrodynamics scheme and planetary equations of state:
+    --with-hydro=planetary --with-equation-of-state=planetary
diff --git a/examples/Planetary/DemoImpact/demo_impact_n50.yml b/examples/Planetary/DemoImpact/demo_impact_n50.yml
new file mode 100755
index 0000000000000000000000000000000000000000..7bbb67c42f655297fabaf081d3e36e568881c53e
--- /dev/null
+++ b/examples/Planetary/DemoImpact/demo_impact_n50.yml
@@ -0,0 +1,64 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+    UnitMass_in_cgs:        1e27        # Sets Earth mass = 5.972
+    UnitLength_in_cgs:      1e8         # Sets Earth radius = 6.371
+    UnitVelocity_in_cgs:    1e8         # Sets time in seconds
+    UnitCurrent_in_cgs:     1           # Amperes
+    UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+    file_name:  demo_impact_n50.hdf5    # The initial conditions file to read
+    periodic:   0                       # Are we running with periodic ICs?
+
+# Parameters governing the time integration
+TimeIntegration:
+    time_begin:     0                   # The starting time of the simulation (in internal units).
+    time_end:       54000               # The end time of the simulation (in internal units).
+    dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
+    dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_impact_n50 # Common part of the name of output files
+    time_first:         0               # Time of the first output (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+    time_first: 0                       # Time of the first output (in internal units)
+    delta_time: 1000                    # Time between statistics output
+
+# Parameters controlling restarts
+Restarts:
+    enable: 0                           # Whether to enable dumping restarts at fixed intervals.
+
+# Parameters for the hydrodynamics scheme
+SPH:
+    resolution_eta:     1.2348          # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+    delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
+    CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
+    h_max:              1.0             # Maximal allowed smoothing length (in internal units).
+    viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
+
+# Parameters for the self-gravity scheme
+Gravity:
+    eta:                            0.025       # Constant dimensionless multiplier for time integration.
+    MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+    epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
+    theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
+    max_physical_baryon_softening:  0.16        # Physical softening length (in internal units).
+
+# Parameters for the task scheduling
+Scheduler:
+    max_top_level_cells:    64          # Maximal number of top-level cells in any dimension. The nu
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/DemoImpact/getICs.sh b/examples/Planetary/DemoImpact/getICs.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ac0d0ba1c9c669bd28be8e9091768a2d53232ce8
--- /dev/null
+++ b/examples/Planetary/DemoImpact/getICs.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/demo_impact_n50.hdf5
diff --git a/examples/Planetary/DemoImpact/plot_snapshots.py b/examples/Planetary/DemoImpact/plot_snapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..361a8df54ba81a0322d0069570fc637ac1bf74d3
--- /dev/null
+++ b/examples/Planetary/DemoImpact/plot_snapshots.py
@@ -0,0 +1,133 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##############################################################################
+
+"""Plot the snapshots from the DemoImpact example, colouring particles by material."""
+
+import os
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 5
+N_label = "n%d" % (10 * np.log10(N))
+
+# Plotting options
+font_size = 20
+params = {
+    "axes.labelsize": font_size,
+    "font.size": font_size,
+    "xtick.labelsize": font_size,
+    "ytick.labelsize": font_size,
+    "font.family": "serif",
+}
+matplotlib.rcParams.update(params)
+
+# Material colours
+Di_mat_colour = {"ANEOS_Fe85Si15": "darkgray", "ANEOS_forsterite": "orangered"}
+Di_id_colour = {woma.Di_mat_id[mat]: colour for mat, colour in Di_mat_colour.items()}
+
+# Scale point size with resolution
+size = (0.5 * np.cbrt(10 ** 6 / N)) ** 2
+
+
+def load_snapshot(filename):
+    """Load and convert the particle data to plot."""
+    with h5py.File(filename, "r") as f:
+        # Units from file metadata
+        file_to_SI = woma.Conversions(
+            m=float(f["Units"].attrs["Unit mass in cgs (U_M)"]) * 1e-3,
+            l=float(f["Units"].attrs["Unit length in cgs (U_L)"]) * 1e-2,
+            t=float(f["Units"].attrs["Unit time in cgs (U_t)"]),
+        )
+
+        # Particle data
+        A2_pos = (
+            np.array(f["PartType0/Coordinates"][()])
+            - 0.5 * f["Header"].attrs["BoxSize"]
+        ) * file_to_SI.l
+        A1_mat_id = np.array(f["PartType0/MaterialIDs"][()])
+
+    # Restrict to z < 0 for plotting
+    A1_sel = np.where(A2_pos[:, 2] < 0)[0]
+    A2_pos = A2_pos[A1_sel]
+    A1_mat_id = A1_mat_id[A1_sel]
+
+    return A2_pos, A1_mat_id
+
+
+def plot_snapshot(A2_pos, A1_mat_id):
+    """Plot the particles, coloured by their material."""
+    plt.figure(figsize=(7, 7))
+    ax = plt.gca()
+    ax.set_aspect("equal")
+
+    # Colour by material
+    A1_colour = np.empty(len(A2_pos), dtype=object)
+    for id_c, c in Di_id_colour.items():
+        A1_colour[A1_mat_id == id_c] = c
+
+    # Earth units
+    R_E = 6.3710e6  # m
+
+    # Plot
+    ax.scatter(
+        A2_pos[:, 0] / R_E,
+        A2_pos[:, 1] / R_E,
+        c=A1_colour,
+        edgecolors="none",
+        marker=".",
+        s=size,
+        alpha=0.5,
+    )
+
+    ax_lim = 10
+    ax.set_xlim(-ax_lim, ax_lim)
+    ax.set_yticks(ax.get_xticks())
+    ax.set_ylim(-ax_lim, ax_lim)
+    ax.set_xlabel(r"$x$ ($R_\oplus$)")
+    ax.set_ylabel(r"$y$ ($R_\oplus$)")
+
+    plt.tight_layout()
+
+
+if __name__ == "__main__":
+    print()
+
+    # Plot each snapshot
+    for snapshot_id in range(28):
+        # Load the data
+        A2_pos, A1_mat_id = load_snapshot(
+            "snapshots/demo_impact_%s_%04d.hdf5" % (N_label, snapshot_id)
+        )
+
+        # Plot the data
+        plot_snapshot(A2_pos, A1_mat_id)
+
+        # Save the figure
+        if not os.path.exists("plots/"):
+            os.makedirs("plots/")
+        save = "plots/demo_impact_%s_%04d.png" % (N_label, snapshot_id)
+        plt.savefig(save, dpi=200)
+        plt.close()
+
+        print("\rSaved %s" % save)
+
+    print()
diff --git a/examples/Planetary/DemoImpact/run.sh b/examples/Planetary/DemoImpact/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bddb8c8373af6bfe48af5756a52d6642dbee91e4
--- /dev/null
+++ b/examples/Planetary/DemoImpact/run.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Resolution
+N_label=n50
+
+# Copy or download the initial conditions if they are not present
+if [ ! -e demo_impact_"$N_label".hdf5 ]
+then
+    if [ -e ../DemoImpactInitCond/demo_impact_"$N_label".hdf5 ]
+    then
+        echo "Copying initial conditions from the DemoInitCond example..."
+        cp ../DemoImpactInitCond/demo_impact_"$N_label".hdf5 ./
+    else
+        echo "Downloading initial conditions..."
+        ./getICs.sh
+    fi
+fi
+
+# Download equation of state tables if not already present
+if [ ! -e ../EoSTables/ANEOS_forsterite_S19.txt ]
+then
+    cd ../EoSTables
+    ./get_eos_tables.sh
+    cd -
+fi
+
+# Run SWIFT
+../../../swift --hydro --self-gravity --threads=28 demo_impact_"$N_label".yml 2>&1 | tee output_"$N_label".txt
+
+# Plot the snapshots
+python3 plot_snapshots.py
diff --git a/examples/Planetary/DemoImpactInitCond/README.md b/examples/Planetary/DemoImpactInitCond/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..f4b0feae768de69c5a45221ca31e3b79698dcd40
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/README.md
@@ -0,0 +1,36 @@
+A demo of making initial conditions for a planetary impact simulation using
+SEAGen and WoMa [1,2], starting by creating separate target and impactor planet 
+models and running standard "settling" simulations. See WoMa's documentation and 
+tutorial at https://github.com/srbonilla/WoMa for more info.
+
+First, the target and impactor planet profiles and particles are generated using
+WoMa. Second, a "settling" simulation is run for each body in isolation to allow
+any final relaxation of the particles to occur. Third, the settled particles are
+loaded and given initial positions and velocities to set up the impact scenario.
+
+The resulting initial conditions can be used by the DemoImpact example. See its
+README.md for more info, and to run the impact simulation. Pre-made impact 
+initial conditions can also be downloaded directly.
+
+The set up for these planets and the collision scenario is described in [3] 
+(see S2 and Fig. A1, etc). These bodies use ANEOS equations of state that 
+include entropies, so for settling simulations the entropy of each particle can
+be kept fixed to ensure adiabatic settling without viscosity heating etc.
+
+The resolution, set by the number of ~equal-mass particles, can be controlled by
+the `N` variables at the start of the scripts, with examples included for 10^5
+(default), 10^6, and 10^7 particles. In the .yml input files for different
+resolutions, in addition to the different file names, the gravitational
+softening set by max_physical_baryon_softening is set to approximately the
+minimum inter-particle separation. See the planetary and other sections of the
+SWIFT documentation for details on the other .yml input parameters and other
+simulation options.
+
+[1] Kegerreis et al. 2019, MNRAS 487:4
+[2] Ruiz-Bonilla et al. 2021, MNRAS 500:3
+[3] Kegerreis et al. 2022, ApJL 937:2 L40
+
+
+This example requires the code to be configured to use the planetary
+hydrodynamics scheme and planetary equations of state, and fixed entropy:
+    --with-hydro=planetary --with-equation-of-state=planetary --enable-planetary-fixed-entropy
diff --git a/examples/Planetary/DemoImpactInitCond/demo_impactor_n50.yml b/examples/Planetary/DemoImpactInitCond/demo_impactor_n50.yml
new file mode 100755
index 0000000000000000000000000000000000000000..44618e11ada150a2e5a243be0ce9cd8ff79cabf7
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/demo_impactor_n50.yml
@@ -0,0 +1,60 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+    UnitMass_in_cgs:        1e27        # Sets Earth mass = 5.972
+    UnitLength_in_cgs:      1e8         # Sets Earth radius = 6.371
+    UnitVelocity_in_cgs:    1e8         # Sets time in seconds
+    UnitCurrent_in_cgs:     1           # Amperes
+    UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+    file_name:  demo_impactor_n50.hdf5  # The initial conditions file to read
+    periodic:   0                       # Are we running with periodic ICs?
+
+# Parameters governing the time integration
+TimeIntegration:
+    time_begin:     0                   # The starting time of the simulation (in internal units).
+    time_end:       10000               # The end time of the simulation (in internal units).
+    dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
+    dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_impactor_n50  # Common part of the name of output files
+    time_first:         0               # Time of the first output (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+    time_first: 0                       # Time of the first output (in internal units)
+    delta_time: 1000                    # Time between statistics output
+
+# Parameters controlling restarts
+Restarts:
+    enable: 0                           # Whether to enable dumping restarts at fixed intervals.
+
+# Parameters for the hydrodynamics scheme
+SPH:
+    resolution_eta:     1.2348          # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+    delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
+    CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
+    h_max:              1.0             # Maximal allowed smoothing length (in internal units).
+    viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
+
+# Parameters for the self-gravity scheme
+Gravity:
+    eta:                            0.025       # Constant dimensionless multiplier for time integration.
+    MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+    epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
+    theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
+    max_physical_baryon_softening:  0.16        # Physical softening length (in internal units).
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/DemoImpactInitCond/demo_impactor_n60.yml b/examples/Planetary/DemoImpactInitCond/demo_impactor_n60.yml
new file mode 100755
index 0000000000000000000000000000000000000000..c90693cb22a7eacc1cab225e8767859c8f901351
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/demo_impactor_n60.yml
@@ -0,0 +1,60 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+    UnitMass_in_cgs:        1e27        # Sets Earth mass = 5.972
+    UnitLength_in_cgs:      1e8         # Sets Earth radius = 6.371
+    UnitVelocity_in_cgs:    1e8         # Sets time in seconds
+    UnitCurrent_in_cgs:     1           # Amperes
+    UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+    file_name:  demo_impactor_n60.hdf5  # The initial conditions file to read
+    periodic:   0                       # Are we running with periodic ICs?
+
+# Parameters governing the time integration
+TimeIntegration:
+    time_begin:     0                   # The starting time of the simulation (in internal units).
+    time_end:       10000               # The end time of the simulation (in internal units).
+    dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
+    dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_impactor_n60  # Common part of the name of output files
+    time_first:         0               # Time of the first output (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+    time_first: 0                       # Time of the first output (in internal units)
+    delta_time: 1000                    # Time between statistics output
+
+# Parameters controlling restarts
+Restarts:
+    enable: 0                           # Whether to enable dumping restarts at fixed intervals.
+
+# Parameters for the hydrodynamics scheme
+SPH:
+    resolution_eta:     1.2348          # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+    delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
+    CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
+    h_max:              1.0             # Maximal allowed smoothing length (in internal units).
+    viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
+
+# Parameters for the self-gravity scheme
+Gravity:
+    eta:                            0.025       # Constant dimensionless multiplier for time integration.
+    MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+    epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
+    theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
+    max_physical_baryon_softening:  0.08        # Physical softening length (in internal units).
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/DemoImpactInitCond/demo_impactor_n70.yml b/examples/Planetary/DemoImpactInitCond/demo_impactor_n70.yml
new file mode 100755
index 0000000000000000000000000000000000000000..5c43237c95ee2d1f26a51c8ea3d6f416e6d443cf
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/demo_impactor_n70.yml
@@ -0,0 +1,64 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+    UnitMass_in_cgs:        1e27        # Sets Earth mass = 5.972
+    UnitLength_in_cgs:      1e8         # Sets Earth radius = 6.371
+    UnitVelocity_in_cgs:    1e8         # Sets time in seconds
+    UnitCurrent_in_cgs:     1           # Amperes
+    UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+    file_name:  demo_impactor_n70.hdf5  # The initial conditions file to read
+    periodic:   0                       # Are we running with periodic ICs?
+
+# Parameters governing the time integration
+TimeIntegration:
+    time_begin:     0                   # The starting time of the simulation (in internal units).
+    time_end:       10000               # The end time of the simulation (in internal units).
+    dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
+    dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_impactor_n70  # Common part of the name of output files
+    time_first:         0               # Time of the first output (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+    time_first: 0                       # Time of the first output (in internal units)
+    delta_time: 1000                    # Time between statistics output
+
+# Parameters controlling restarts
+Restarts:
+    enable:             1               # Whether to enable dumping restarts at fixed intervals.
+    save:               1               # Whether to save copies of the previous set of restart files (named .prev)
+    subdir:             restart         # Name of subdirectory for restart files.
+    basename:           demo_impactor_n70  # Prefix used in naming restart files.
+    delta_hours:        10.0            # Decimal hours between dumps of restart files.
+
+# Parameters for the hydrodynamics scheme
+SPH:
+    resolution_eta:     1.2348          # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+    delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
+    CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
+    h_max:              1.0             # Maximal allowed smoothing length (in internal units).
+    viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
+
+# Parameters for the self-gravity scheme
+Gravity:
+    eta:                            0.025       # Constant dimensionless multiplier for time integration.
+    MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+    epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
+    theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
+    max_physical_baryon_softening:  0.04        # Physical softening length (in internal units).
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/EarthImpact/earth_impact.yml b/examples/Planetary/DemoImpactInitCond/demo_target_n50.yml
old mode 100644
new mode 100755
similarity index 73%
rename from examples/Planetary/EarthImpact/earth_impact.yml
rename to examples/Planetary/DemoImpactInitCond/demo_target_n50.yml
index 39f561770efce4fbd41183a7462371f79bd8b95e..45fe23fa963dd314abe067790a0071d9792c8577
--- a/examples/Planetary/EarthImpact/earth_impact.yml
+++ b/examples/Planetary/DemoImpactInitCond/demo_target_n50.yml
@@ -7,22 +7,23 @@ InternalUnitSystem:
     UnitTemp_in_cgs:        1           # Kelvin
 
 # Parameters related to the initial conditions
-InitialConditions:      
-    file_name:  earth_impact.hdf5       # The initial conditions file to read
+InitialConditions:
+    file_name:  demo_target_n50.hdf5    # The initial conditions file to read
     periodic:   0                       # Are we running with periodic ICs?
 
 # Parameters governing the time integration
 TimeIntegration:
     time_begin:     0                   # The starting time of the simulation (in internal units).
-    time_end:       36000               # The end time of the simulation (in internal units).
+    time_end:       10000               # The end time of the simulation (in internal units).
     dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
     dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
 
 # Parameters governing the snapshots
 Snapshots:
-    basename:           earth_impact    # Common part of the name of output files
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_target_n50 # Common part of the name of output files
     time_first:         0               # Time of the first output (in internal units)
-    delta_time:         1000            # Time difference between consecutive outputs (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
 
 # Parameters governing the conserved quantities statistics
 Statistics:
@@ -38,7 +39,7 @@ SPH:
     resolution_eta:     1.2348          # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
     delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
     CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
-    h_max:              1.2             # Maximal allowed smoothing length (in internal units).
+    h_max:              1.0             # Maximal allowed smoothing length (in internal units).
     viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
 
 # Parameters for the self-gravity scheme
@@ -47,14 +48,13 @@ Gravity:
     MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
     epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
     theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
-    max_physical_baryon_softening:  0.05        # Physical softening length (in internal units).
-
-# Parameters for the task scheduling
-Scheduler:
-    max_top_level_cells:    16          # Maximal number of top-level cells in any dimension. The nu
+    max_physical_baryon_softening:  0.16        # Physical softening length (in internal units).
 
 # Parameters related to the equation of state
 EoS:
     # Select which planetary EoS material(s) to enable for use.
-    planetary_use_Til_iron:       1     # Tillotson iron, material ID 100
-    planetary_use_Til_granite:    1     # Tillotson granite, material ID 101
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/DemoImpactInitCond/demo_target_n60.yml b/examples/Planetary/DemoImpactInitCond/demo_target_n60.yml
new file mode 100755
index 0000000000000000000000000000000000000000..659f431eeb7ecb3d026dacd180255d8feb4aaf51
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/demo_target_n60.yml
@@ -0,0 +1,60 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+    UnitMass_in_cgs:        1e27        # Sets Earth mass = 5.972
+    UnitLength_in_cgs:      1e8         # Sets Earth radius = 6.371
+    UnitVelocity_in_cgs:    1e8         # Sets time in seconds
+    UnitCurrent_in_cgs:     1           # Amperes
+    UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+    file_name:  demo_target_n60.hdf5    # The initial conditions file to read
+    periodic:   0                       # Are we running with periodic ICs?
+
+# Parameters governing the time integration
+TimeIntegration:
+    time_begin:     0                   # The starting time of the simulation (in internal units).
+    time_end:       10000               # The end time of the simulation (in internal units).
+    dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
+    dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_target_n60 # Common part of the name of output files
+    time_first:         0               # Time of the first output (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+    time_first: 0                       # Time of the first output (in internal units)
+    delta_time: 1000                    # Time between statistics output
+
+# Parameters controlling restarts
+Restarts:
+    enable: 0                           # Whether to enable dumping restarts at fixed intervals.
+
+# Parameters for the hydrodynamics scheme
+SPH:
+    resolution_eta:     1.2348          # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+    delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
+    CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
+    h_max:              1.0             # Maximal allowed smoothing length (in internal units).
+    viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
+
+# Parameters for the self-gravity scheme
+Gravity:
+    eta:                            0.025       # Constant dimensionless multiplier for time integration.
+    MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+    epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
+    theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
+    max_physical_baryon_softening:  0.08        # Physical softening length (in internal units).
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/DemoImpactInitCond/demo_target_n70.yml b/examples/Planetary/DemoImpactInitCond/demo_target_n70.yml
new file mode 100755
index 0000000000000000000000000000000000000000..9c920cbdfa1b0941115bc466e59151e557969025
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/demo_target_n70.yml
@@ -0,0 +1,64 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+    UnitMass_in_cgs:        1e27        # Sets Earth mass = 5.972
+    UnitLength_in_cgs:      1e8         # Sets Earth radius = 6.371
+    UnitVelocity_in_cgs:    1e8         # Sets time in seconds
+    UnitCurrent_in_cgs:     1           # Amperes
+    UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+    file_name:  demo_target_n70.hdf5    # The initial conditions file to read
+    periodic:   0                       # Are we running with periodic ICs?
+
+# Parameters governing the time integration
+TimeIntegration:
+    time_begin:     0                   # The starting time of the simulation (in internal units).
+    time_end:       10000               # The end time of the simulation (in internal units).
+    dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
+    dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_target_n70 # Common part of the name of output files
+    time_first:         0               # Time of the first output (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+    time_first: 0                       # Time of the first output (in internal units)
+    delta_time: 1000                    # Time between statistics output
+
+# Parameters controlling restarts
+Restarts:
+    enable:             1               # Whether to enable dumping restarts at fixed intervals.
+    save:               1               # Whether to save copies of the previous set of restart files (named .prev)
+    subdir:             restart         # Name of subdirectory for restart files.
+    basename:           demo_target_n70 # Prefix used in naming restart files.
+    delta_hours:        10.0            # Decimal hours between dumps of restart files.
+
+# Parameters for the hydrodynamics scheme
+SPH:
+    resolution_eta:     1.2348          # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+    delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
+    CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
+    h_max:              1.0             # Maximal allowed smoothing length (in internal units).
+    viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
+
+# Parameters for the self-gravity scheme
+Gravity:
+    eta:                            0.025       # Constant dimensionless multiplier for time integration.
+    MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+    epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
+    theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
+    max_physical_baryon_softening:  0.04        # Physical softening length (in internal units).
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/DemoImpactInitCond/make_impact_init_cond.py b/examples/Planetary/DemoImpactInitCond/make_impact_init_cond.py
new file mode 100755
index 0000000000000000000000000000000000000000..28245075c8636c7b4b4dd603b1252f3e20b6cd8e
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/make_impact_init_cond.py
@@ -0,0 +1,113 @@
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+################################################################################
+
+"""Create the initial conditions for the DemoImpact example. See README.md for more info."""
+
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 5
+N_label = "n%d" % (10 * np.log10(N))
+
+# Earth units
+M_E = 5.9724e24  # kg
+R_E = 6.3710e6  # m
+
+# Set profile inputs
+M_t = 0.887 * M_E
+M_i = 0.133 * M_E
+
+# Load the settled particle planets
+def load_snapshot_data(filename):
+    with h5py.File(filename, "r") as f:
+        # Units from file metadata
+        file_to_SI = woma.Conversions(
+            m=float(f["Units"].attrs["Unit mass in cgs (U_M)"]) * 1e-3,
+            l=float(f["Units"].attrs["Unit length in cgs (U_L)"]) * 1e-2,
+            t=float(f["Units"].attrs["Unit time in cgs (U_t)"]),
+        )
+
+        # Particle data (converted to SI)
+        A2_pos = (
+            np.array(f["PartType0/Coordinates"][()])
+            - 0.5 * f["Header"].attrs["BoxSize"]
+        ) * file_to_SI.l
+        A1_m = np.array(f["PartType0/Masses"][()]) * file_to_SI.m
+        A1_h = np.array(f["PartType0/SmoothingLengths"][()]) * file_to_SI.l
+        A1_rho = np.array(f["PartType0/Densities"][()]) * file_to_SI.rho
+        A1_P = np.array(f["PartType0/Pressures"][()]) * file_to_SI.P
+        A1_u = np.array(f["PartType0/InternalEnergies"][()]) * file_to_SI.u
+        A1_mat_id = np.array(f["PartType0/MaterialIDs"][()])
+
+        return A2_pos, A1_m, A1_h, A1_rho, A1_P, A1_u, A1_mat_id
+
+
+snapshot_id = 5
+A2_pos_t, A1_m_t, A1_h_t, A1_rho_t, A1_P_t, A1_u_t, A1_mat_id_t = load_snapshot_data(
+    "snapshots/demo_target_%s_%04d.hdf5" % (N_label, snapshot_id)
+)
+A2_pos_i, A1_m_i, A1_h_i, A1_rho_i, A1_P_i, A1_u_i, A1_mat_id_i = load_snapshot_data(
+    "snapshots/demo_impactor_%s_%04d.hdf5" % (N_label, snapshot_id)
+)
+
+# Impact initial conditions (target rest frame)
+# Collide at 45 degrees, at the mutual escape speed, start 1 hour before contact
+A1_pos_t, A1_vel_t = np.zeros(3), np.zeros(3)
+A1_pos_i, A1_vel_i = woma.impact_pos_vel_b_v_c_t(
+    b=np.sin(45 * np.pi / 180),
+    v_c=1,
+    units_v_c="v_esc",
+    t=3600,
+    R_t=1.000 * R_E,
+    R_i=0.566 * R_E,
+    M_t=0.887 * M_E,
+    M_i=0.133 * M_E,
+)
+
+# Shift to centre-of-mass frame
+A1_pos_com = (M_t * A1_pos_t + M_i * A1_pos_i) / (M_t + M_i)
+A1_vel_com = (M_t * A1_vel_t + M_i * A1_vel_i) / (M_t + M_i)
+
+A1_pos_t -= A1_pos_com
+A1_vel_t -= A1_vel_com
+A1_pos_i -= A1_pos_com
+A1_vel_i -= A1_vel_com
+
+# Update particle positions
+A2_pos_t[:] += A1_pos_t
+A2_vel_t = np.zeros_like(A2_pos_t) + A1_vel_t
+A2_pos_i[:] += A1_pos_i
+A2_vel_i = np.zeros_like(A2_pos_i) + A1_vel_i
+
+# Combine and save the particle data (label by the resolution)
+with h5py.File("demo_impact_%s.hdf5" % N_label, "w") as f:
+    woma.save_particle_data(
+        f,
+        A2_pos=np.append(A2_pos_t, A2_pos_i, axis=0),
+        A2_vel=np.append(A2_vel_t, A2_vel_i, axis=0),
+        A1_m=np.append(A1_m_t, A1_m_i),
+        A1_h=np.append(A1_h_t, A1_h_i),
+        A1_rho=np.append(A1_rho_t, A1_rho_i),
+        A1_P=np.append(A1_P_t, A1_P_i),
+        A1_u=np.append(A1_u_t, A1_u_i),
+        A1_mat_id=np.append(A1_mat_id_t, A1_mat_id_i),
+        boxsize=100 * R_E,
+        file_to_SI=woma.Conversions(m=1e24, l=1e6, t=1),
+    )
diff --git a/examples/Planetary/DemoImpactInitCond/make_init_cond.py b/examples/Planetary/DemoImpactInitCond/make_init_cond.py
new file mode 100755
index 0000000000000000000000000000000000000000..3f76fbb641cfaaa06c4a53ad3a76355bfe87ccc2
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/make_init_cond.py
@@ -0,0 +1,89 @@
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+################################################################################
+
+"""Create initial conditions for settling, using WoMa. See README.md for more info."""
+
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 6
+N_label = "n%d" % (10 * np.log10(N))
+
+# Earth units
+M_E = 5.9724e24  # kg
+R_E = 6.3710e6  # m
+
+# Set profile inputs
+M_t = 0.887 * M_E
+M_i = 0.133 * M_E
+target_prof = woma.Planet(
+    name="target",
+    A1_mat_layer=["ANEOS_Fe85Si15", "ANEOS_forsterite"],
+    A1_T_rho_type=["adiabatic", "adiabatic"],
+    M=M_t,
+    A1_M_layer=[M_t * 0.3, M_t * 0.7],
+    P_s=1e5,
+    T_s=2000,
+)
+impactor_prof = woma.Planet(
+    name="impactor",
+    A1_mat_layer=["ANEOS_Fe85Si15", "ANEOS_forsterite"],
+    A1_T_rho_type=["adiabatic", "adiabatic"],
+    M=M_i,
+    A1_M_layer=[M_i * 0.3, M_i * 0.7],
+    P_s=1e5,
+    T_s=2000,
+)
+
+# Load material tables
+woma.load_eos_tables(
+    np.unique(np.append(target_prof.A1_mat_layer, impactor_prof.A1_mat_layer))
+)
+
+# Compute profiles
+target_prof.gen_prof_L2_find_R_R1_given_M1_M2(R_min=0.95 * R_E, R_max=1.05 * R_E)
+impactor_prof.gen_prof_L2_find_R_R1_given_M1_M2(R_min=0.5 * R_E, R_max=0.6 * R_E)
+
+# Save profile data
+target_prof.save("demo_target_profile.hdf5")
+impactor_prof.save("demo_impactor_profile.hdf5")
+
+# Place particles
+target = woma.ParticlePlanet(target_prof, 0.887 * N, seed=12345)
+impactor = woma.ParticlePlanet(impactor_prof, 0.133 * N, seed=23456)
+
+print()
+print("N_target     = %d" % target.N_particles)
+print("N_impactor   = %d" % impactor.N_particles)
+
+# Save the settling initial conditions
+file_to_SI = woma.Conversions(m=1e24, l=1e6, t=1)
+target.save(
+    "demo_target_%s.hdf5" % N_label,
+    boxsize=10 * R_E,
+    file_to_SI=file_to_SI,
+    do_entropies=True,
+)
+impactor.save(
+    "demo_impactor_%s.hdf5" % N_label,
+    boxsize=10 * R_E,
+    file_to_SI=file_to_SI,
+    do_entropies=True,
+)
diff --git a/examples/Planetary/DemoImpactInitCond/plot_profiles.py b/examples/Planetary/DemoImpactInitCond/plot_profiles.py
new file mode 100755
index 0000000000000000000000000000000000000000..e60b2e3201495017a86642bd498bef98c1efc6e8
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/plot_profiles.py
@@ -0,0 +1,106 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##############################################################################
+
+"""Plot the post-settling planetary profiles of the DemoImpactInitCond simulations.
+
+Note that, for standard SPH hydro schemes, especially at low resolution, the
+standard issues that arise at discontinuities in material and density lead the
+SPH particles to settle at slightly different densities near discontinuties.
+For more info and to explore ways to resolve these issues, check out e.g.
+Sandnes et al. (2025), Ruiz-Bonilla el al. (2022), and Kegerreis et al. (2019).
+The overall profile of the settled SPH planet should still align with the input.
+"""
+
+import os
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 5
+N_label = "n%d" % (10 * np.log10(N))
+
+# Plotting options
+font_size = 20
+params = {
+    "axes.labelsize": font_size,
+    "font.size": font_size,
+    "xtick.labelsize": font_size,
+    "ytick.labelsize": font_size,
+    "font.family": "serif",
+}
+matplotlib.rcParams.update(params)
+
+
+def plot_profile_and_particles(profile, A1_r, A1_rho):
+    """Plot the particles."""
+    plt.figure(figsize=(7, 7))
+    ax = plt.gca()
+
+    # Earth units
+    R_E = 6.3710e6  # m
+
+    # Profile
+    ax.plot(profile.A1_r / R_E, profile.A1_rho)
+
+    # Particles
+    ax.scatter(A1_r / R_E, A1_rho, c="k", marker=".", s=1 ** 2)
+
+    ax.set_xlim(0, None)
+    ax.set_xlabel(r"Radial distance ($R_\oplus$)")
+    ax.set_ylabel(r"Density (kg m$^{-3}$)")
+
+    plt.tight_layout()
+
+
+if __name__ == "__main__":
+    # Plot each snapshot
+    for body in ["target", "impactor"]:
+        # Load profiles
+        profile = woma.Planet(load_file="demo_%s_profile.hdf5" % body)
+
+        # Load the data
+        snapshot_id = 5
+        filename = "snapshots/demo_%s_%s_%04d.hdf5" % (body, N_label, snapshot_id)
+        with h5py.File(filename, "r") as f:
+            # Units from file metadata
+            file_to_SI = woma.Conversions(
+                m=float(f["Units"].attrs["Unit mass in cgs (U_M)"]) * 1e-3,
+                l=float(f["Units"].attrs["Unit length in cgs (U_L)"]) * 1e-2,
+                t=float(f["Units"].attrs["Unit time in cgs (U_t)"]),
+            )
+
+            # Particle data
+            A2_pos = (
+                np.array(f["PartType0/Coordinates"][()])
+                - 0.5 * f["Header"].attrs["BoxSize"]
+            ) * file_to_SI.l
+            A1_r = np.sqrt(np.sum(A2_pos ** 2, axis=1))
+            A1_rho = np.array(f["PartType0/Densities"][()]) * file_to_SI.rho
+
+        # Plot the data
+        plot_profile_and_particles(profile, A1_r, A1_rho)
+
+        # Save the figure
+        save = "demo_%s_%s_%04d_prof.png" % (body, N_label, snapshot_id)
+        plt.savefig(save, dpi=200)
+        plt.close()
+
+        print("\rSaved %s" % save)
diff --git a/examples/Planetary/DemoImpactInitCond/plot_snapshots.py b/examples/Planetary/DemoImpactInitCond/plot_snapshots.py
new file mode 100755
index 0000000000000000000000000000000000000000..b436b973e953a13bf322d3817e470913307fab97
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/plot_snapshots.py
@@ -0,0 +1,126 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2023 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##############################################################################
+
+"""Plot the particle positions from the DemoImpactInitCond settling simulations."""
+
+import os
+import matplotlib
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 5
+N_label = "n%d" % (10 * np.log10(N))
+
+# Plotting options
+font_size = 20
+params = {
+    "axes.labelsize": font_size,
+    "font.size": font_size,
+    "xtick.labelsize": font_size,
+    "ytick.labelsize": font_size,
+    "font.family": "serif",
+}
+matplotlib.rcParams.update(params)
+
+# Material colours
+Di_mat_colour = {"ANEOS_Fe85Si15": "orangered", "ANEOS_forsterite": "gold"}
+Di_id_colour = {woma.Di_mat_id[mat]: colour for mat, colour in Di_mat_colour.items()}
+
+# Scale point size with resolution
+size = (1 * np.cbrt(10 ** 6 / N)) ** 2
+
+
+def load_snapshot(filename):
+    """Load and convert the particle data to plot."""
+    with h5py.File(filename, "r") as f:
+        # Units from file metadata
+        file_to_SI = woma.Conversions(
+            m=float(f["Units"].attrs["Unit mass in cgs (U_M)"]) * 1e-3,
+            l=float(f["Units"].attrs["Unit length in cgs (U_L)"]) * 1e-2,
+            t=float(f["Units"].attrs["Unit time in cgs (U_t)"]),
+        )
+
+        # Particle data
+        A2_pos = (
+            np.array(f["PartType0/Coordinates"][()])
+            - 0.5 * f["Header"].attrs["BoxSize"]
+        ) * file_to_SI.l
+        A1_u = np.array(f["PartType0/InternalEnergies"][()]) * file_to_SI.u
+
+    # Restrict to z < 0 for plotting
+    A1_sel = np.where(A2_pos[:, 2] < 0)[0]
+    A2_pos = A2_pos[A1_sel]
+    A1_u = A1_u[A1_sel]
+
+    return A2_pos, A1_u
+
+
+def plot_snapshot(A2_pos, A1_u):
+    """Plot the particles, coloured by their internal energy."""
+    plt.figure(figsize=(7, 7))
+    ax = plt.gca()
+    ax.set_aspect("equal")
+    cax = make_axes_locatable(ax).append_axes("right", size="5%", pad=0.05)
+
+    # Earth units
+    R_E = 6.3710e6  # m
+
+    # Plot
+    scat = ax.scatter(
+        A2_pos[:, 0] / R_E,
+        A2_pos[:, 1] / R_E,
+        c=A1_u,
+        edgecolors="none",
+        marker=".",
+        s=size,
+    )
+    cbar = plt.colorbar(scat, cax=cax)
+    cbar.set_label(r"Sp. Int. Energy (J kg$^{-1}$)")
+
+    ax_lim = 1.5
+    ax.set_xlim(-ax_lim, ax_lim)
+    ax.set_yticks(ax.get_xticks())
+    ax.set_ylim(-ax_lim, ax_lim)
+    ax.set_xlabel(r"$x$ ($R_\oplus$)")
+    ax.set_ylabel(r"$y$ ($R_\oplus$)")
+
+    plt.tight_layout()
+
+
+if __name__ == "__main__":
+    # Plot each snapshot
+    for body in ["target", "impactor"]:
+        # Load the data
+        snapshot_id = 5
+        A2_pos, A1_u = load_snapshot(
+            "snapshots/demo_%s_%s_%04d.hdf5" % (body, N_label, snapshot_id)
+        )
+
+        # Plot the data
+        plot_snapshot(A2_pos, A1_u)
+
+        # Save the figure
+        save = "demo_%s_%s_%04d.png" % (body, N_label, snapshot_id)
+        plt.savefig(save, dpi=200)
+        plt.close()
+
+        print("\rSaved %s" % save)
diff --git a/examples/Planetary/DemoImpactInitCond/run.sh b/examples/Planetary/DemoImpactInitCond/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..7dd29c38343249588ce88daab53438aa65919aa8
--- /dev/null
+++ b/examples/Planetary/DemoImpactInitCond/run.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+set -o xtrace
+
+# Resolution
+N_label=n50
+
+# Create the initial particle planets
+python3 make_init_cond.py
+
+# Download the equation of state tables if not already present
+if [ ! -e ../EoSTables/ANEOS_forsterite_S19.txt ]
+then
+    cd ../EoSTables
+    ./get_eos_tables.sh
+    cd -
+fi
+
+# Run SWIFT settling simulations
+../../../swift --hydro --self-gravity --threads=28 demo_target_"$N_label".yml \
+    2>&1 | tee output_"$N_label"_t.txt
+../../../swift --hydro --self-gravity --threads=28 demo_impactor_"$N_label".yml \
+    2>&1 | tee output_"$N_label"_i.txt
+
+# Plot the settled particles
+python3 plot_snapshots.py
+python3 plot_profiles.py
+
+# Create the impact initial conditions
+python3 make_impact_init_cond.py
diff --git a/examples/Planetary/EarthImpact/README.md b/examples/Planetary/EarthImpact/README.md
index b49289f105332fcc621b2b2e95058e4e332d2bb8..8d70281f46f48e636d5debcfd616435372aee1bd 100644
--- a/examples/Planetary/EarthImpact/README.md
+++ b/examples/Planetary/EarthImpact/README.md
@@ -1,12 +1,2 @@
-An example planetary simulation of the first 10 hours of a grazing giant impact 
-onto the proto-Earth (a roughly canonical Moon-forming impact) with the simple 
-Tillotson equation of state and 10^5 SPH particles.
-
-See Kegerreis et al. (2019), Mon. Not. R. Astron. Soc., 487:4 for more about 
-using SWIFT for planetary simulations.
-
-More examples and planetary-specific documentation coming soon!
-
-This example requires the code to be configured to use the planetary
-hydrodynamics scheme and equations of state:
- --with-hydro=planetary --with-equation-of-state=planetary
+This deprecated example has been superseded by the DemoImpact and 
+DemoImpactInitCond examples.
diff --git a/examples/Planetary/EarthImpact/configuration.yml b/examples/Planetary/EarthImpact/configuration.yml
deleted file mode 100644
index ccce852862bec6d1eeba2c132457678564979b8a..0000000000000000000000000000000000000000
--- a/examples/Planetary/EarthImpact/configuration.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-with-hydro:             planetary 
-with-equation-of-state: planetary
diff --git a/examples/Planetary/EarthImpact/get_init_cond.sh b/examples/Planetary/EarthImpact/get_init_cond.sh
deleted file mode 100755
index c8428c528afd61f7d25f0f0ccf4305eced63e429..0000000000000000000000000000000000000000
--- a/examples/Planetary/EarthImpact/get_init_cond.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/bash
-wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/Planetary/earth_impact.hdf5
diff --git a/examples/Planetary/EarthImpact/make_anim.sh b/examples/Planetary/EarthImpact/make_anim.sh
deleted file mode 100755
index 3ee234ab17ce384891587fda12c5a85f1b2c6466..0000000000000000000000000000000000000000
--- a/examples/Planetary/EarthImpact/make_anim.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-# Make a simple animation of the snapshots
-out="earth_impact.mp4"
-ffmpeg -framerate 5 -i earth_impact_%?%?%?%?.png $out -y
-
-echo Saved $out
diff --git a/examples/Planetary/EarthImpact/make_movie_logger.py b/examples/Planetary/EarthImpact/make_movie_logger.py
deleted file mode 100644
index 22891b6e925bdd6dfb83b1ab7004401e544f1956..0000000000000000000000000000000000000000
--- a/examples/Planetary/EarthImpact/make_movie_logger.py
+++ /dev/null
@@ -1,270 +0,0 @@
-#!/usr/bin/env python3
-
-import numpy as np
-import matplotlib.pyplot as plt
-import sys
-import swiftsimio.visualisation.projection as vis
-from copy import deepcopy
-from shutil import copyfile
-import os
-from subprocess import call
-
-sys.path.append("../../../csds/.libs/")
-import libcsds as csds
-
-boxsize = 80.0
-large_width = 15
-small_width = 0.4
-alpha_particle = 0
-
-width = 0
-basename = "index"
-resolution = 1080
-resolution_mini = resolution * 100 // 512
-id_foc = 99839
-v_max = 8.0
-
-traj = None
-
-
-def selectParticles(pos, width):
-    ind1 = np.logical_and(pos[:, 0] > 0, pos[:, 0] < width)
-    ind2 = np.logical_and(pos[:, 1] > 0, pos[:, 1] < width)
-    ind3 = np.logical_and(ind1, ind2)
-
-    # avoid the zoom
-    size = resolution_mini * width / resolution
-    box = np.logical_and(pos[:, 0] > (width - size), pos[:, 1] < size)
-    ind3 = np.logical_and(~box, ind3)
-
-    return ind3
-
-
-def getImage(parts, width, center, res):
-    pos = parts["positions"]
-    pos = pos - center + 0.5 * width
-    pos /= width
-    h = parts["smoothing_lengths"] / width
-
-    m = np.ones(pos.shape[0])
-    # Do the projection
-    img = vis.scatter(pos[:, 0], pos[:, 1], m, h, res).T
-    img /= width ** 3
-    ind = img == 0
-    img[ind] = img[~ind].min()
-    img = np.log10(img)
-
-    return img
-
-
-def doProjection(parts, t, skip):
-    plt.figure(figsize=(8, 8))
-    global traj, id_foc
-
-    # evolve in time the particles
-    interp = csds.moveForwardInTime(basename, parts, t)
-
-    # Check if some particles where removed
-    ind = parts["smoothing_lengths"] == 0
-    ind = np.arange(parts.shape[0])[ind]
-    if len(ind) != 0:
-        parts = np.delete(parts, ind)
-        interp = np.delete(interp, ind)
-        id_foc -= np.sum(ind < id_foc)
-
-    # Get the position and the image center
-    pos = interp["positions"]
-    position = pos[id_foc, :]
-
-    # save the trajectory
-    if traj is None:
-        traj = position[np.newaxis, :]
-    else:
-        traj = np.append(traj, position[np.newaxis, :], axis=0)
-
-    # Do we want to generate the image?
-    if skip:
-        return parts
-
-    # Compute the images
-    img = getImage(interp, width, position, resolution)
-    img[:resolution_mini, -resolution_mini:] = getImage(
-        interp, 0.2 * boxsize, position, resolution_mini
-    )
-    box = width * np.array([0, 1, 0, 1])
-    plt.imshow(img, origin="lower", extent=box, cmap="plasma", vmin=0, vmax=v_max)
-
-    # plot the particles
-    pos = interp["positions"] - position + 0.5 * width
-    ind = selectParticles(pos, width)
-    ms = 2
-    plt.plot(
-        pos[ind, 0],
-        pos[ind, 1],
-        "o",
-        markersize=ms,
-        alpha=alpha_particle,
-        color="silver",
-    )
-    plt.plot(
-        pos[id_foc, 0], pos[id_foc, 1], "or", markersize=2 * ms, alpha=alpha_particle
-    )
-
-    # plot time
-    plt.text(
-        0.5 * width,
-        0.95 * width,
-        "Time = %0.2f Hours" % (t / (60 * 60)),
-        color="w",
-        horizontalalignment="center",
-    )
-
-    # plot trajectory
-    tr = deepcopy(traj) - position + 0.5 * width
-    plt.plot(tr[:, 0], tr[:, 1], "-", color="w", alpha=alpha_particle)
-
-    # plot scale
-    plt.plot([0.05 * width, 0.15 * width], [0.05 * width, 0.05 * width], "-w")
-    plt.text(
-        0.1 * width,
-        0.06 * width,
-        "%.2f R$_\oplus$" % (0.1 * width),
-        horizontalalignment="center",
-        color="w",
-    )
-
-    # style
-    plt.axis("off")
-    plt.xlim(box[:2])
-    plt.ylim(box[2:])
-    plt.tight_layout()
-    plt.style.use("dark_background")
-    return parts
-
-
-def skipImage(i):
-    if os.path.isfile("output/image_%04i.png" % i):
-        print("Skipping image %i" % i)
-        return True
-    else:
-        return False
-
-
-def doMovie(parts, t0, t1, N, init):
-    times = np.linspace(t0, t1, N)
-    for i, t in enumerate(times):
-        print("Image %i / %i" % (i + 1, N))
-        skip = skipImage(i + init)
-        parts = doProjection(parts, t, skip)
-        if not skip:
-            plt.savefig("output/image_%04i.png" % (i + init))
-        plt.close()
-    return init + N
-
-
-def doZoom(parts, w_init, w_end, N, init, t0, t1, increase_alpha):
-    global width, alpha_particle
-    widths = np.linspace(w_init, w_end, N)
-    alpha = np.linspace(-8, 0.2, N)
-    if not increase_alpha:
-        alpha = alpha[::-1]
-    k = 5  # parameter for the steepness
-    alpha = 1.0 / (1 + np.exp(-2 * k * alpha))
-    times = np.linspace(t0, t1, N)
-    for i, w in enumerate(widths):
-        print("Image %i / %i" % (i + 1, N))
-        skip = skipImage(i + init)
-        width = w
-        alpha_particle = alpha[i]
-        parts = doProjection(parts, times[i], skip)
-        if not skip:
-            plt.savefig("output/image_%04i.png" % (i + init))
-        plt.close()
-    return init + N
-
-
-def doStatic(init, number, ref):
-    # copy the first picture
-    for i in range(number):
-        copyfile("output/image_%04i.png" % ref, "output/image_%04i.png" % (i + init))
-    return init + number
-
-
-def doTitle(frames):
-    plt.figure()
-    plt.axis("off")
-    box = np.array([0, 1, 0, 1])
-    plt.xlim(box[:2])
-    plt.ylim(box[2:])
-    plt.tight_layout()
-    plt.style.use("dark_background")
-
-    style = {
-        "verticalalignment": "center",
-        "horizontalalignment": "center",
-        "fontweight": "bold",
-    }
-    plt.text(0.5, 0.6, "Planetary Impact with the CSDS", **style)
-    plt.text(0.5, 0.5, "L. Hausammann, J. Kegerreis and P. Gonnet 2020", **style)
-
-    for i in range(frames):
-        plt.savefig("output/image_%04i.png" % i)
-
-    plt.close()
-    return frames
-
-
-def main():
-    t0, t1 = csds.getTimeLimits(basename)
-    parts = csds.loadSnapshotAtTime(basename, t0)
-
-    # Do a few title frames
-    init = doTitle(40)
-
-    after_title = init
-    frames_after_title = 10
-    init += frames_after_title
-
-    # do a zoom while moving forward in time
-    init = doZoom(
-        parts,
-        large_width,
-        small_width,
-        50,
-        init=init,
-        t0=t0,
-        t1=0.07 * t1,
-        increase_alpha=True,
-    )
-
-    # Copy a few frames
-    doStatic(after_title, frames_after_title, after_title + frames_after_title)
-    init = doStatic(init, 10, init - 1)
-    # Do the main part of the movie
-    init = doMovie(parts, 0.07 * t1, 0.15 * t1, N=250, init=init)
-    # copy a few frames
-    init = doStatic(init, 10, init - 1)
-
-    # zoom out and finish the movie
-    init = doZoom(
-        parts,
-        small_width,
-        large_width,
-        607,
-        init=init,
-        t0=0.15 * t1,
-        t1=t1,
-        increase_alpha=False,
-    )
-
-    # copy a few frames
-    init = doStatic(init, 10, init - 1)
-
-
-if __name__ == "__main__":
-    main()
-
-    convert = "ffmpeg -i output/image_%04d.png -y -vcodec libx264 "
-    convert += "-profile:v high444 -refs 16 -crf 0 "
-    convert += "-preset ultrafast movie.mp4"
-    call(convert, shell=True)
diff --git a/examples/Planetary/EarthImpact/plot_solution.py b/examples/Planetary/EarthImpact/plot_solution.py
deleted file mode 100644
index c6acf483b15ceb3bece3ca482a6374b716a4f8c3..0000000000000000000000000000000000000000
--- a/examples/Planetary/EarthImpact/plot_solution.py
+++ /dev/null
@@ -1,141 +0,0 @@
-###############################################################################
-# This file is part of SWIFT.
-# Copyright (c) 2019 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##############################################################################
-
-# Plot the snapshots from the example giant impact on the proto-Earth, showing
-# the particles in a thin slice near z=0, coloured by their material.
-
-import matplotlib
-import matplotlib.pyplot as plt
-import numpy as np
-import swiftsimio as sw
-import unyt
-
-font_size = 20
-params = {
-    "axes.labelsize": font_size,
-    "font.size": font_size,
-    "xtick.labelsize": font_size,
-    "ytick.labelsize": font_size,
-    "font.family": "serif",
-}
-matplotlib.rcParams.update(params)
-
-# Material IDs ( = type_id * type_factor + unit_id )
-type_factor = 100
-type_Til = 1
-id_body = 10000
-# Name and ID
-Di_mat_id = {
-    "Til_iron": type_Til * type_factor,
-    "Til_iron_2": type_Til * type_factor + id_body,
-    "Til_granite": type_Til * type_factor + 1,
-    "Til_granite_2": type_Til * type_factor + 1 + id_body,
-}
-# Colour
-Di_mat_colour = {
-    "Til_iron": "darkgray",
-    "Til_granite": "orangered",
-    "Til_iron_2": "saddlebrown",
-    "Til_granite_2": "gold",
-}
-Di_id_colour = {Di_mat_id[mat]: colour for mat, colour in Di_mat_colour.items()}
-
-
-def load_snapshot(snapshot_id, ax_lim):
-    """ Select and load the particles to plot. """
-    # Snapshot to load
-    snapshot = "earth_impact_%04d.hdf5" % snapshot_id
-
-    # 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 plot_snapshot(pos, mat_id, ax_lim):
-    """ Plot the particles, coloured by their material. """
-    plt.figure(figsize=(7, 7))
-    ax = plt.gca()
-    ax.set_aspect("equal")
-
-    colour = np.empty(len(pos), dtype=object)
-    for id_c, c in Di_id_colour.items():
-        colour[mat_id == id_c] = c
-
-    ax.scatter(
-        pos[:, 0], pos[:, 1], c=colour, edgecolors="none", marker=".", s=10, alpha=0.5
-    )
-
-    ax.set_xlim(-ax_lim, ax_lim)
-    ax.set_yticks(ax.get_xticks())
-    ax.set_ylim(-ax_lim, ax_lim)
-    ax.set_xlabel(r"x Position ($R_\oplus$)")
-    ax.set_ylabel(r"y Position ($R_\oplus$)")
-
-    plt.tight_layout()
-
-
-if __name__ == "__main__":
-    print()
-    # Axis limits (Earth radii)
-    ax_lim = 3.4
-
-    # Plot each snapshot
-    for snapshot_id in range(37):
-        # Load the data
-        pos, mat_id = load_snapshot(snapshot_id, ax_lim)
-
-        # Plot the data
-        plot_snapshot(pos, mat_id, ax_lim)
-
-        # Save the figure
-        save = "earth_impact_%04d.png" % snapshot_id
-        plt.savefig(save, dpi=100)
-
-        print("\rSaved %s" % save)
-
-    print()
diff --git a/examples/Planetary/EarthImpact/run.sh b/examples/Planetary/EarthImpact/run.sh
deleted file mode 100755
index b73dea6e0c938ace0de0fe4fa82d5a6c1102fc2a..0000000000000000000000000000000000000000
--- a/examples/Planetary/EarthImpact/run.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-
-# Get the initial conditions if they are not present.
-if [ ! -e earth_impact.hdf5 ]
-then
-    echo "Fetching initial conditions file for the Earth impact example..."
-    ./get_init_cond.sh
-fi
-
-# Run SWIFT
-../../../swift -s -G -t 8 earth_impact.yml 2>&1 | tee output.log
-
-# Plot the snapshots
-python3 plot_solution.py
-
-# Make a simple animation
-./make_anim.sh
diff --git a/examples/Planetary/EarthImpact/system.yml b/examples/Planetary/EarthImpact/system.yml
deleted file mode 100644
index 8e942aa1fcc99da28f15d212fc1792cfcd99c417..0000000000000000000000000000000000000000
--- a/examples/Planetary/EarthImpact/system.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-input:
-  - earth_impact.yml
-
-output:
-  - earth_impact.pdf
-
-swift_parameters:   -s -G
-swift_threads:      8
diff --git a/examples/Planetary/EoSTables/get_eos_tables.sh b/examples/Planetary/EoSTables/get_eos_tables.sh
index aaae425fd5dd9e22490291f966d1f9c76e76f30e..e47f08816dac41099cde2b980772c7320c9567bb 100755
--- a/examples/Planetary/EoSTables/get_eos_tables.sh
+++ b/examples/Planetary/EoSTables/get_eos_tables.sh
@@ -7,6 +7,10 @@ wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/SESAME_basalt_7530.txt
 wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/SESAME_iron_2140.txt
 wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/SESAME_water_7154.txt
 wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/SS08_water.txt
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/AQUA_H20.txt
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/CMS19_H.txt
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/CMS19_He.txt
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/CD21_HHe.txt
 
 wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/ANEOS_forsterite_S19.txt
 wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/EoS/ANEOS_iron_S20.txt
diff --git a/examples/Planetary/EvrardCollapse_3D/README.md b/examples/Planetary/EvrardCollapse_3D/README.md
index f47661a79dabc98d85e7bc0d049a19e6f249fcca..f1c8546f42bdf4c44fa02dbbf6914705cb5cdabb 100644
--- a/examples/Planetary/EvrardCollapse_3D/README.md
+++ b/examples/Planetary/EvrardCollapse_3D/README.md
@@ -2,13 +2,20 @@ Evrard Collapse 3D (Planetary)
 ===============
 
 This is a copy of `/examples/HydroTests/EvrardCollapse_3D` for testing the 
-Planetary hydro scheme with the planetary ideal gas equation of state. 
+Planetary and REMIX hydro schemes with the planetary ideal gas equation of state.
 
-The results should be highly similar to the Minimal hydro scheme, though 
-slightly different because of the higher viscosity beta used here. To recover 
-the Minimal scheme behaviour, edit `const_viscosity_beta` from 4 to 3 in 
-`src/hydro/Planetary/hydro_parameters.h`.
+The Planetary hydro scheme results should be highly similar to the Minimal
+hydro scheme, though  slightly different because of the higher viscosity beta
+used here. To recover  the Minimal scheme behaviour, edit `const_viscosity_beta`
+from 4 to 3 in `src/hydro/Planetary/hydro_parameters.h`.
 
-This requires the code to be configured to use the planetary hydrodynamics 
-scheme and equations of state: 
-`--with-hydro=planetary --with-equation-of-state=planetary`
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/EvrardCollapse_3D/evrard.yml b/examples/Planetary/EvrardCollapse_3D/evrard.yml
index 018e767fc45bfad3c5aec1b81e8f9ec1bca46db1..dea62913b3b3958a147a835ff536ab53d144b9cb 100644
--- a/examples/Planetary/EvrardCollapse_3D/evrard.yml
+++ b/examples/Planetary/EvrardCollapse_3D/evrard.yml
@@ -26,9 +26,8 @@ Statistics:
 
 # Parameters for the hydrodynamics scheme
 SPH:
-  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
   CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
-  viscosity_alpha:       0.8      # Override for the initial value of the artificial viscosity. In schemes that have a fixed AV, this remains as alpha throughout the run.
 
 # Parameters for the self-gravity scheme
 Gravity:
diff --git a/examples/Planetary/EvrardCollapse_3D/makeIC.py b/examples/Planetary/EvrardCollapse_3D/makeIC.py
index dd7d91ed95824daef0019cb215d8e1902388ff7e..85bf1d4969a625067f482790dfab28922334c47e 100644
--- a/examples/Planetary/EvrardCollapse_3D/makeIC.py
+++ b/examples/Planetary/EvrardCollapse_3D/makeIC.py
@@ -67,6 +67,7 @@ h = ones(numPart) * 2.0 * R / numPart ** (1.0 / 3.0)
 v = zeros((numPart, 3))
 ids = linspace(1, numPart, numPart)
 m = ones(numPart) * M / numPart
+rho = M / (2 * pi * R ** 2 * r)
 u = ones(numPart) * u0
 mat = zeros(numPart)
 
@@ -100,8 +101,9 @@ grp = file.create_group("/PartType0")
 grp.create_dataset("Coordinates", data=pos, dtype="d")
 grp.create_dataset("Velocities", data=v, dtype="f")
 grp.create_dataset("Masses", data=m, dtype="f")
-grp.create_dataset("SmoothingLengths", data=h, dtype="f")
-grp.create_dataset("InternalEnergies", data=u, dtype="f")
+grp.create_dataset("Density", data=rho, dtype="f")
+grp.create_dataset("SmoothingLength", data=h, dtype="f")
+grp.create_dataset("InternalEnergy", data=u, dtype="f")
 grp.create_dataset("ParticleIDs", data=ids, dtype="L")
 grp.create_dataset("MaterialIDs", data=mat, dtype="i")
 
diff --git a/examples/Planetary/GreshoVortex_3D/README.md b/examples/Planetary/GreshoVortex_3D/README.md
deleted file mode 100644
index e2278c894e05849e518d56bea0bc94963758c0b7..0000000000000000000000000000000000000000
--- a/examples/Planetary/GreshoVortex_3D/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-Gresho Vortex 3D (Planetary)
-=============
-
-This is a copy of `/examples/HydroTests/GreshoVortex_3D` for testing the 
-Planetary hydro scheme with the planetary ideal gas equation of state. 
-
-The results should be highly similar to the Minimal hydro scheme, though 
-slightly different because of the higher viscosity beta used here. To recover 
-the Minimal scheme behaviour, edit `const_viscosity_beta` from 4 to 3 in 
-`src/hydro/Planetary/hydro_parameters.h`.
-
-This requires the code to be configured to use the planetary hydrodynamics 
-scheme and equations of state: 
-`--with-hydro=planetary --with-equation-of-state=planetary`
diff --git a/examples/Planetary/GreshoVortex_3D/makeIC.py b/examples/Planetary/GreshoVortex_3D/makeIC.py
deleted file mode 100644
index 0259bc9303a920799612cae0347adaab4d7c0de8..0000000000000000000000000000000000000000
--- a/examples/Planetary/GreshoVortex_3D/makeIC.py
+++ /dev/null
@@ -1,118 +0,0 @@
-################################################################################
-# This file is part of SWIFT.
-# Copyright (c) 2016 Matthieu Schaller (schaller@strw.leidenuniv.nl)
-#               2017 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-################################################################################
-
-import h5py
-from numpy import *
-
-# Generates a swift IC file for the Gresho-Chan vortex in a periodic box
-
-# Parameters
-gamma = 5.0 / 3.0  # Gas adiabatic index
-rho0 = 1  # Gas density
-P0 = 0.0  # Constant additional pressure (should have no impact on the dynamics)
-fileOutputName = "greshoVortex.hdf5"
-fileGlass = "glassCube_64.hdf5"
-# ---------------------------------------------------
-
-# Get position and smoothing lengths from the glass
-fileInput = h5py.File(fileGlass, "r")
-coords = fileInput["/PartType0/Coordinates"][:, :]
-h = fileInput["/PartType0/SmoothingLength"][:]
-ids = fileInput["/PartType0/ParticleIDs"][:]
-boxSize = fileInput["/Header"].attrs["BoxSize"][0]
-numPart = size(h)
-fileInput.close()
-
-# Now generate the rest
-m = ones(numPart) * rho0 * boxSize ** 3 / numPart
-u = zeros(numPart)
-v = zeros((numPart, 3))
-mat = zeros(numPart)
-
-for i in range(numPart):
-
-    x = coords[i, 0]
-    y = coords[i, 1]
-
-    r2 = (x - boxSize / 2) ** 2 + (y - boxSize / 2) ** 2
-    r = sqrt(r2)
-
-    v_phi = 0.0
-    if r < 0.2:
-        v_phi = 5.0 * r
-    elif r < 0.4:
-        v_phi = 2.0 - 5.0 * r
-    else:
-        v_phi = 0.0
-    v[i, 0] = -v_phi * (y - boxSize / 2) / r
-    v[i, 1] = v_phi * (x - boxSize / 2) / r
-    v[i, 2] = 0.0
-
-    P = P0
-    if r < 0.2:
-        P = P + 5.0 + 12.5 * r2
-    elif r < 0.4:
-        P = P + 9.0 + 12.5 * r2 - 20.0 * r + 4.0 * log(r / 0.2)
-    else:
-        P = P + 3.0 + 4.0 * log(2.0)
-    u[i] = P / ((gamma - 1.0) * rho0)
-
-
-# File
-fileOutput = h5py.File(fileOutputName, "w")
-
-# Header
-grp = fileOutput.create_group("/Header")
-grp.attrs["BoxSize"] = [boxSize, boxSize, boxSize]
-grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["Time"] = 0.0
-grp.attrs["NumFileOutputsPerSnapshot"] = 1
-grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
-grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["Dimension"] = 3
-
-# Units
-grp = fileOutput.create_group("/Units")
-grp.attrs["Unit length in cgs (U_L)"] = 1.0
-grp.attrs["Unit mass in cgs (U_M)"] = 1.0
-grp.attrs["Unit time in cgs (U_t)"] = 1.0
-grp.attrs["Unit current in cgs (U_I)"] = 1.0
-grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
-
-# Particle group
-grp = fileOutput.create_group("/PartType0")
-ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
-ds[()] = coords
-ds = grp.create_dataset("Velocities", (numPart, 3), "f")
-ds[()] = v
-ds = grp.create_dataset("Masses", (numPart, 1), "f")
-ds[()] = m.reshape((numPart, 1))
-ds = grp.create_dataset("SmoothingLengths", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergies", (numPart, 1), "f")
-ds[()] = u.reshape((numPart, 1))
-ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
-ds[()] = ids.reshape((numPart, 1))
-ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
-ds[()] = mat.reshape((numPart, 1))
-
-fileOutput.close()
diff --git a/examples/Planetary/GreshoVortex_3D/run.sh b/examples/Planetary/GreshoVortex_3D/run.sh
deleted file mode 100755
index 06ca28c934ddbd64fe251ebced0e4f5d1f231c54..0000000000000000000000000000000000000000
--- a/examples/Planetary/GreshoVortex_3D/run.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-
- # Generate the initial conditions if they are not present.
-if [ ! -e glassCube_64.hdf5 ]
-then
-    echo "Fetching initial glass file for the Gresho-Chan vortex example..."
-    ../../HydroTests/GreshoVortex_3D/getGlass.sh
-fi
-if [ ! -e greshoVortex.hdf5 ]
-then
-    echo "Generating initial conditions for the Gresho-Chan vortex example..."
-    python3 makeIC.py
-fi
-
-# Run SWIFT
-../../../swift --hydro --threads=4 gresho.yml 2>&1 | tee output.log
-
-# Plot the solution
-python3 ../../HydroTests/GreshoVortex_3D/plotSolution.py 11
diff --git a/examples/Planetary/JupiterLikePlanet/demo_target_n70.yml b/examples/Planetary/JupiterLikePlanet/demo_target_n70.yml
new file mode 100755
index 0000000000000000000000000000000000000000..f64f79bd266b5c50bd93931a43172ff61e6c2267
--- /dev/null
+++ b/examples/Planetary/JupiterLikePlanet/demo_target_n70.yml
@@ -0,0 +1,68 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+    UnitMass_in_cgs:        1e27        # Sets Earth mass = 5.972
+    UnitLength_in_cgs:      1e8         # Sets Earth radius = 6.371
+    UnitVelocity_in_cgs:    1e8         # Sets time in seconds
+    UnitCurrent_in_cgs:     1           # Amperes
+    UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+    file_name:  demo_target_n70.hdf5    # The initial conditions file to read
+    periodic:   0                       # Are we running with periodic ICs?
+
+# Parameters governing the time integration
+TimeIntegration:
+    time_begin:     0                   # The starting time of the simulation (in internal units).
+    time_end:       20000               # The end time of the simulation (in internal units).
+    dt_min:         0.000001            # The minimal time-step size of the simulation (in internal units).
+    dt_max:         1000                # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+    subdir:             snapshots       # Sub-directory in which to write the snapshots. Defaults to "" (i.e. the directory where SWIFT is run).
+    basename:           demo_target_n70 # Common part of the name of output files
+    time_first:         0               # Time of the first output (in internal units)
+    delta_time:         2000            # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+    time_first: 0                       # Time of the first output (in internal units)
+    delta_time: 1000                    # Time between statistics output
+
+# Parameters controlling restarts
+Restarts:
+    enable:             1               # Whether to enable dumping restarts at fixed intervals.
+    save:               1               # Whether to save copies of the previous set of restart files (named .prev)
+    subdir:             restart         # Name of subdirectory for restart files.
+    basename:           demo_target_n70 # Prefix used in naming restart files.
+    delta_hours:        10.0            # Decimal hours between dumps of restart files.
+
+# Parameters for the hydrodynamics scheme
+SPH:
+    resolution_eta:     1.487           # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+    delta_neighbours:   0.1             # The tolerance for the targetted number of neighbours.
+    CFL_condition:      0.2             # Courant-Friedrich-Levy condition for time integration.
+    h_max:              10.             # Maximal allowed smoothing length (in internal units).
+    viscosity_alpha:    1.5             # Override for the initial value of the artificial viscosity.
+
+# Parameters for the self-gravity scheme
+Gravity:
+    eta:                            0.025       # Constant dimensionless multiplier for time integration.
+    MAC:                            adaptive    # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+    epsilon_fmm:                    0.001       # Tolerance parameter for the adaptive multipole acceptance criterion.
+    theta_cr:                       0.5         # Opening angle for the purely gemoetric criterion.
+    max_physical_baryon_softening:  0.04        # Physical softening length (in internal units).
+    
+# Parameters for the task scheduling
+Scheduler:
+    max_top_level_cells:    64          # Maximal number of top-level cells in any dimension.
+    
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_CD21_HHe:  1     # Hydrogen--helium (Chabrier and Debras 2021), material ID 307
+    planetary_use_AQUA:      1     # AQUA (Haldemann et al. 2020), material ID 304
+    # Tablulated EoS file paths.
+    planetary_CD21_HHe_table_file: ../EoSTables/CD21_HHe.txt
+    planetary_AQUA_table_file:     ../EoSTables/AQUA_H20.txt
diff --git a/examples/Planetary/JupiterLikePlanet/make_init_cond.py b/examples/Planetary/JupiterLikePlanet/make_init_cond.py
new file mode 100755
index 0000000000000000000000000000000000000000..8e1b5fcdfe9aae9d642f453df63eea6f0db12a8b
--- /dev/null
+++ b/examples/Planetary/JupiterLikePlanet/make_init_cond.py
@@ -0,0 +1,69 @@
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+# 		        2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+################################################################################
+
+"""Create initial conditions for settling, using WoMa. See README.md for more info."""
+
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 7
+N_label = "n%d" % (10 * np.log10(N))
+
+# Earth units
+M_E = 5.9724e24  # kg
+R_E = 6.3710e6  # m
+
+# Set profile inputs
+M_t = 308 * M_E
+target_prof = woma.Planet(
+    name="target",
+    A1_mat_layer=["AQUA", "CD21_HHe"],
+    A1_T_rho_type=["adiabatic", "adiabatic"],
+    M=M_t,
+    A1_M_layer=[10 * M_E, 298 * M_E],
+    P_s=1e5,
+    T_s=165,
+    num_prof=10000,
+)
+
+# Load material tables
+woma.load_eos_tables(np.unique(target_prof.A1_mat_layer))
+
+# Compute profiles
+target_prof.gen_prof_L2_find_R_R1_given_M1_M2(R_min=10.7 * R_E, R_max=11.2 * R_E)
+
+# Save profile data
+target_prof.save("demo_target_profile.hdf5")
+
+# Place particles
+target = woma.ParticlePlanet(target_prof, N, seed=12345)
+
+print()
+print("N_target     = %d" % target.N_particles)
+
+# Save the settling initial conditions
+file_to_SI = woma.Conversions(m=1e24, l=1e6, t=1)
+target.save(
+    "demo_target_%s.hdf5" % N_label,
+    boxsize=30 * R_E,
+    file_to_SI=file_to_SI,
+    do_entropies=True,
+)
diff --git a/examples/Planetary/JupiterLikePlanet/plot_profiles.py b/examples/Planetary/JupiterLikePlanet/plot_profiles.py
new file mode 100755
index 0000000000000000000000000000000000000000..11f48aa67564b8debc6f684015596923f14e421f
--- /dev/null
+++ b/examples/Planetary/JupiterLikePlanet/plot_profiles.py
@@ -0,0 +1,107 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+# 	            2023 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##############################################################################
+
+"""Plot the post-settling planetary profiles of the DemoImpactInitCond simulations.
+
+Note that, for standard SPH hydro schemes, especially at low resolution, the
+standard issues that arise at discontinuities in material and density lead the
+SPH particles to settle at slightly different densities near discontinuties.
+For more info and to explore ways to resolve these issues, check out e.g.
+Sandnes et al. (2025), Ruiz-Bonilla el al. (2022), and Kegerreis et al. (2019).
+The overall profile of the settled SPH planet should still align with the input.
+"""
+
+import os
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 7
+N_label = "n%d" % (10 * np.log10(N))
+
+# Plotting options
+font_size = 20
+params = {
+    "axes.labelsize": font_size,
+    "font.size": font_size,
+    "xtick.labelsize": font_size,
+    "ytick.labelsize": font_size,
+    "font.family": "serif",
+}
+matplotlib.rcParams.update(params)
+
+
+def plot_profile_and_particles(profile, A1_r, A1_rho):
+    """Plot the particles."""
+    plt.figure(figsize=(7, 7))
+    ax = plt.gca()
+
+    # Earth units
+    R_E = 6.3710e6  # m
+
+    # Profile
+    ax.plot(profile.A1_r / R_E, profile.A1_rho)
+
+    # Particles
+    ax.scatter(A1_r / R_E, A1_rho, c="k", marker=".", s=1 ** 2)
+
+    ax.set_xlim(0, None)
+    ax.set_xlabel(r"Radial distance ($R_\oplus$)")
+    ax.set_ylabel(r"Density (kg m$^{-3}$)")
+
+    plt.tight_layout()
+
+
+if __name__ == "__main__":
+    # Plot each snapshot
+    for body in ["target"]:
+        # Load profiles
+        profile = woma.Planet(load_file="demo_%s_profile.hdf5" % body)
+
+        # Load the data
+        snapshot_id = 5
+        filename = "snapshots/demo_%s_%s_%04d.hdf5" % (body, N_label, snapshot_id)
+        with h5py.File(filename, "r") as f:
+            # Units from file metadata
+            file_to_SI = woma.Conversions(
+                m=float(f["Units"].attrs["Unit mass in cgs (U_M)"]) * 1e-3,
+                l=float(f["Units"].attrs["Unit length in cgs (U_L)"]) * 1e-2,
+                t=float(f["Units"].attrs["Unit time in cgs (U_t)"]),
+            )
+
+            # Particle data
+            A2_pos = (
+                np.array(f["PartType0/Coordinates"][()])
+                - 0.5 * f["Header"].attrs["BoxSize"]
+            ) * file_to_SI.l
+            A1_r = np.sqrt(np.sum(A2_pos ** 2, axis=1))
+            A1_rho = np.array(f["PartType0/Densities"][()]) * file_to_SI.rho
+
+        # Plot the data
+        plot_profile_and_particles(profile, A1_r, A1_rho)
+
+        # Save the figure
+        save = "demo_%s_%s_%04d_prof.png" % (body, N_label, snapshot_id)
+        plt.savefig(save, dpi=200)
+        plt.close()
+
+        print("\rSaved %s" % save)
diff --git a/examples/Planetary/JupiterLikePlanet/plot_snapshots.py b/examples/Planetary/JupiterLikePlanet/plot_snapshots.py
new file mode 100755
index 0000000000000000000000000000000000000000..57329935a97a270a1fde8d4a15d2b8d4a42b1cd6
--- /dev/null
+++ b/examples/Planetary/JupiterLikePlanet/plot_snapshots.py
@@ -0,0 +1,127 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+# 	            2023 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##############################################################################
+
+"""Plot the particle positions from the DemoImpactInitCond settling simulations."""
+
+import os
+import matplotlib
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+import numpy as np
+import h5py
+import woma
+
+# Number of particles
+N = 10 ** 7
+N_label = "n%d" % (10 * np.log10(N))
+
+# Plotting options
+font_size = 20
+params = {
+    "axes.labelsize": font_size,
+    "font.size": font_size,
+    "xtick.labelsize": font_size,
+    "ytick.labelsize": font_size,
+    "font.family": "serif",
+}
+matplotlib.rcParams.update(params)
+
+# Material colours
+Di_mat_colour = {"AQUA": "orangered", "CD21_HHe": "gold"}
+Di_id_colour = {woma.Di_mat_id[mat]: colour for mat, colour in Di_mat_colour.items()}
+
+# Scale point size with resolution
+size = (1 * np.cbrt(10 ** 6 / N)) ** 2
+
+
+def load_snapshot(filename):
+    """Load and convert the particle data to plot."""
+    with h5py.File(filename, "r") as f:
+        # Units from file metadata
+        file_to_SI = woma.Conversions(
+            m=float(f["Units"].attrs["Unit mass in cgs (U_M)"]) * 1e-3,
+            l=float(f["Units"].attrs["Unit length in cgs (U_L)"]) * 1e-2,
+            t=float(f["Units"].attrs["Unit time in cgs (U_t)"]),
+        )
+
+        # Particle data
+        A2_pos = (
+            np.array(f["PartType0/Coordinates"][()])
+            - 0.5 * f["Header"].attrs["BoxSize"]
+        ) * file_to_SI.l
+        A1_u = np.array(f["PartType0/InternalEnergies"][()]) * file_to_SI.u
+
+    # Restrict to z < 0 for plotting
+    A1_sel = np.where(A2_pos[:, 2] < 0)[0]
+    A2_pos = A2_pos[A1_sel]
+    A1_u = A1_u[A1_sel]
+
+    return A2_pos, A1_u
+
+
+def plot_snapshot(A2_pos, A1_u):
+    """Plot the particles, coloured by their internal energy."""
+    plt.figure(figsize=(7, 7))
+    ax = plt.gca()
+    ax.set_aspect("equal")
+    cax = make_axes_locatable(ax).append_axes("right", size="5%", pad=0.05)
+
+    # Earth units
+    R_E = 6.3710e6  # m
+
+    # Plot
+    scat = ax.scatter(
+        A2_pos[:, 0] / R_E,
+        A2_pos[:, 1] / R_E,
+        c=A1_u,
+        edgecolors="none",
+        marker=".",
+        s=size,
+    )
+    cbar = plt.colorbar(scat, cax=cax)
+    cbar.set_label(r"Sp. Int. Energy (J kg$^{-1}$)")
+
+    ax_lim = 15
+    ax.set_xlim(-ax_lim, ax_lim)
+    ax.set_yticks(ax.get_xticks())
+    ax.set_ylim(-ax_lim, ax_lim)
+    ax.set_xlabel(r"$x$ ($R_\oplus$)")
+    ax.set_ylabel(r"$y$ ($R_\oplus$)")
+
+    plt.tight_layout()
+
+
+if __name__ == "__main__":
+    # Plot each snapshot
+    for body in ["target"]:
+        # Load the data
+        snapshot_id = 5
+        A2_pos, A1_u = load_snapshot(
+            "snapshots/demo_%s_%s_%04d.hdf5" % (body, N_label, snapshot_id)
+        )
+
+        # Plot the data
+        plot_snapshot(A2_pos, A1_u)
+
+        # Save the figure
+        save = "demo_%s_%s_%04d.png" % (body, N_label, snapshot_id)
+        plt.savefig(save, dpi=200)
+        plt.close()
+
+        print("\rSaved %s" % save)
diff --git a/examples/Planetary/JupiterLikePlanet/run.sh b/examples/Planetary/JupiterLikePlanet/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e6cd3484e65c5a35054d595527144f6d92d0ef8b
--- /dev/null
+++ b/examples/Planetary/JupiterLikePlanet/run.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+set -o xtrace
+
+# Resolution
+N_label=n70
+
+# Create the initial particle planets
+python3 make_init_cond.py
+
+# Download the equation of state tables if not already present
+if [ ! -e ../EoSTables/CD21_HHe.txt ]
+then
+    cd ../EoSTables
+    ./get_eos_tables.sh
+    cd -
+fi
+
+# Run SWIFT settling simulations
+../../../swift --hydro --self-gravity --threads=28 demo_target_"$N_label".yml \
+    2>&1 | tee output_"$N_label"_t.txt
+
+# Plot the settled particles
+python3 plot_snapshots.py
+python3 plot_profiles.py
diff --git a/examples/Planetary/KelvinHelmholtz_2D/README.md b/examples/Planetary/KelvinHelmholtz_2D/README.md
deleted file mode 100644
index ecc18a7228fea3819bf10b2d0d34c1e3c6c9c3e3..0000000000000000000000000000000000000000
--- a/examples/Planetary/KelvinHelmholtz_2D/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-Kelvin Helmholtz 2D (Planetary)
-===================
-
-This is a copy of `/examples/HydroTests/KelvinHelmholtz_2D` for testing the 
-Planetary hydro scheme with the planetary ideal gas equation of state. 
-
-The results should be highly similar to the Minimal hydro scheme, though 
-slightly different because of the higher viscosity beta used here. To recover 
-the Minimal scheme behaviour, edit `const_viscosity_beta` from 4 to 3 in 
-`src/hydro/Planetary/hydro_parameters.h`.
-
-This requires the code to be configured to use the planetary hydrodynamics 
-scheme and equations of state: 
-`--with-hydro=planetary --with-equation-of-state=planetary --with-hydro-dimension=2`
diff --git a/examples/Planetary/KelvinHelmholtz_2D/kelvinHelmholtz.yml b/examples/Planetary/KelvinHelmholtz_2D/kelvinHelmholtz.yml
deleted file mode 100644
index 30d45f30f17c62dd541ee7ddcebfa69e03c2f366..0000000000000000000000000000000000000000
--- a/examples/Planetary/KelvinHelmholtz_2D/kelvinHelmholtz.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Define the system of units to use internally. 
-InternalUnitSystem:
-  UnitMass_in_cgs:     1   # Grams
-  UnitLength_in_cgs:   1   # Centimeters
-  UnitVelocity_in_cgs: 1   # Centimeters per second
-  UnitCurrent_in_cgs:  1   # Amperes
-  UnitTemp_in_cgs:     1   # Kelvin
-  
-# Parameters governing the time integration
-TimeIntegration:
-  time_begin: 0.0    # The starting time of the simulation (in internal units).
-  time_end:   4.5   # The end time of the simulation (in internal units).
-  dt_min:     1e-6  # The minimal time-step size of the simulation (in internal units).
-  dt_max:     1e-2  # The maximal time-step size of the simulation (in internal units).
-
-# Parameters governing the snapshots
-Snapshots:
-  basename:            kelvinHelmholtz  # Common part of the name of output files
-  time_first:          0.               # Time of the first output (in internal units)
-  delta_time:          0.01      # Time difference between consecutive outputs (in internal units)
-
-# Parameters governing the conserved quantities statistics
-Statistics:
-  delta_time:          1e-2 # Time between statistics output
-
-# Parameters for the hydrodynamics scheme
-SPH:
-  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
-  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
-  viscosity_alpha:       0.8      # Override for the initial value of the artificial viscosity. In schemes that have a fixed AV, this remains as alpha throughout the run.
-  
-# Parameters related to the initial conditions
-InitialConditions:
-  file_name:  ./kelvinHelmholtz.hdf5     # The file to read
-  periodic:   1
-
-# Parameters related to the equation of state
-EoS:
-    planetary_use_idg_def:    1               # Default ideal gas, material ID 0
diff --git a/examples/Planetary/KelvinHelmholtz_2D/makeIC.py b/examples/Planetary/KelvinHelmholtz_2D/makeIC.py
deleted file mode 100644
index 57f82ea9c4b750e804b1693bb18e56957fe8aac4..0000000000000000000000000000000000000000
--- a/examples/Planetary/KelvinHelmholtz_2D/makeIC.py
+++ /dev/null
@@ -1,158 +0,0 @@
-###############################################################################
-# This file is part of SWIFT.
-# Copyright (c) 2016 Matthieu Schaller (schaller@strw.leidenuniv.nl)
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##############################################################################
-
-import h5py
-from numpy import *
-import sys
-
-# Generates a swift IC file for the Kelvin-Helmholtz vortex in a periodic box
-
-# Parameters
-L2 = 256  # Particles along one edge in the low-density region
-gamma = 5.0 / 3.0  # Gas adiabatic index
-P1 = 2.5  # Central region pressure
-P2 = 2.5  # Outskirts pressure
-v1 = 0.5  # Central region velocity
-v2 = -0.5  # Outskirts vlocity
-rho1 = 2  # Central density
-rho2 = 1  # Outskirts density
-omega0 = 0.1
-sigma = 0.05 / sqrt(2)
-fileOutputName = "kelvinHelmholtz.hdf5"
-# ---------------------------------------------------
-
-# Start by generating grids of particles at the two densities
-numPart2 = L2 * L2
-L1 = int(sqrt(numPart2 / rho2 * rho1))
-numPart1 = L1 * L1
-
-print("N2 =", numPart2, "N1 =", numPart1)
-print("L2 =", L2, "L1 = ", L1)
-print("rho2 =", rho2, "rho1 =", (float(L1 * L1)) / (float(L2 * L2)))
-
-coords1 = zeros((numPart1, 3))
-coords2 = zeros((numPart2, 3))
-h1 = ones(numPart1) * 1.2348 / L1
-h2 = ones(numPart2) * 1.2348 / L2
-m1 = zeros(numPart1)
-m2 = zeros(numPart2)
-u1 = zeros(numPart1)
-u2 = zeros(numPart2)
-vel1 = zeros((numPart1, 3))
-vel2 = zeros((numPart2, 3))
-
-# Particles in the central region
-for i in range(L1):
-    for j in range(L1):
-
-        index = i * L1 + j
-
-        x = i / float(L1) + 1.0 / (2.0 * L1)
-        y = j / float(L1) + 1.0 / (2.0 * L1)
-
-        coords1[index, 0] = x
-        coords1[index, 1] = y
-        u1[index] = P1 / (rho1 * (gamma - 1.0))
-        vel1[index, 0] = v1
-
-# Particles in the outskirts
-for i in range(L2):
-    for j in range(L2):
-
-        index = i * L2 + j
-
-        x = i / float(L2) + 1.0 / (2.0 * L2)
-        y = j / float(L2) + 1.0 / (2.0 * L2)
-
-        coords2[index, 0] = x
-        coords2[index, 1] = y
-        u2[index] = P2 / (rho2 * (gamma - 1.0))
-        vel2[index, 0] = v2
-
-
-# Now concatenate arrays
-where1 = abs(coords1[:, 1] - 0.5) < 0.25
-where2 = abs(coords2[:, 1] - 0.5) > 0.25
-
-coords = append(coords1[where1, :], coords2[where2, :], axis=0)
-
-# print L2*(L2/2), L1*(L1/2)
-# print shape(coords), shape(coords1[where1,:]), shape(coords2[where2,:])
-# print shape(coords), shape(logical_not(coords1[where1,:])), shape(logical_not(coords2[where2,:]))
-
-vel = append(vel1[where1, :], vel2[where2, :], axis=0)
-h = append(h1[where1], h2[where2], axis=0)
-m = append(m1[where1], m2[where2], axis=0)
-u = append(u1[where1], u2[where2], axis=0)
-numPart = size(h)
-ids = linspace(1, numPart, numPart)
-mat = zeros(numPart)
-m[:] = (0.5 * rho1 + 0.5 * rho2) / float(numPart)
-
-# Velocity perturbation
-vel[:, 1] = (
-    omega0
-    * sin(4 * pi * coords[:, 0])
-    * (
-        exp(-((coords[:, 1] - 0.25) ** 2) / (2 * sigma ** 2))
-        + exp(-((coords[:, 1] - 0.75) ** 2) / (2 * sigma ** 2))
-    )
-)
-
-# File
-fileOutput = h5py.File(fileOutputName, "w")
-
-# Header
-grp = fileOutput.create_group("/Header")
-grp.attrs["BoxSize"] = [1.0, 1.0, 0.1]
-grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["Time"] = 0.0
-grp.attrs["NumFileOutputsPerSnapshot"] = 1
-grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
-grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["Dimension"] = 2
-
-# Units
-grp = fileOutput.create_group("/Units")
-grp.attrs["Unit length in cgs (U_L)"] = 1.0
-grp.attrs["Unit mass in cgs (U_M)"] = 1.0
-grp.attrs["Unit time in cgs (U_t)"] = 1.0
-grp.attrs["Unit current in cgs (U_I)"] = 1.0
-grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
-
-# Particle group
-grp = fileOutput.create_group("/PartType0")
-ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
-ds[()] = coords
-ds = grp.create_dataset("Velocities", (numPart, 3), "f")
-ds[()] = vel
-ds = grp.create_dataset("Masses", (numPart, 1), "f")
-ds[()] = m.reshape((numPart, 1))
-ds = grp.create_dataset("SmoothingLengths", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergies", (numPart, 1), "f")
-ds[()] = u.reshape((numPart, 1))
-ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
-ds[()] = ids.reshape((numPart, 1))
-ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
-ds[()] = mat.reshape((numPart, 1))
-
-fileOutput.close()
diff --git a/examples/Planetary/KelvinHelmholtz_2D/run.sh b/examples/Planetary/KelvinHelmholtz_2D/run.sh
deleted file mode 100755
index da6121423688415d08dd1fdcd26bb457a5c8a9eb..0000000000000000000000000000000000000000
--- a/examples/Planetary/KelvinHelmholtz_2D/run.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/bash
-
- # Generate the initial conditions if they are not present.
-if [ ! -e kelvinHelmholtz.hdf5 ]
-then
-    echo "Generating initial conditions for the Kelvin-Helmholtz example..."
-    python3 makeIC.py
-fi
-
-# Run SWIFT
-../../../swift --hydro --threads=4 kelvinHelmholtz.yml 2>&1 | tee output.log
-
-
-# Plot the solution
-python3 ../../HydroTests/KelvinHelmholtz_2D/makeMovieSwiftsimIO.py
diff --git a/examples/Planetary/KelvinHelmholtz_EarthLike_3D/README.md b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..fc65e782a35dbd02c99b09c48a31a39b11321f55
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/README.md
@@ -0,0 +1,21 @@
+Kelvin--Helmholtz Instabilty (Earth-like, equal mass, 3D)
+--------------
+
+This is a 3D version of the Kelvin--Helmholtz instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025), section 4.4.
+
+This test uses particles of equal mass and has sharp density and velocity
+discontinuities. Equations of state and conditions are representative of those
+within Earth's interior.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/KelvinHelmholtz_EarthLike_3D/kelvin_helmholtz.yml b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/kelvin_helmholtz.yml
new file mode 100644
index 0000000000000000000000000000000000000000..58b43e702f04d3cea962add5773fbb23567e9960
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/kelvin_helmholtz.yml
@@ -0,0 +1,46 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:        5.9724e27   # Grams
+  UnitLength_in_cgs:      6.371e8     # Centimeters
+  UnitVelocity_in_cgs:    6.371e8     # Centimeters per second
+  UnitCurrent_in_cgs:     1           # Amperes
+  UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   10520 # The end time of the simulation (in internal units). Corresponding to 2 \tau_{KH}.
+  dt_min:     1e-9  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            kelvin_helmholtz # Common part of the name of output files
+  time_first:          0.               # Time of the first output (in internal units)
+  delta_time:          526              # Time difference between consecutive outputs (in internal units). Corresponding to 0.1 \tau_{KH}.
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          526 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./kelvin_helmholtz.hdf5      # The file to read
+  periodic:   1
+
+Scheduler:
+    max_top_level_cells:     40
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
diff --git a/examples/Planetary/KelvinHelmholtz_EarthLike_3D/makeIC.py b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba1a9634c3471adba3a7f581b1e6cc221417deff
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/makeIC.py
@@ -0,0 +1,198 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+
+# Generates a swift IC file for the Kelvin-Helmholtz test in a periodic box
+
+# Constants
+R_earth = 6371000  # Earth radius
+
+# Parameters
+N2_l = 128  # Particles along one edge in the low-density region
+N2_depth = 18  # Particles in z direction in low-density region
+matID1 = 402  # Central region material ID: ANEOS Fe85Si15
+matID2 = 400  # Outskirts material ID: ANEOS forsterite
+P1 = 1.2e11  # Central region pressure
+P2 = 1.2e11  # Outskirts pressure
+u1 = 4069874  # Central region specific internal energy
+u2 = 9899952  # Outskirts specific internal energy
+rho1_approx = 10000  # Central region density. Readjusted later
+rho2 = 5000  # Outskirts density
+boxsize_l = R_earth  # size of simulation box in x and y dimension
+v1 = boxsize_l / 10000  # Central region velocity
+v2 = -boxsize_l / 10000  # Outskirts velocity
+boxsize_depth = boxsize_l * N2_depth / N2_l  # size of simulation box in z dimension
+mass = rho2 * (boxsize_l * boxsize_l * boxsize_depth) / (N2_l * N2_l * N2_depth)
+fileOutputName = "kelvin_helmholtz.hdf5"
+# ---------------------------------------------------
+
+# Start by calculating N1_l and rho1
+numPart2 = N2_l * N2_l * N2_depth
+numPart1_approx = int(numPart2 / rho2 * rho1_approx)
+
+# Consider numPart1 = N1_l * N1_l * N1_depth
+# Substituting boxsize_depth / boxsize_l = N1_depth / N1_l gives,
+# numPart1 = N1_l * N1_l * (boxsize_depth / boxsize_l) * N1_l, which ranges to:
+N1_l = int(np.cbrt(numPart1_approx * boxsize_l / boxsize_depth))
+# Make sure this is a multiple of 4 since this is the number of KH vortices
+N1_l -= N1_l % 4
+
+N1_depth = int(boxsize_depth * N1_l / boxsize_l)
+numPart1 = int(N1_l * N1_l * N1_depth)
+
+# The density of the central region can then be calculated
+rho1 = mass * (N1_l * N1_l * N1_depth) / (boxsize_l * boxsize_l * boxsize_depth)
+
+# Now construct two lattices of particles in the two regions
+A2_coords1 = np.empty((numPart1, 3))
+A2_coords2 = np.empty((numPart2, 3))
+A2_vel1 = np.zeros((numPart1, 3))
+A2_vel2 = np.zeros((numPart2, 3))
+A2_vel1[:, 0] = v1
+A2_vel2[:, 0] = v2
+
+A1_mat1 = np.full(numPart1, matID1)
+A1_mat2 = np.full(numPart2, matID2)
+A1_m1 = np.full(numPart1, mass)
+A1_m2 = np.full(numPart2, mass)
+A1_rho1 = np.full(numPart1, rho1)
+A1_rho2 = np.full(numPart2, rho2)
+A1_u1 = np.full(numPart1, u1)
+A1_u2 = np.full(numPart2, u2)
+A1_h1 = np.full(numPart1, boxsize_l / N1_l)
+A1_h2 = np.full(numPart2, boxsize_l / N2_l)
+
+# Particles in the central region
+for i in range(N1_depth):
+    for j in range(N1_l):
+        for k in range(N1_l):
+            index = i * N1_l ** 2 + j * N1_l + k
+            A2_coords1[index, 0] = (j / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 1] = (k / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 2] = (
+                i / float(N1_depth) + 1.0 / (2.0 * N1_depth)
+            ) * boxsize_depth
+
+# Particles in the outskirts
+for i in range(N2_depth):
+    for j in range(N2_l):
+        for k in range(N2_l):
+            index = i * N2_l ** 2 + j * N2_l + k
+            A2_coords2[index, 0] = (j / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 1] = (k / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 2] = (
+                i / float(N2_depth) + 1.0 / (2.0 * N2_depth)
+            ) * boxsize_depth
+
+
+# Masks for the particles to be selected for the outer and inner regions
+mask1 = abs(A2_coords1[:, 1] - 0.5 * boxsize_l) < 0.25 * boxsize_l
+mask2 = abs(A2_coords2[:, 1] - 0.5 * boxsize_l) > 0.25 * boxsize_l
+
+# The positions of the particles are now selected
+# and the placement of the lattices are adjusted to give appropriate interfaces
+A2_coords_inside = A2_coords1[mask1, :]
+A2_coords_outside = A2_coords2[mask2, :]
+
+# Calculate the separation of particles across the density discontinuity
+pcl_separation_1 = np.cbrt(mass / rho1)
+pcl_separation_2 = np.cbrt(mass / rho2)
+boundary_separation = 0.5 * (pcl_separation_1 + pcl_separation_2)
+
+# Shift all the "inside" particles to get boundary_separation across the bottom interface
+min_y_inside = np.min(A2_coords_inside[:, 1])
+max_y_outside_bot = np.max(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l < -0.25 * boxsize_l, 1]
+)
+shift_distance_bot = boundary_separation - (min_y_inside - max_y_outside_bot)
+A2_coords_inside[:, 1] += shift_distance_bot
+
+# Shift the top section of the "outside" particles to get boundary_separation across the top interface
+max_y_inside = np.max(A2_coords_inside[:, 1])
+min_y_outside_top = np.min(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1]
+)
+shift_distance_top = boundary_separation - (min_y_outside_top - max_y_inside)
+A2_coords_outside[
+    A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1
+] += shift_distance_top
+
+# Adjust box size in y direction based on the shifting of the lattices.
+new_box_y = boxsize_l + shift_distance_top
+
+# Now the two lattices can be combined
+A2_coords = np.append(A2_coords_inside, A2_coords_outside, axis=0)
+A2_vel = np.append(A2_vel1[mask1], A2_vel2[mask2], axis=0)
+A1_mat = np.append(A1_mat1[mask1], A1_mat2[mask2], axis=0)
+A1_m = np.append(A1_m1[mask1], A1_m2[mask2], axis=0)
+A1_rho = np.append(A1_rho1[mask1], A1_rho2[mask2], axis=0)
+A1_u = np.append(A1_u1[mask1], A1_u2[mask2], axis=0)
+A1_h = np.append(A1_h1[mask1], A1_h2[mask2], axis=0)
+numPart = np.size(A1_m)
+A1_ids = np.linspace(1, numPart, numPart)
+
+# Finally add the velocity perturbation
+vel_perturb_factor = 0.01 * (v1 - v2)
+A2_vel[:, 1] = vel_perturb_factor * np.sin(
+    2 * np.pi * A2_coords[:, 0] / (0.5 * boxsize_l)
+)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [boxsize_l, new_box_y, boxsize_depth]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 100.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1000.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/KelvinHelmholtz_EarthLike_3D/plotModeGrowth.py b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/plotModeGrowth.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4a1408aeaddd8cb6c141daf264126b22da4bec4
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/plotModeGrowth.py
@@ -0,0 +1,131 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot Mode growth of the 3D Kelvin--Helmholtz instability.
+This is based on the quantity calculated in Eqns. 10--13 of McNally et al. 2012
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def calculate_mode_growth(snap, vy_init_amp, wavelength):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A2_pos = f["/PartType0/Coordinates"][:, :] / boxsize_l
+        A2_vel = f["/PartType0/Velocities"][:, :] / (vy_init_amp * boxsize_l)
+        A1_h = f["/PartType0/SmoothingLengths"][:] / boxsize_l
+
+    # Adjust positions
+    A2_pos[:, 0] += 0.5
+    A2_pos[:, 1] += 0.5
+
+    # Masks to select the upper and lower halfs of the simulations
+    mask_up = A2_pos[:, 1] >= 0.5
+    mask_down = A2_pos[:, 1] < 0.5
+
+    # McNally et al. 2012 Eqn. 10
+    s = np.empty(len(A1_h))
+    s[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    s[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 11
+    c = np.empty(len(A1_h))
+    c[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    c[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 12
+    d = np.empty(len(A1_h))
+    d[mask_down] = A1_h[mask_down] ** 3 * np.exp(
+        -2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength
+    )
+    d[mask_up] = A1_h[mask_up] ** 3 * np.exp(
+        -2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength
+    )
+
+    # McNally et al. 2012 Eqn. 13
+    M = 2 * np.sqrt((np.sum(s) / np.sum(d)) ** 2 + (np.sum(c) / np.sum(d)) ** 2)
+
+    return M
+
+
+if __name__ == "__main__":
+
+    # Simulation paramerters for nomralisation of mode growth
+    vy_init_amp = (
+        2e-6
+    )  # Initial amplitude of y velocity perturbation in units of the boxsize per second
+    wavelength = 0.5  # wavelength of initial perturbation in units of the boxsize
+
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    ax_len = 5
+    fig = plt.figure(figsize=(9, 6))
+    gs = mpl.gridspec.GridSpec(nrows=40, ncols=60)
+    ax = plt.subplot(gs[:, :])
+
+    # Snapshots and corresponding times
+    snaps = np.arange(21)
+    times = 0.1 * snaps
+
+    # Calculate mode mode growth
+    mode_growth = np.empty(len(snaps))
+    for i, snap in enumerate(snaps):
+        M = calculate_mode_growth(snap, vy_init_amp, wavelength)
+        mode_growth[i] = M
+
+    # Plot
+    ax.plot(times, np.log10(mode_growth), linewidth=1.5)
+
+    ax.set_ylabel(r"$\log( \, M \; / \; M^{}_0 \, )$", fontsize=18)
+    ax.set_xlabel(r"$t \; / \; \tau^{}_{\rm KH}$", fontsize=18)
+    ax.minorticks_on()
+    ax.tick_params(which="major", direction="in")
+    ax.tick_params(which="minor", direction="in")
+    ax.tick_params(labelsize=14)
+
+    plt.savefig("mode_growth.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_EarthLike_3D/plotSnapshots.py b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..c74617536b557f7cc4dd4f7023f353b23a44c936
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/plotSnapshots.py
@@ -0,0 +1,210 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Kelvin--Helmholtz instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len = 5
+    ax_gap_len = n_gs_ax_gap * ax_len / n_gs_ax
+    cbar_gap_len = n_gs_cbar_gap * ax_len / n_gs_ax
+    cbar_len = n_gs_cbar * ax_len / n_gs_ax
+
+    fig = plt.figure(
+        figsize=(3 * ax_len + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax, ncols=3 * n_gs_ax + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax, :n_gs_ax])
+    ax_1 = plt.subplot(gs[:n_gs_ax, n_gs_ax + n_gs_ax_gap : 2 * n_gs_ax + n_gs_ax_gap])
+    ax_2 = plt.subplot(
+        gs[:n_gs_ax, 2 * n_gs_ax + 2 * n_gs_ax_gap : 3 * n_gs_ax + 2 * n_gs_ax_gap]
+    )
+
+    cax_0 = plt.subplot(
+        gs[
+            : int(0.5 * n_gs_ax) - 1,
+            3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+    cax_1 = plt.subplot(
+        gs[
+            int(0.5 * n_gs_ax) + 1 :,
+            3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+    caxs = [cax_0, cax_1]
+
+    return axs, caxs
+
+
+def plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        # Units from file metadata to SI
+        m = float(f["Units"].attrs["Unit mass in cgs (U_M)"][0]) * 1e-3
+        l = float(f["Units"].attrs["Unit length in cgs (U_L)"][0]) * 1e-2
+
+        boxsize_l = f["Header"].attrs["BoxSize"][0] * l
+        A1_x = f["/PartType0/Coordinates"][:, 0] * l
+        A1_y = f["/PartType0/Coordinates"][:, 1] * l
+        A1_z = f["/PartType0/Coordinates"][:, 2] * l
+        A1_rho = f["/PartType0/Densities"][:] * (m / l ** 3)
+        A1_m = f["/PartType0/Masses"][:] * m
+        A1_mat_id = f["/PartType0/MaterialIDs"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+    A1_mat_id = A1_mat_id[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.2 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+    A1_mat_id_slice = A1_mat_id[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_l ** 2
+
+    mask_mat1 = A1_mat_id_slice == mat_id1
+    mask_mat2 = A1_mat_id_slice == mat_id2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat1],
+        A1_y_slice[mask_mat1],
+        c=A1_rho_slice[mask_mat1],
+        norm=norm1,
+        cmap=cmap1,
+        s=A1_size[mask_mat1],
+        edgecolors="none",
+    )
+
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat2],
+        A1_y_slice[mask_mat2],
+        c=A1_rho_slice[mask_mat2],
+        norm=norm2,
+        cmap=cmap2,
+        s=A1_size[mask_mat2],
+        edgecolors="none",
+    )
+
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0, boxsize_l))
+    ax.set_ylim((0, boxsize_l))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap1 = plt.get_cmap("YlOrRd")
+    mat_id1 = 402
+    rho_min1 = 7950
+    rho_max1 = 10050
+    norm1 = mpl.colors.Normalize(vmin=rho_min1, vmax=rho_max1)
+
+    cmap2 = plt.get_cmap("Blues_r")
+    mat_id2 = 400
+    rho_min2 = 4950
+    rho_max2 = 5550
+    norm2 = mpl.colors.Normalize(vmin=rho_min2, vmax=rho_max2)
+
+    # Generate axes
+    axs, caxs = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [10, 15, 20]
+    times = ["1.0", "1.5", "2.0"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2)
+        ax.text(
+            0.5,
+            -0.1,
+            r"$t =\;$" + time + r"$\, \tau^{}_{\rm KH}$",
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm1 = plt.cm.ScalarMappable(cmap=cmap1, norm=norm1)
+    cbar1 = plt.colorbar(sm1, caxs[0])
+    cbar1.ax.tick_params(labelsize=14)
+    cbar1.set_label(r"Iron density (kg/m$^3$)", rotation=90, labelpad=8, fontsize=12)
+
+    sm2 = plt.cm.ScalarMappable(cmap=cmap2, norm=norm2)
+    cbar2 = plt.colorbar(sm2, caxs[1])
+    cbar2.ax.tick_params(labelsize=14)
+    cbar2.set_label(r"Rock density (kg/m$^3$)", rotation=90, labelpad=16, fontsize=12)
+
+    plt.savefig("kelvin_helmholtz.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_EarthLike_3D/run.sh b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..751884b8869a6e02369372f2e504a38c310dc67d
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_EarthLike_3D/run.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e kelvin_helmholtz.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Kelvin--Helmholtz test..."
+    python3 makeIC.py
+fi
+
+# Download the equation of state tables if not already present
+if [ ! -e ../EoSTables/ANEOS_forsterite_S19.txt ]
+then
+    cd ../EoSTables
+    ./get_eos_tables.sh
+    cd -
+fi
+
+# Run SWIFT
+../../../swift --hydro --threads=4 kelvin_helmholtz.yml 2>&1 | tee output_kelvin_helmholtz.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
+python3 ./plotModeGrowth.py
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_3D/README.md b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..2d022165395fa6182535ea5bc413b18e79bf2c0c
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/README.md
@@ -0,0 +1,20 @@
+Kelvin--Helmholtz Instabilty (ideal gas, equal mass, 3D)
+--------------
+
+This is a 3D version of the Kelvin--Helmholtz instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025), section 4.3.2.
+
+This test uses particles of equal mass and has sharp density and velocity
+discontinuities.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_3D/kelvin_helmholtz.yml b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/kelvin_helmholtz.yml
new file mode 100644
index 0000000000000000000000000000000000000000..445e2eac785b0e60c89785e408c471caa23f9159
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/kelvin_helmholtz.yml
@@ -0,0 +1,41 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1   # Grams
+  UnitLength_in_cgs:   1   # Centimeters
+  UnitVelocity_in_cgs: 1   # Centimeters per second
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.0    # The starting time of the simulation (in internal units).
+  time_end:   2.1    # The end time of the simulation (in internal units). Corresponding to 2 \tau_{KH}.
+  dt_min:     1e-9   # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            kelvin_helmholtz  # Common part of the name of output files
+  time_first:          0.                # Time of the first output (in internal units)
+  delta_time:          0.105             # Time difference between consecutive outputs (in internal units). Corresponding to 0.1 \tau_{KH}.
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          0.105 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./kelvin_helmholtz.hdf5     # The file to read
+  periodic:   1
+  
+Scheduler:
+    max_top_level_cells:      40         # Maximal number of top-level cells in any dimension.
+    
+# Parameters related to the equation of state
+EoS:
+    planetary_use_idg_def:    1     # Default ideal gas, material ID 0
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_3D/makeIC.py b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..442595ed409e080c4d9f795b29112e7c1b0ceacf
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/makeIC.py
@@ -0,0 +1,194 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+
+# Generates a swift IC file for the Kelvin-Helmholtz test in a periodic box
+
+# Parameters
+N2_l = 128  # Particles along one edge in the low-density region
+N2_depth = 18  # Particles in z direction in low-density region
+gamma = 5.0 / 3.0  # Gas adiabatic index
+matID1 = 0  # Central region material ID: ideal gas
+matID2 = 0  # Outskirts material ID: ideal gas
+P1 = 2.5  # Central region pressure
+P2 = 2.5  # Outskirts pressure
+rho1_approx = 2  # Central region density. Readjusted later
+rho2 = 1  # Outskirts density
+v1 = 0.5  # Central region velocity
+v2 = -0.5  # Outskirts velocity
+boxsize_l = 1  # size of simulation box in x and y dimension
+boxsize_depth = boxsize_l * N2_depth / N2_l  # size of simulation box in z dimension
+mass = rho2 * (boxsize_l * boxsize_l * boxsize_depth) / (N2_l * N2_l * N2_depth)
+fileOutputName = "kelvin_helmholtz.hdf5"
+# ---------------------------------------------------
+
+# Start by calculating N1_l and rho1
+numPart2 = N2_l * N2_l * N2_depth
+numPart1_approx = int(numPart2 / rho2 * rho1_approx)
+
+# Consider numPart1 = N1_l * N1_l * N1_depth
+# Substituting boxsize_depth / boxsize_l = N1_depth / N1_l gives,
+# numPart1 = N1_l * N1_l * (boxsize_depth / boxsize_l) * N1_l, which ranges to:
+N1_l = int(np.cbrt(numPart1_approx * boxsize_l / boxsize_depth))
+# Make sure this is a multiple of 4 since this is the number of KH vortices
+N1_l -= N1_l % 4
+
+N1_depth = int(boxsize_depth * N1_l / boxsize_l)
+numPart1 = int(N1_l * N1_l * N1_depth)
+
+# The density of the central region can then be calculated
+rho1 = mass * (N1_l * N1_l * N1_depth) / (boxsize_l * boxsize_l * boxsize_depth)
+
+# Now construct two lattices of particles in the two regions
+A2_coords1 = np.empty((numPart1, 3))
+A2_coords2 = np.empty((numPart2, 3))
+A2_vel1 = np.zeros((numPart1, 3))
+A2_vel2 = np.zeros((numPart2, 3))
+A2_vel1[:, 0] = v1
+A2_vel2[:, 0] = v2
+
+A1_mat1 = np.full(numPart1, matID1)
+A1_mat2 = np.full(numPart2, matID2)
+A1_m1 = np.full(numPart1, mass)
+A1_m2 = np.full(numPart2, mass)
+A1_rho1 = np.full(numPart1, rho1)
+A1_rho2 = np.full(numPart2, rho2)
+A1_u1 = np.full(numPart1, P1 / (rho1 * (gamma - 1.0)))
+A1_u2 = np.full(numPart2, P2 / (rho2 * (gamma - 1.0)))
+A1_h1 = np.full(numPart1, boxsize_l / N1_l)
+A1_h2 = np.full(numPart2, boxsize_l / N2_l)
+
+# Particles in the central region
+for i in range(N1_depth):
+    for j in range(N1_l):
+        for k in range(N1_l):
+            index = i * N1_l ** 2 + j * N1_l + k
+            A2_coords1[index, 0] = (j / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 1] = (k / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 2] = (
+                i / float(N1_depth) + 1.0 / (2.0 * N1_depth)
+            ) * boxsize_depth
+
+# Particles in the outskirts
+for i in range(N2_depth):
+    for j in range(N2_l):
+        for k in range(N2_l):
+            index = i * N2_l ** 2 + j * N2_l + k
+            A2_coords2[index, 0] = (j / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 1] = (k / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 2] = (
+                i / float(N2_depth) + 1.0 / (2.0 * N2_depth)
+            ) * boxsize_depth
+
+
+# Masks for the particles to be selected for the outer and inner regions
+mask1 = abs(A2_coords1[:, 1] - 0.5 * boxsize_l) < 0.25 * boxsize_l
+mask2 = abs(A2_coords2[:, 1] - 0.5 * boxsize_l) > 0.25 * boxsize_l
+
+# The positions of the particles are now selected
+# and the placement of the lattices are adjusted to give appropriate interfaces
+A2_coords_inside = A2_coords1[mask1, :]
+A2_coords_outside = A2_coords2[mask2, :]
+
+# Calculate the separation of particles across the density discontinuity
+pcl_separation_1 = np.cbrt(mass / rho1)
+pcl_separation_2 = np.cbrt(mass / rho2)
+boundary_separation = 0.5 * (pcl_separation_1 + pcl_separation_2)
+
+# Shift all the "inside" particles to get boundary_separation across the bottom interface
+min_y_inside = np.min(A2_coords_inside[:, 1])
+max_y_outside_bot = np.max(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l < -0.25 * boxsize_l, 1]
+)
+shift_distance_bot = boundary_separation - (min_y_inside - max_y_outside_bot)
+A2_coords_inside[:, 1] += shift_distance_bot
+
+# Shift the top section of the "outside" particles to get boundary_separation across the top interface
+max_y_inside = np.max(A2_coords_inside[:, 1])
+min_y_outside_top = np.min(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1]
+)
+shift_distance_top = boundary_separation - (min_y_outside_top - max_y_inside)
+A2_coords_outside[
+    A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1
+] += shift_distance_top
+
+# Adjust box size in y direction based on the shifting of the lattices.
+new_box_y = boxsize_l + shift_distance_top
+
+# Now the two lattices can be combined
+A2_coords = np.append(A2_coords_inside, A2_coords_outside, axis=0)
+A2_vel = np.append(A2_vel1[mask1], A2_vel2[mask2], axis=0)
+A1_mat = np.append(A1_mat1[mask1], A1_mat2[mask2], axis=0)
+A1_m = np.append(A1_m1[mask1], A1_m2[mask2], axis=0)
+A1_rho = np.append(A1_rho1[mask1], A1_rho2[mask2], axis=0)
+A1_u = np.append(A1_u1[mask1], A1_u2[mask2], axis=0)
+A1_h = np.append(A1_h1[mask1], A1_h2[mask2], axis=0)
+numPart = np.size(A1_m)
+A1_ids = np.linspace(1, numPart, numPart)
+
+# Finally add the velocity perturbation
+vel_perturb_factor = 0.01 * (v1 - v2)
+A2_vel[:, 1] = vel_perturb_factor * np.sin(
+    2 * np.pi * A2_coords[:, 0] / (0.5 * boxsize_l)
+)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [boxsize_l, new_box_y, boxsize_depth]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_3D/plotModeGrowth.py b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/plotModeGrowth.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e33269533e56c0c95862362b370fe622f5bb823
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/plotModeGrowth.py
@@ -0,0 +1,131 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot Mode growth of the 3D Kelvin--Helmholtz instability.
+This is based on the quantity calculated in Eqns. 10--13 of McNally et al. 2012
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def calculate_mode_growth(snap, vy_init_amp, wavelength):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A2_pos = f["/PartType0/Coordinates"][:, :] / boxsize_l
+        A2_vel = f["/PartType0/Velocities"][:, :] / (vy_init_amp * boxsize_l)
+        A1_h = f["/PartType0/SmoothingLengths"][:] / boxsize_l
+
+    # Adjust positions
+    A2_pos[:, 0] += 0.5
+    A2_pos[:, 1] += 0.5
+
+    # Masks to select the upper and lower halfs of the simulations
+    mask_up = A2_pos[:, 1] >= 0.5
+    mask_down = A2_pos[:, 1] < 0.5
+
+    # McNally et al. 2012 Eqn. 10
+    s = np.empty(len(A1_h))
+    s[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    s[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 11
+    c = np.empty(len(A1_h))
+    c[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    c[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 12
+    d = np.empty(len(A1_h))
+    d[mask_down] = A1_h[mask_down] ** 3 * np.exp(
+        -2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength
+    )
+    d[mask_up] = A1_h[mask_up] ** 3 * np.exp(
+        -2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength
+    )
+
+    # McNally et al. 2012 Eqn. 13
+    M = 2 * np.sqrt((np.sum(s) / np.sum(d)) ** 2 + (np.sum(c) / np.sum(d)) ** 2)
+
+    return M
+
+
+if __name__ == "__main__":
+
+    # Simulation paramerters for nomralisation of mode growth
+    vy_init_amp = (
+        0.01
+    )  # Initial amplitude of y velocity perturbation in units of the boxsize per second
+    wavelength = 0.5  # wavelength of initial perturbation in units of the boxsize
+
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    ax_len = 5
+    fig = plt.figure(figsize=(9, 6))
+    gs = mpl.gridspec.GridSpec(nrows=40, ncols=60)
+    ax = plt.subplot(gs[:, :])
+
+    # Snapshots and corresponding times
+    snaps = np.arange(21)
+    times = 0.1 * snaps
+
+    # Calculate mode mode growth
+    mode_growth = np.empty(len(snaps))
+    for i, snap in enumerate(snaps):
+        M = calculate_mode_growth(snap, vy_init_amp, wavelength)
+        mode_growth[i] = M
+
+    # Plot
+    ax.plot(times, np.log10(mode_growth), linewidth=1.5)
+
+    ax.set_ylabel(r"$\log( \, M \; / \; M^{}_0 \, )$", fontsize=18)
+    ax.set_xlabel(r"$t \; / \; \tau^{}_{\rm KH}$", fontsize=18)
+    ax.minorticks_on()
+    ax.tick_params(which="major", direction="in")
+    ax.tick_params(which="minor", direction="in")
+    ax.tick_params(labelsize=14)
+
+    plt.savefig("mode_growth.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_3D/plotSnapshots.py b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..963568d1b2f1080a5b3b0d845848ece1fa8ced1e
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/plotSnapshots.py
@@ -0,0 +1,163 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Kelvin--Helmholtz instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len = 5
+    ax_gap_len = n_gs_ax_gap * ax_len / n_gs_ax
+    cbar_gap_len = n_gs_cbar_gap * ax_len / n_gs_ax
+    cbar_len = n_gs_cbar * ax_len / n_gs_ax
+
+    fig = plt.figure(
+        figsize=(3 * ax_len + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax, ncols=3 * n_gs_ax + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax, :n_gs_ax])
+    ax_1 = plt.subplot(gs[:n_gs_ax, n_gs_ax + n_gs_ax_gap : 2 * n_gs_ax + n_gs_ax_gap])
+    ax_2 = plt.subplot(
+        gs[:n_gs_ax, 2 * n_gs_ax + 2 * n_gs_ax_gap : 3 * n_gs_ax + 2 * n_gs_ax_gap]
+    )
+    cax = plt.subplot(
+        gs[
+            :n_gs_ax,
+            3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+
+    return axs, cax
+
+
+def plot_kh(ax, snap, cmap, norm):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A1_x = f["/PartType0/Coordinates"][:, 0]
+        A1_y = f["/PartType0/Coordinates"][:, 1]
+        A1_z = f["/PartType0/Coordinates"][:, 2]
+        A1_rho = f["/PartType0/Densities"][:]
+        A1_m = f["/PartType0/Masses"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.2 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_l ** 2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice,
+        A1_y_slice,
+        c=A1_rho_slice,
+        norm=norm,
+        cmap=cmap,
+        s=A1_size,
+        edgecolors="none",
+    )
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0, boxsize_l))
+    ax.set_ylim((0, boxsize_l))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap = plt.get_cmap("Spectral_r")
+    vmin, vmax = 0.95, 2.05
+    norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
+
+    # Generate axes
+    axs, cax = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [10, 15, 20]
+    times = ["1.0", "1.5", "2.0"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, cmap, norm)
+        ax.text(
+            0.5,
+            -0.1,
+            r"$t =\;$" + time + r"$\, \tau^{}_{\rm KH}$",
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
+    cbar = plt.colorbar(sm, cax)
+    cbar.ax.tick_params(labelsize=14)
+    cbar.set_label("Density", rotation=90, labelpad=8, fontsize=18)
+
+    plt.savefig("kelvin_helmholtz.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_3D/run.sh b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5fe5ca13702e5b91eb89ab4f791230242360b6da
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_3D/run.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e kelvin_helmholtz.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Kelvin--Helmholtz test..."
+    python3 makeIC.py
+fi
+
+# Run SWIFT
+../../../swift --hydro --threads=4 kelvin_helmholtz.yml 2>&1 | tee output_kelvin_helmholtz.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
+python3 ./plotModeGrowth.py
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/README.md b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..e1fb6683339afe23294a116dfc47a8484691527e
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/README.md
@@ -0,0 +1,20 @@
+Kelvin--Helmholtz Instabilty (ideal gas, equal mass, 1:10 density ratio, 3D)
+--------------
+
+This is a 3D version of the Kelvin--Helmholtz instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025), section 4.3.3.
+
+This test uses particles of equal mass and has sharp density and velocity
+discontinuities. The ratio of densities across the discontinuity is approximately 1:10.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/kelvin_helmholtz.yml b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/kelvin_helmholtz.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a7f6cbbd388d11026a01bb7153786b1ab907b39e
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/kelvin_helmholtz.yml
@@ -0,0 +1,41 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1   # Grams
+  UnitLength_in_cgs:   1   # Centimeters
+  UnitVelocity_in_cgs: 1   # Centimeters per second
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.0    # The starting time of the simulation (in internal units).
+  time_end:   0.6705 # The end time of the simulation (in internal units). Corresponding to 2 \tau_{KH}.
+  dt_min:     1e-9   # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            kelvin_helmholtz  # Common part of the name of output files
+  time_first:          0.                # Time of the first output (in internal units)
+  delta_time:          0.0447            # Time difference between consecutive outputs (in internal units). Corresponding to 0.1 \tau_{KH}.
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          0.0447 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./kelvin_helmholtz.hdf5     # The file to read
+  periodic:   1
+  
+Scheduler:
+    max_top_level_cells:      80         # Maximal number of top-level cells in any dimension.
+    
+# Parameters related to the equation of state
+EoS:
+    planetary_use_idg_def:    1     # Default ideal gas, material ID 0
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/makeIC.py b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c4eb8333c28bf49aece0285d4b238bd876c0928
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/makeIC.py
@@ -0,0 +1,194 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+
+# Generates a swift IC file for the Kelvin-Helmholtz test in a periodic box
+
+# Parameters
+N2_l = 256  # Particles along one edge in the low-density region
+N2_depth = 18  # Particles in z direction in low-density region
+gamma = 5.0 / 3.0  # Gas adiabatic index
+matID1 = 0  # Central region material ID: ideal gas
+matID2 = 0  # Outskirts material ID: ideal gas
+P1 = 2.5  # Central region pressure
+P2 = 2.5  # Outskirts pressure
+rho1_approx = 11  # Central region density. Readjusted later
+rho2 = 1  # Outskirts density
+v1 = 0.5  # Central region velocity
+v2 = -0.5  # Outskirts velocity
+boxsize_l = 1  # size of simulation box in x and y dimension
+boxsize_depth = boxsize_l * N2_depth / N2_l  # size of simulation box in z dimension
+mass = rho2 * (boxsize_l * boxsize_l * boxsize_depth) / (N2_l * N2_l * N2_depth)
+fileOutputName = "kelvin_helmholtz.hdf5"
+# ---------------------------------------------------
+
+# Start by calculating N1_l and rho1
+numPart2 = N2_l * N2_l * N2_depth
+numPart1_approx = int(numPart2 / rho2 * rho1_approx)
+
+# Consider numPart1 = N1_l * N1_l * N1_depth
+# Substituting boxsize_depth / boxsize_l = N1_depth / N1_l gives,
+# numPart1 = N1_l * N1_l * (boxsize_depth / boxsize_l) * N1_l, which ranges to:
+N1_l = int(np.cbrt(numPart1_approx * boxsize_l / boxsize_depth))
+# Make sure this is a multiple of 16 since this is the number of KH vortices
+N1_l -= N1_l % 16
+
+N1_depth = int(boxsize_depth * N1_l / boxsize_l)
+numPart1 = int(N1_l * N1_l * N1_depth)
+
+# The density of the central region can then be calculated
+rho1 = mass * (N1_l * N1_l * N1_depth) / (boxsize_l * boxsize_l * boxsize_depth)
+
+# Now construct two lattices of particles in the two regions
+A2_coords1 = np.empty((numPart1, 3))
+A2_coords2 = np.empty((numPart2, 3))
+A2_vel1 = np.zeros((numPart1, 3))
+A2_vel2 = np.zeros((numPart2, 3))
+A2_vel1[:, 0] = v1
+A2_vel2[:, 0] = v2
+
+A1_mat1 = np.full(numPart1, matID1)
+A1_mat2 = np.full(numPart2, matID2)
+A1_m1 = np.full(numPart1, mass)
+A1_m2 = np.full(numPart2, mass)
+A1_rho1 = np.full(numPart1, rho1)
+A1_rho2 = np.full(numPart2, rho2)
+A1_u1 = np.full(numPart1, P1 / (rho1 * (gamma - 1.0)))
+A1_u2 = np.full(numPart2, P2 / (rho2 * (gamma - 1.0)))
+A1_h1 = np.full(numPart1, boxsize_l / N1_l)
+A1_h2 = np.full(numPart2, boxsize_l / N2_l)
+
+# Particles in the central region
+for i in range(N1_depth):
+    for j in range(N1_l):
+        for k in range(N1_l):
+            index = i * N1_l ** 2 + j * N1_l + k
+            A2_coords1[index, 0] = (j / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 1] = (k / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 2] = (
+                i / float(N1_depth) + 1.0 / (2.0 * N1_depth)
+            ) * boxsize_depth
+
+# Particles in the outskirts
+for i in range(N2_depth):
+    for j in range(N2_l):
+        for k in range(N2_l):
+            index = i * N2_l ** 2 + j * N2_l + k
+            A2_coords2[index, 0] = (j / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 1] = (k / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 2] = (
+                i / float(N2_depth) + 1.0 / (2.0 * N2_depth)
+            ) * boxsize_depth
+
+
+# Masks for the particles to be selected for the outer and inner regions
+mask1 = abs(A2_coords1[:, 1] - 0.5 * boxsize_l) < 0.25 * boxsize_l
+mask2 = abs(A2_coords2[:, 1] - 0.5 * boxsize_l) > 0.25 * boxsize_l
+
+# The positions of the particles are now selected
+# and the placement of the lattices are adjusted to give appropriate interfaces
+A2_coords_inside = A2_coords1[mask1, :]
+A2_coords_outside = A2_coords2[mask2, :]
+
+# Calculate the separation of particles across the density discontinuity
+pcl_separation_1 = np.cbrt(mass / rho1)
+pcl_separation_2 = np.cbrt(mass / rho2)
+boundary_separation = 0.5 * (pcl_separation_1 + pcl_separation_2)
+
+# Shift all the "inside" particles to get boundary_separation across the bottom interface
+min_y_inside = np.min(A2_coords_inside[:, 1])
+max_y_outside_bot = np.max(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l < -0.25 * boxsize_l, 1]
+)
+shift_distance_bot = boundary_separation - (min_y_inside - max_y_outside_bot)
+A2_coords_inside[:, 1] += shift_distance_bot
+
+# Shift the top section of the "outside" particles to get boundary_separation across the top interface
+max_y_inside = np.max(A2_coords_inside[:, 1])
+min_y_outside_top = np.min(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1]
+)
+shift_distance_top = boundary_separation - (min_y_outside_top - max_y_inside)
+A2_coords_outside[
+    A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1
+] += shift_distance_top
+
+# Adjust box size in y direction based on the shifting of the lattices.
+new_box_y = boxsize_l + shift_distance_top
+
+# Now the two lattices can be combined
+A2_coords = np.append(A2_coords_inside, A2_coords_outside, axis=0)
+A2_vel = np.append(A2_vel1[mask1], A2_vel2[mask2], axis=0)
+A1_mat = np.append(A1_mat1[mask1], A1_mat2[mask2], axis=0)
+A1_m = np.append(A1_m1[mask1], A1_m2[mask2], axis=0)
+A1_rho = np.append(A1_rho1[mask1], A1_rho2[mask2], axis=0)
+A1_u = np.append(A1_u1[mask1], A1_u2[mask2], axis=0)
+A1_h = np.append(A1_h1[mask1], A1_h2[mask2], axis=0)
+numPart = np.size(A1_m)
+A1_ids = np.linspace(1, numPart, numPart)
+
+# Finally add the velocity perturbation
+vel_perturb_factor = 0.01 * (v1 - v2)
+A2_vel[:, 1] = vel_perturb_factor * np.sin(
+    2 * np.pi * A2_coords[:, 0] / (0.125 * boxsize_l)
+)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [boxsize_l, new_box_y, boxsize_depth]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/plotModeGrowth.py b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/plotModeGrowth.py
new file mode 100644
index 0000000000000000000000000000000000000000..d70e133740dab263b5eec10e5ce26987d668aac2
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/plotModeGrowth.py
@@ -0,0 +1,131 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot Mode growth of the 3D Kelvin--Helmholtz instability.
+This is based on the quantity calculated in Eqns. 10--13 of McNally et al. 2012
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def calculate_mode_growth(snap, vy_init_amp, wavelength):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A2_pos = f["/PartType0/Coordinates"][:, :] / boxsize_l
+        A2_vel = f["/PartType0/Velocities"][:, :] / (vy_init_amp * boxsize_l)
+        A1_h = f["/PartType0/SmoothingLengths"][:] / boxsize_l
+
+    # Adjust positions
+    A2_pos[:, 0] += 0.5
+    A2_pos[:, 1] += 0.5
+
+    # Masks to select the upper and lower halfs of the simulations
+    mask_up = A2_pos[:, 1] >= 0.5
+    mask_down = A2_pos[:, 1] < 0.5
+
+    # McNally et al. 2012 Eqn. 10
+    s = np.empty(len(A1_h))
+    s[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    s[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 11
+    c = np.empty(len(A1_h))
+    c[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    c[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 12
+    d = np.empty(len(A1_h))
+    d[mask_down] = A1_h[mask_down] ** 3 * np.exp(
+        -2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength
+    )
+    d[mask_up] = A1_h[mask_up] ** 3 * np.exp(
+        -2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength
+    )
+
+    # McNally et al. 2012 Eqn. 13
+    M = 2 * np.sqrt((np.sum(s) / np.sum(d)) ** 2 + (np.sum(c) / np.sum(d)) ** 2)
+
+    return M
+
+
+if __name__ == "__main__":
+
+    # Simulation paramerters for nomralisation of mode growth
+    vy_init_amp = (
+        0.01
+    )  # Initial amplitude of y velocity perturbation in units of the boxsize per second
+    wavelength = 0.125  # wavelength of initial perturbation in units of the boxsize
+
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    ax_len = 5
+    fig = plt.figure(figsize=(9, 6))
+    gs = mpl.gridspec.GridSpec(nrows=40, ncols=60)
+    ax = plt.subplot(gs[:, :])
+
+    # Snapshots and corresponding times
+    snaps = np.arange(16)
+    times = 0.1 * snaps
+
+    # Calculate mode mode growth
+    mode_growth = np.empty(len(snaps))
+    for i, snap in enumerate(snaps):
+        M = calculate_mode_growth(snap, vy_init_amp, wavelength)
+        mode_growth[i] = M
+
+    # Plot
+    ax.plot(times, np.log10(mode_growth), linewidth=1.5)
+
+    ax.set_ylabel(r"$\log( \, M \; / \; M^{}_0 \, )$", fontsize=18)
+    ax.set_xlabel(r"$t \; / \; \tau^{}_{\rm KH}$", fontsize=18)
+    ax.minorticks_on()
+    ax.tick_params(which="major", direction="in")
+    ax.tick_params(which="minor", direction="in")
+    ax.tick_params(labelsize=14)
+
+    plt.savefig("mode_growth.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/plotSnapshots.py b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..a9b8ab09cd402805d21dfe4b2c6631c476a8e20d
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/plotSnapshots.py
@@ -0,0 +1,163 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Kelvin--Helmholtz instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len = 5
+    ax_gap_len = n_gs_ax_gap * ax_len / n_gs_ax
+    cbar_gap_len = n_gs_cbar_gap * ax_len / n_gs_ax
+    cbar_len = n_gs_cbar * ax_len / n_gs_ax
+
+    fig = plt.figure(
+        figsize=(3 * ax_len + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax, ncols=3 * n_gs_ax + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax, :n_gs_ax])
+    ax_1 = plt.subplot(gs[:n_gs_ax, n_gs_ax + n_gs_ax_gap : 2 * n_gs_ax + n_gs_ax_gap])
+    ax_2 = plt.subplot(
+        gs[:n_gs_ax, 2 * n_gs_ax + 2 * n_gs_ax_gap : 3 * n_gs_ax + 2 * n_gs_ax_gap]
+    )
+    cax = plt.subplot(
+        gs[
+            :n_gs_ax,
+            3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+
+    return axs, cax
+
+
+def plot_kh(ax, snap, cmap, norm):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A1_x = f["/PartType0/Coordinates"][:, 0]
+        A1_y = f["/PartType0/Coordinates"][:, 1]
+        A1_z = f["/PartType0/Coordinates"][:, 2]
+        A1_rho = f["/PartType0/Densities"][:]
+        A1_m = f["/PartType0/Masses"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.2 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_l ** 2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice,
+        A1_y_slice,
+        c=A1_rho_slice,
+        norm=norm,
+        cmap=cmap,
+        s=A1_size,
+        edgecolors="none",
+    )
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0, boxsize_l))
+    ax.set_ylim((0, boxsize_l))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap = plt.get_cmap("Spectral_r")
+    vmin, vmax = 0.95, 2.05
+    norm = mpl.colors.LogNorm(vmin=0.8, vmax=12)
+
+    # Generate axes
+    axs, cax = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [10, 12, 14]
+    times = ["1.0", "1.2", "1.4"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, cmap, norm)
+        ax.text(
+            0.5,
+            -0.1,
+            r"$t =\;$" + time + r"$\, \tau^{}_{\rm KH}$",
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
+    cbar = plt.colorbar(sm, cax)
+    cbar.ax.tick_params(labelsize=14)
+    cbar.set_label("Density", rotation=90, labelpad=8, fontsize=18)
+
+    plt.savefig("kelvin_helmholtz.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/run.sh b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5fe5ca13702e5b91eb89ab4f791230242360b6da
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_LargeDensityContrast_3D/run.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e kelvin_helmholtz.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Kelvin--Helmholtz test..."
+    python3 makeIC.py
+fi
+
+# Run SWIFT
+../../../swift --hydro --threads=4 kelvin_helmholtz.yml 2>&1 | tee output_kelvin_helmholtz.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
+python3 ./plotModeGrowth.py
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/README.md b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..33e912dd64182eb664bd15aa755351c1a0690898
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/README.md
@@ -0,0 +1,20 @@
+Kelvin--Helmholtz Instabilty (ideal gas, smooth interfaces, 3D)
+--------------
+
+This is a 3D version of the Kelvin--Helmholtz instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025), section 4.3.1.
+
+This test uses particles set up with equal initial spacing and smoothed density
+and velocity discontinuities.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/kelvin_helmholtz.yml b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/kelvin_helmholtz.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7a66931e4f2d17c813449c5eadc8c667e377c626
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/kelvin_helmholtz.yml
@@ -0,0 +1,41 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1   # Grams
+  UnitLength_in_cgs:   1   # Centimeters
+  UnitVelocity_in_cgs: 1   # Centimeters per second
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.0    # The starting time of the simulation (in internal units).
+  time_end:   2.625  # The end time of the simulation (in internal units). Corresponding to 2 \tau_{KH}.
+  dt_min:     1e-9   # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            kelvin_helmholtz  # Common part of the name of output files
+  time_first:          0.                # Time of the first output (in internal units)
+  delta_time:          0.106             # Time difference between consecutive outputs (in internal units). Corresponding to 0.1 \tau_{KH}.
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          0.105 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./kelvin_helmholtz.hdf5     # The file to read
+  periodic:   1
+  
+Scheduler:
+    max_top_level_cells:      40         # Maximal number of top-level cells in any dimension.
+    
+# Parameters related to the equation of state
+EoS:
+    planetary_use_idg_def:    1     # Default ideal gas, material ID 0
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/makeIC.py b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..51033f364d2fee9aeb94cb1aa7ae07b48c50108e
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/makeIC.py
@@ -0,0 +1,142 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+
+# Generates a swift IC file for the Kelvin-Helmholtz test in a periodic box
+
+# Parameters
+N_l = 128  # Particles along one edge in the low-density region
+N_depth = 18  # Particles in z direction in low-density region
+gamma = 5.0 / 3.0  # Gas adiabatic index
+P1 = 2.5  # Central region pressure
+P2 = 2.5  # Outskirts pressure
+rho1 = 2  # Central region density
+rho2 = 1  # Outskirts density
+v1 = 0.5  # Central region velocity
+v2 = -0.5  # Outskirts velocity
+boxsize_l = 1  # size of simulation box in x and y dimension
+boxsize_depth = boxsize_l * N_depth / N_l  # size of simulation box in z dimension
+fileOutputName = "kelvin_helmholtz.hdf5"
+
+# Parameters for smoothing of interfaces
+vm = (v2 - v1) / 2
+rhom = (rho2 - rho1) / 2
+delta = 0.025
+# ---------------------------------------------------
+
+numPart = N_l * N_l * N_depth
+
+# Now construct two lattices of particles in the two regions
+A2_coords = np.empty((numPart, 3))
+A2_vel = np.zeros((numPart, 3))
+
+A1_mat = np.zeros(numPart)
+A1_m = np.empty(numPart)
+A1_rho = np.empty(numPart)
+A1_u = np.empty(numPart)
+A1_h = np.ones(numPart) * boxsize_l / N_l
+A1_ids = np.linspace(1, numPart, numPart)
+
+# Set up particles
+for i in range(N_depth):
+    for j in range(N_l):
+        for k in range(N_l):
+            index = i * N_l ** 2 + j * N_l + k
+
+            x = (j / float(N_l) + 1.0 / (2.0 * N_l)) * boxsize_l
+            y = (k / float(N_l) + 1.0 / (2.0 * N_l)) * boxsize_l
+            z = (i / float(N_depth) + 1.0 / (2.0 * N_depth)) * boxsize_depth
+
+            A2_coords[index, 0] = x
+            A2_coords[index, 1] = y
+            A2_coords[index, 2] = z
+
+            if 0.0 <= y <= 0.25:
+                A1_rho[index] = rho2 - rhom * np.exp((y - 0.25) / delta)
+                A2_vel[index, 0] = v2 - vm * np.exp((y - 0.25) / delta)
+                A1_m[index] = A1_rho[index] / N_l ** 3
+                A1_u[index] = P2 / (A1_rho[index] * (gamma - 1.0))
+
+            elif 0.25 <= y <= 0.5:
+                A1_rho[index] = rho1 + rhom * np.exp((0.25 - y) / delta)
+                A2_vel[index, 0] = v1 + vm * np.exp((0.25 - y) / delta)
+                A1_m[index] = A1_rho[index] / N_l ** 3
+                A1_u[index] = P1 / (A1_rho[index] * (gamma - 1.0))
+
+            elif 0.5 <= y <= 0.75:
+                A1_rho[index] = rho1 + rhom * np.exp((y - 0.75) / delta)
+                A2_vel[index, 0] = v1 + vm * np.exp((y - 0.75) / delta)
+                A1_m[index] = A1_rho[index] / N_l ** 3
+                A1_u[index] = P1 / (A1_rho[index] * (gamma - 1.0))
+
+            elif 0.75 <= y <= 1:
+                A1_rho[index] = rho2 - rhom * np.exp((0.75 - y) / delta)
+                A2_vel[index, 0] = v2 - vm * np.exp((0.75 - y) / delta)
+                A1_m[index] = A1_rho[index] / N_l ** 3
+                A1_u[index] = P2 / (A1_rho[index] * (gamma - 1.0))
+
+# Finally add the velocity perturbation
+vel_perturb_factor = 0.01 * (v1 - v2)
+A2_vel[:, 1] = vel_perturb_factor * np.sin(
+    2 * np.pi * A2_coords[:, 0] / (0.5 * boxsize_l)
+)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [boxsize_l, boxsize_l, boxsize_depth]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/plotModeGrowth.py b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/plotModeGrowth.py
new file mode 100644
index 0000000000000000000000000000000000000000..31a4b450c6e3436b75889ae5af7c30f8c17efbbb
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/plotModeGrowth.py
@@ -0,0 +1,131 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot Mode growth of the 3D Kelvin--Helmholtz instability.
+This is based on the quantity calculated in Eqns. 10--13 of McNally et al. 2012
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def calculate_mode_growth(snap, vy_init_amp, wavelength):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A2_pos = f["/PartType0/Coordinates"][:, :] / boxsize_l
+        A2_vel = f["/PartType0/Velocities"][:, :] / (vy_init_amp * boxsize_l)
+        A1_h = f["/PartType0/SmoothingLengths"][:] / boxsize_l
+
+    # Adjust positions
+    A2_pos[:, 0] += 0.5
+    A2_pos[:, 1] += 0.5
+
+    # Masks to select the upper and lower halfs of the simulations
+    mask_up = A2_pos[:, 1] >= 0.5
+    mask_down = A2_pos[:, 1] < 0.5
+
+    # McNally et al. 2012 Eqn. 10
+    s = np.empty(len(A1_h))
+    s[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    s[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 11
+    c = np.empty(len(A1_h))
+    c[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    c[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 12
+    d = np.empty(len(A1_h))
+    d[mask_down] = A1_h[mask_down] ** 3 * np.exp(
+        -2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength
+    )
+    d[mask_up] = A1_h[mask_up] ** 3 * np.exp(
+        -2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength
+    )
+
+    # McNally et al. 2012 Eqn. 13
+    M = 2 * np.sqrt((np.sum(s) / np.sum(d)) ** 2 + (np.sum(c) / np.sum(d)) ** 2)
+
+    return M
+
+
+if __name__ == "__main__":
+
+    # Simulation paramerters for nomralisation of mode growth
+    vy_init_amp = (
+        0.01
+    )  # Initial amplitude of y velocity perturbation in units of the boxsize per second
+    wavelength = 0.5  # wavelength of initial perturbation in units of the boxsize
+
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    ax_len = 5
+    fig = plt.figure(figsize=(9, 6))
+    gs = mpl.gridspec.GridSpec(nrows=40, ncols=60)
+    ax = plt.subplot(gs[:, :])
+
+    # Snapshots and corresponding times
+    snaps = np.arange(26)
+    times = 0.1 * snaps
+
+    # Calculate mode mode growth
+    mode_growth = np.empty(len(snaps))
+    for i, snap in enumerate(snaps):
+        M = calculate_mode_growth(snap, vy_init_amp, wavelength)
+        mode_growth[i] = M
+
+    # Plot
+    ax.plot(times, np.log10(mode_growth), linewidth=1.5)
+
+    ax.set_ylabel(r"$\log( \, M \; / \; M^{}_0 \, )$", fontsize=18)
+    ax.set_xlabel(r"$t \; / \; \tau^{}_{\rm KH}$", fontsize=18)
+    ax.minorticks_on()
+    ax.tick_params(which="major", direction="in")
+    ax.tick_params(which="minor", direction="in")
+    ax.tick_params(labelsize=14)
+
+    plt.savefig("mode_growth.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/plotSnapshots.py b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..38fe76a411b99bcfb1197ea4c81c54a2b958d91f
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/plotSnapshots.py
@@ -0,0 +1,163 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Kelvin--Helmholtz instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len = 5
+    ax_gap_len = n_gs_ax_gap * ax_len / n_gs_ax
+    cbar_gap_len = n_gs_cbar_gap * ax_len / n_gs_ax
+    cbar_len = n_gs_cbar * ax_len / n_gs_ax
+
+    fig = plt.figure(
+        figsize=(3 * ax_len + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax, ncols=3 * n_gs_ax + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax, :n_gs_ax])
+    ax_1 = plt.subplot(gs[:n_gs_ax, n_gs_ax + n_gs_ax_gap : 2 * n_gs_ax + n_gs_ax_gap])
+    ax_2 = plt.subplot(
+        gs[:n_gs_ax, 2 * n_gs_ax + 2 * n_gs_ax_gap : 3 * n_gs_ax + 2 * n_gs_ax_gap]
+    )
+    cax = plt.subplot(
+        gs[
+            :n_gs_ax,
+            3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+
+    return axs, cax
+
+
+def plot_kh(ax, snap, cmap, norm):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A1_x = f["/PartType0/Coordinates"][:, 0]
+        A1_y = f["/PartType0/Coordinates"][:, 1]
+        A1_z = f["/PartType0/Coordinates"][:, 2]
+        A1_rho = f["/PartType0/Densities"][:]
+        A1_m = f["/PartType0/Masses"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.2 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_l ** 2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice,
+        A1_y_slice,
+        c=A1_rho_slice,
+        norm=norm,
+        cmap=cmap,
+        s=A1_size,
+        edgecolors="none",
+    )
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0, boxsize_l))
+    ax.set_ylim((0, boxsize_l))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap = plt.get_cmap("Spectral_r")
+    vmin, vmax = 0.95, 2.05
+    norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
+
+    # Generate axes
+    axs, cax = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [15, 20, 25]
+    times = ["1.5", "2.0", "2.5"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, cmap, norm)
+        ax.text(
+            0.5,
+            -0.1,
+            r"$t =\;$" + time + r"$\, \tau^{}_{\rm KH}$",
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
+    cbar = plt.colorbar(sm, cax)
+    cbar.ax.tick_params(labelsize=14)
+    cbar.set_label("Density", rotation=90, labelpad=8, fontsize=18)
+
+    plt.savefig("kelvin_helmholtz.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/run.sh b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5fe5ca13702e5b91eb89ab4f791230242360b6da
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_IdealGas_Smooth_3D/run.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e kelvin_helmholtz.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Kelvin--Helmholtz test..."
+    python3 makeIC.py
+fi
+
+# Run SWIFT
+../../../swift --hydro --threads=4 kelvin_helmholtz.yml 2>&1 | tee output_kelvin_helmholtz.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
+python3 ./plotModeGrowth.py
diff --git a/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/README.md b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..da6327298a0888d7d48395643b6c04edc0e96ed0
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/README.md
@@ -0,0 +1,21 @@
+Kelvin--Helmholtz Instabilty (Jupiter-like, equal mass, 3D)
+--------------
+
+This is a 3D version of the Kelvin--Helmholtz instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025b).
+
+This test uses particles of equal mass and has sharp density and velocity
+discontinuities. Equations of state and conditions are representative of those
+within Jupiter's interior.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/kelvin_helmholtz.yml b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/kelvin_helmholtz.yml
new file mode 100644
index 0000000000000000000000000000000000000000..df1ed8670f5edad6e2fb10dd5a32d59e143dbfa2
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/kelvin_helmholtz.yml
@@ -0,0 +1,46 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:        5.9724e27   # Grams
+  UnitLength_in_cgs:      6.371e8     # Centimeters
+  UnitVelocity_in_cgs:    6.371e8     # Centimeters per second
+  UnitCurrent_in_cgs:     1           # Amperes
+  UnitTemp_in_cgs:        1           # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   10980 # The end time of the simulation (in internal units). Corresponding to 2 \tau_{KH}.
+  dt_min:     1e-9  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            kelvin_helmholtz # Common part of the name of output files
+  time_first:          0.               # Time of the first output (in internal units)
+  delta_time:          549              # Time difference between consecutive outputs (in internal units). Corresponding to 0.1 \tau_{KH}.
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          549 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./kelvin_helmholtz.hdf5      # The file to read
+  periodic:   1
+
+Scheduler:
+    max_top_level_cells:     40
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_CD21_HHe:  1     # Hydrogen--helium (Chabrier and Debras 2021), material ID 307
+    planetary_use_AQUA:      1     # AQUA (Haldemann et al. 2020), material ID 304
+    # Tablulated EoS file paths.
+    planetary_CD21_HHe_table_file: ../EoSTables/CD21_HHe.txt
+    planetary_AQUA_table_file:     ../EoSTables/AQUA_H20.txt
diff --git a/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/makeIC.py b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..85b2c24f5364ff8d7137000fa466c4c99179741b
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/makeIC.py
@@ -0,0 +1,199 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+
+# Generates a swift IC file for the Kelvin-Helmholtz test in a periodic box
+
+# Constants
+R_earth = 6371000  # Earth radius
+R_jupiter = 11.2089 * R_earth  # Jupiter radius
+
+# Parameters
+N2_l = 128  # Particles along one edge in the low-density region
+N2_depth = 18  # Particles in z direction in low-density region
+matID1 = 304  # Central region material ID: AQUA
+matID2 = 307  # Outskirts material ID: CD21 H--He
+P1 = 3.2e12  # Central region pressure
+P2 = 3.2e12  # Outskirts pressure
+u1 = 283591514  # Central region specific internal energy
+u2 = 804943158  # Outskirts specific internal energy
+rho1_approx = 9000  # Central region density. Readjusted later
+rho2 = 3500  # Outskirts density
+boxsize_l = R_jupiter  # size of simulation box in x and y dimension
+v1 = boxsize_l / 10000  # Central region velocity
+v2 = -boxsize_l / 10000  # Outskirts velocity
+boxsize_depth = boxsize_l * N2_depth / N2_l  # size of simulation box in z dimension
+mass = rho2 * (boxsize_l * boxsize_l * boxsize_depth) / (N2_l * N2_l * N2_depth)
+fileOutputName = "kelvin_helmholtz.hdf5"
+# ---------------------------------------------------
+
+# Start by calculating N1_l and rho1
+numPart2 = N2_l * N2_l * N2_depth
+numPart1_approx = int(numPart2 / rho2 * rho1_approx)
+
+# Consider numPart1 = N1_l * N1_l * N1_depth
+# Substituting boxsize_depth / boxsize_l = N1_depth / N1_l gives,
+# numPart1 = N1_l * N1_l * (boxsize_depth / boxsize_l) * N1_l, which ranges to:
+N1_l = int(np.cbrt(numPart1_approx * boxsize_l / boxsize_depth))
+# Make sure this is a multiple of 4 since this is the number of KH vortices
+N1_l -= N1_l % 4
+
+N1_depth = int(boxsize_depth * N1_l / boxsize_l)
+numPart1 = int(N1_l * N1_l * N1_depth)
+
+# The density of the central region can then be calculated
+rho1 = mass * (N1_l * N1_l * N1_depth) / (boxsize_l * boxsize_l * boxsize_depth)
+
+# Now construct two lattices of particles in the two regions
+A2_coords1 = np.empty((numPart1, 3))
+A2_coords2 = np.empty((numPart2, 3))
+A2_vel1 = np.zeros((numPart1, 3))
+A2_vel2 = np.zeros((numPart2, 3))
+A2_vel1[:, 0] = v1
+A2_vel2[:, 0] = v2
+
+A1_mat1 = np.full(numPart1, matID1)
+A1_mat2 = np.full(numPart2, matID2)
+A1_m1 = np.full(numPart1, mass)
+A1_m2 = np.full(numPart2, mass)
+A1_rho1 = np.full(numPart1, rho1)
+A1_rho2 = np.full(numPart2, rho2)
+A1_u1 = np.full(numPart1, u1)
+A1_u2 = np.full(numPart2, u2)
+A1_h1 = np.full(numPart1, boxsize_l / N1_l)
+A1_h2 = np.full(numPart2, boxsize_l / N2_l)
+
+# Particles in the central region
+for i in range(N1_depth):
+    for j in range(N1_l):
+        for k in range(N1_l):
+            index = i * N1_l ** 2 + j * N1_l + k
+            A2_coords1[index, 0] = (j / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 1] = (k / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_l
+            A2_coords1[index, 2] = (
+                i / float(N1_depth) + 1.0 / (2.0 * N1_depth)
+            ) * boxsize_depth
+
+# Particles in the outskirts
+for i in range(N2_depth):
+    for j in range(N2_l):
+        for k in range(N2_l):
+            index = i * N2_l ** 2 + j * N2_l + k
+            A2_coords2[index, 0] = (j / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 1] = (k / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_l
+            A2_coords2[index, 2] = (
+                i / float(N2_depth) + 1.0 / (2.0 * N2_depth)
+            ) * boxsize_depth
+
+
+# Masks for the particles to be selected for the outer and inner regions
+mask1 = abs(A2_coords1[:, 1] - 0.5 * boxsize_l) < 0.25 * boxsize_l
+mask2 = abs(A2_coords2[:, 1] - 0.5 * boxsize_l) > 0.25 * boxsize_l
+
+# The positions of the particles are now selected
+# and the placement of the lattices are adjusted to give appropriate interfaces
+A2_coords_inside = A2_coords1[mask1, :]
+A2_coords_outside = A2_coords2[mask2, :]
+
+# Calculate the separation of particles across the density discontinuity
+pcl_separation_1 = np.cbrt(mass / rho1)
+pcl_separation_2 = np.cbrt(mass / rho2)
+boundary_separation = 0.5 * (pcl_separation_1 + pcl_separation_2)
+
+# Shift all the "inside" particles to get boundary_separation across the bottom interface
+min_y_inside = np.min(A2_coords_inside[:, 1])
+max_y_outside_bot = np.max(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l < -0.25 * boxsize_l, 1]
+)
+shift_distance_bot = boundary_separation - (min_y_inside - max_y_outside_bot)
+A2_coords_inside[:, 1] += shift_distance_bot
+
+# Shift the top section of the "outside" particles to get boundary_separation across the top interface
+max_y_inside = np.max(A2_coords_inside[:, 1])
+min_y_outside_top = np.min(
+    A2_coords_outside[A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1]
+)
+shift_distance_top = boundary_separation - (min_y_outside_top - max_y_inside)
+A2_coords_outside[
+    A2_coords_outside[:, 1] - 0.5 * boxsize_l > 0.25 * boxsize_l, 1
+] += shift_distance_top
+
+# Adjust box size in y direction based on the shifting of the lattices.
+new_box_y = boxsize_l + shift_distance_top
+
+# Now the two lattices can be combined
+A2_coords = np.append(A2_coords_inside, A2_coords_outside, axis=0)
+A2_vel = np.append(A2_vel1[mask1], A2_vel2[mask2], axis=0)
+A1_mat = np.append(A1_mat1[mask1], A1_mat2[mask2], axis=0)
+A1_m = np.append(A1_m1[mask1], A1_m2[mask2], axis=0)
+A1_rho = np.append(A1_rho1[mask1], A1_rho2[mask2], axis=0)
+A1_u = np.append(A1_u1[mask1], A1_u2[mask2], axis=0)
+A1_h = np.append(A1_h1[mask1], A1_h2[mask2], axis=0)
+numPart = np.size(A1_m)
+A1_ids = np.linspace(1, numPart, numPart)
+
+# Finally add the velocity perturbation
+vel_perturb_factor = 0.01 * (v1 - v2)
+A2_vel[:, 1] = vel_perturb_factor * np.sin(
+    2 * np.pi * A2_coords[:, 0] / (0.5 * boxsize_l)
+)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [boxsize_l, new_box_y, boxsize_depth]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 100.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1000.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/plotModeGrowth.py b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/plotModeGrowth.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4a1408aeaddd8cb6c141daf264126b22da4bec4
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/plotModeGrowth.py
@@ -0,0 +1,131 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot Mode growth of the 3D Kelvin--Helmholtz instability.
+This is based on the quantity calculated in Eqns. 10--13 of McNally et al. 2012
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def calculate_mode_growth(snap, vy_init_amp, wavelength):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A2_pos = f["/PartType0/Coordinates"][:, :] / boxsize_l
+        A2_vel = f["/PartType0/Velocities"][:, :] / (vy_init_amp * boxsize_l)
+        A1_h = f["/PartType0/SmoothingLengths"][:] / boxsize_l
+
+    # Adjust positions
+    A2_pos[:, 0] += 0.5
+    A2_pos[:, 1] += 0.5
+
+    # Masks to select the upper and lower halfs of the simulations
+    mask_up = A2_pos[:, 1] >= 0.5
+    mask_down = A2_pos[:, 1] < 0.5
+
+    # McNally et al. 2012 Eqn. 10
+    s = np.empty(len(A1_h))
+    s[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    s[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.sin(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 11
+    c = np.empty(len(A1_h))
+    c[mask_down] = (
+        A2_vel[mask_down, 1]
+        * A1_h[mask_down] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_down, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength)
+    )
+    c[mask_up] = (
+        A2_vel[mask_up, 1]
+        * A1_h[mask_up] ** 3
+        * np.cos(2 * np.pi * A2_pos[mask_up, 0] / wavelength)
+        * np.exp(-2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength)
+    )
+
+    # McNally et al. 2012 Eqn. 12
+    d = np.empty(len(A1_h))
+    d[mask_down] = A1_h[mask_down] ** 3 * np.exp(
+        -2 * np.pi * np.abs(A2_pos[mask_down, 1] - 0.25) / wavelength
+    )
+    d[mask_up] = A1_h[mask_up] ** 3 * np.exp(
+        -2 * np.pi * np.abs((1 - A2_pos[mask_up, 1]) - 0.25) / wavelength
+    )
+
+    # McNally et al. 2012 Eqn. 13
+    M = 2 * np.sqrt((np.sum(s) / np.sum(d)) ** 2 + (np.sum(c) / np.sum(d)) ** 2)
+
+    return M
+
+
+if __name__ == "__main__":
+
+    # Simulation paramerters for nomralisation of mode growth
+    vy_init_amp = (
+        2e-6
+    )  # Initial amplitude of y velocity perturbation in units of the boxsize per second
+    wavelength = 0.5  # wavelength of initial perturbation in units of the boxsize
+
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    ax_len = 5
+    fig = plt.figure(figsize=(9, 6))
+    gs = mpl.gridspec.GridSpec(nrows=40, ncols=60)
+    ax = plt.subplot(gs[:, :])
+
+    # Snapshots and corresponding times
+    snaps = np.arange(21)
+    times = 0.1 * snaps
+
+    # Calculate mode mode growth
+    mode_growth = np.empty(len(snaps))
+    for i, snap in enumerate(snaps):
+        M = calculate_mode_growth(snap, vy_init_amp, wavelength)
+        mode_growth[i] = M
+
+    # Plot
+    ax.plot(times, np.log10(mode_growth), linewidth=1.5)
+
+    ax.set_ylabel(r"$\log( \, M \; / \; M^{}_0 \, )$", fontsize=18)
+    ax.set_xlabel(r"$t \; / \; \tau^{}_{\rm KH}$", fontsize=18)
+    ax.minorticks_on()
+    ax.tick_params(which="major", direction="in")
+    ax.tick_params(which="minor", direction="in")
+    ax.tick_params(labelsize=14)
+
+    plt.savefig("mode_growth.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/plotSnapshots.py b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..47414d9f28c69a486de35b372df315f76205cc82
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/plotSnapshots.py
@@ -0,0 +1,210 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Kelvin--Helmholtz instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len = 5
+    ax_gap_len = n_gs_ax_gap * ax_len / n_gs_ax
+    cbar_gap_len = n_gs_cbar_gap * ax_len / n_gs_ax
+    cbar_len = n_gs_cbar * ax_len / n_gs_ax
+
+    fig = plt.figure(
+        figsize=(3 * ax_len + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax, ncols=3 * n_gs_ax + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax, :n_gs_ax])
+    ax_1 = plt.subplot(gs[:n_gs_ax, n_gs_ax + n_gs_ax_gap : 2 * n_gs_ax + n_gs_ax_gap])
+    ax_2 = plt.subplot(
+        gs[:n_gs_ax, 2 * n_gs_ax + 2 * n_gs_ax_gap : 3 * n_gs_ax + 2 * n_gs_ax_gap]
+    )
+
+    cax_0 = plt.subplot(
+        gs[
+            : int(0.5 * n_gs_ax) - 1,
+            3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+    cax_1 = plt.subplot(
+        gs[
+            int(0.5 * n_gs_ax) + 1 :,
+            3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+    caxs = [cax_0, cax_1]
+
+    return axs, caxs
+
+
+def plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2):
+
+    # Load data
+    snap_file = "kelvin_helmholtz_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        # Units from file metadata to SI
+        m = float(f["Units"].attrs["Unit mass in cgs (U_M)"][0]) * 1e-3
+        l = float(f["Units"].attrs["Unit length in cgs (U_L)"][0]) * 1e-2
+
+        boxsize_l = f["Header"].attrs["BoxSize"][0] * l
+        A1_x = f["/PartType0/Coordinates"][:, 0] * l
+        A1_y = f["/PartType0/Coordinates"][:, 1] * l
+        A1_z = f["/PartType0/Coordinates"][:, 2] * l
+        A1_rho = f["/PartType0/Densities"][:] * (m / l ** 3)
+        A1_m = f["/PartType0/Masses"][:] * m
+        A1_mat_id = f["/PartType0/MaterialIDs"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+    A1_mat_id = A1_mat_id[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.2 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+    A1_mat_id_slice = A1_mat_id[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_l ** 2
+
+    mask_mat1 = A1_mat_id_slice == mat_id1
+    mask_mat2 = A1_mat_id_slice == mat_id2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat1],
+        A1_y_slice[mask_mat1],
+        c=A1_rho_slice[mask_mat1],
+        norm=norm1,
+        cmap=cmap1,
+        s=A1_size[mask_mat1],
+        edgecolors="none",
+    )
+
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat2],
+        A1_y_slice[mask_mat2],
+        c=A1_rho_slice[mask_mat2],
+        norm=norm2,
+        cmap=cmap2,
+        s=A1_size[mask_mat2],
+        edgecolors="none",
+    )
+
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0, boxsize_l))
+    ax.set_ylim((0, boxsize_l))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap1 = plt.get_cmap("winter_r")
+    mat_id1 = 304
+    rho_min1 = 6800
+    rho_max1 = 8700
+    norm1 = mpl.colors.Normalize(vmin=rho_min1, vmax=rho_max1)
+
+    cmap2 = plt.get_cmap("autumn")
+    mat_id2 = 307
+    rho_min2 = 3450
+    rho_max2 = 4050
+    norm2 = mpl.colors.Normalize(vmin=rho_min2, vmax=rho_max2)
+
+    # Generate axes
+    axs, caxs = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [10, 15, 20]
+    times = ["1.0", "1.5", "2.0"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2)
+        ax.text(
+            0.5,
+            -0.1,
+            r"$t =\;$" + time + r"$\, \tau^{}_{\rm KH}$",
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm1 = plt.cm.ScalarMappable(cmap=cmap1, norm=norm1)
+    cbar1 = plt.colorbar(sm1, caxs[0])
+    cbar1.ax.tick_params(labelsize=14)
+    cbar1.set_label(r"Ice density (kg/m$^3$)", rotation=90, labelpad=8, fontsize=12)
+
+    sm2 = plt.cm.ScalarMappable(cmap=cmap2, norm=norm2)
+    cbar2 = plt.colorbar(sm2, caxs[1])
+    cbar2.ax.tick_params(labelsize=14)
+    cbar2.set_label(r"H--He density (kg/m$^3$)", rotation=90, labelpad=8, fontsize=12)
+
+    plt.savefig("kelvin_helmholtz.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/run.sh b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2048398c362a6dfab1b18bd0c220977512edb8ae
--- /dev/null
+++ b/examples/Planetary/KelvinHelmholtz_JupiterLike_3D/run.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e kelvin_helmholtz.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Kelvin--Helmholtz test..."
+    python3 makeIC.py
+fi
+
+# Download the equation of state tables if not already present
+if [ ! -e ../EoSTables/CD21_HHe.txt ]
+then
+    cd ../EoSTables
+    ./get_eos_tables.sh
+    cd -
+fi
+
+# Run SWIFT
+../../../swift --hydro --threads=4 kelvin_helmholtz.yml 2>&1 | tee output_kelvin_helmholtz.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
+python3 ./plotModeGrowth.py
diff --git a/examples/Planetary/RayleighTaylor_EarthLike_3D/README.md b/examples/Planetary/RayleighTaylor_EarthLike_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..b8d3896d0f7163e368070e9fc177d03e82a254c6
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_EarthLike_3D/README.md
@@ -0,0 +1,20 @@
+Rayleigh--Taylor Instabilty (Earth-like, equal mass, 3D)
+--------------
+
+This is a 3D version of the Rayleigh--Taylor instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025), section 4.6.
+
+This test uses particles of equal mass and has a sharp density. Equations
+of state and conditions are representative of those within Earth's interior.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2 --with-ext-potential=constant --enable-boundary-particles=20993`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2 --with-ext-potential=constant --enable-boundary-particles=20993`
diff --git a/examples/Planetary/RayleighTaylor_EarthLike_3D/makeIC.py b/examples/Planetary/RayleighTaylor_EarthLike_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..aaee262df1c538d0cb04d9a92b9321905e4dbbe3
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_EarthLike_3D/makeIC.py
@@ -0,0 +1,236 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+import woma
+
+# Load EoS tables
+woma.load_eos_tables(["ANEOS_forsterite", "ANEOS_Fe85Si15"])
+
+# Generates a swift IC file for the Rayleigh-Taylor instability test
+
+# Constants
+R_earth = 6371000  # Earth radius
+
+# Parameters
+N2_l = 64  # Number of particles along one edge in lower region
+N2_depth = 18  # Number of particles along in z dimension in lower region
+matID1 = 402  # Upper region material ID: ANEOS Fe85Si15
+matID2 = 400  # Lower region material ID: ANEOS forsterite
+rho1_approx = 10000  # Approximate density of upper region. To be recalculated
+rho2 = 5000  # Density of lower region
+g = -9.91  # Constant gravitational acceleration
+P0 = 1.2e11  # Pressure at interface
+boxsize_factor = 0.1 * R_earth
+dv = 0.00025 * boxsize_factor  # Size of velocity perturbation
+boxsize_xy = [
+    0.5 * boxsize_factor,
+    1.0 * boxsize_factor,
+]  # Size of the box in x and y dimensions
+boxsize_depth = boxsize_xy[0] * N2_depth / N2_l  # Size of simulation box in z dimension
+fixed_region = [
+    0.05 * boxsize_factor,
+    0.95 * boxsize_factor,
+]  # y-range of non fixed_region particles
+perturbation_region = [
+    0.3 * boxsize_factor,
+    0.7 * boxsize_factor,
+]  # y-range for the velocity perturbation
+fileOutputName = "rayleigh_taylor.hdf5"
+# ---------------------------------------------------
+
+# Start by generating grids of particles of the two densities
+numPart2 = N2_l * N2_l * N2_depth
+numPart1 = int(numPart2 / rho2 * rho1_approx)
+N1_l = int(np.cbrt(boxsize_xy[0] * numPart1 / boxsize_depth))
+N1_l -= N1_l % 4  # Make RT symmetric across centre of both instability regions
+N1_depth = int(boxsize_depth * N1_l / boxsize_xy[0])
+numPart1 = int(N1_l * N1_l * N1_depth)
+numPart = numPart2 + numPart1
+
+# Calculate particle masses and rho1
+part_volume_l = (boxsize_xy[0] * 0.5 * boxsize_xy[1] * boxsize_depth) / numPart2
+mass = rho2 * part_volume_l
+part_volume_h = (boxsize_xy[0] * 0.5 * boxsize_xy[1] * boxsize_depth) / numPart1
+rho1 = mass / part_volume_h
+
+# Now construct two lattices of particles in the two regions
+A2_coords1 = np.empty((numPart1, 3))
+A2_coords2 = np.empty((numPart2, 3))
+A2_vel1 = np.zeros((numPart1, 3))
+A2_vel2 = np.zeros((numPart2, 3))
+A1_mat1 = np.full(numPart1, matID1)
+A1_mat2 = np.full(numPart2, matID2)
+A1_m1 = np.full(numPart1, mass)
+A1_m2 = np.full(numPart2, mass)
+A1_rho1 = np.full(numPart1, rho1)
+A1_rho2 = np.full(numPart2, rho2)
+A1_u1 = np.empty(numPart1)
+A1_u2 = np.empty(numPart2)
+A1_h1 = np.full(numPart1, boxsize_xy[0] / N1_l)
+A1_h2 = np.full(numPart2, boxsize_xy[0] / N2_l)
+A1_ids = np.zeros(numPart)
+
+# Set up boundary particle counter
+# Boundary particles are set by the N lowest ids of particles, where N is set when configuring swift
+boundary_particles = 1
+
+# Particles in the upper region
+for i in range(N1_depth):
+    for j in range(N1_l):
+        for k in range(N1_l):
+            index = i * N1_l ** 2 + j * N1_l + k
+            A2_coords1[index, 0] = (j / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_xy[
+                0
+            ]
+            A2_coords1[index, 1] = (k / float(N1_l) + 1.0 / (2.0 * N1_l)) * (
+                0.5 * boxsize_xy[1]
+            ) + 0.5 * boxsize_xy[1]
+            A2_coords1[index, 2] = (
+                i / float(N1_depth) + 1.0 / (2.0 * N1_depth)
+            ) * boxsize_depth
+            A1_rho1[index] = rho1
+
+            # If in top and bottom where particles are fixed
+            if (
+                A2_coords1[index, 1] < fixed_region[0]
+                or A2_coords1[index, 1] > fixed_region[1]
+            ):
+                A1_ids[index] = boundary_particles
+                boundary_particles += 1
+
+
+# Particles in the lower region
+for i in range(N2_depth):
+    for j in range(N2_l):
+        for k in range(N2_l):
+            index = i * N2_l ** 2 + j * N2_l + k
+            A2_coords2[index, 0] = (j / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_xy[
+                0
+            ]
+            A2_coords2[index, 1] = (k / float(N2_l) + 1.0 / (2.0 * N2_l)) * (
+                0.5 * boxsize_xy[1]
+            )
+            A2_coords2[index, 2] = (
+                i / float(N2_depth) + 1.0 / (2.0 * N2_depth)
+            ) * boxsize_depth
+            A1_rho2[index] = rho2
+
+            # If in top and bottom where particles are fixed
+            if (
+                A2_coords2[index, 1] < fixed_region[0]
+                or A2_coords2[index, 1] > fixed_region[1]
+            ):
+                A1_ids[index + numPart1] = boundary_particles
+                boundary_particles += 1
+
+print(
+    "You need to compile the code with "
+    "--enable-boundary-particles=%i" % boundary_particles
+)
+
+# Set IDs of non-boundary particles
+A1_ids[A1_ids == 0] = np.linspace(
+    boundary_particles, numPart, numPart - boundary_particles + 1
+)
+
+# The placement of the lattices are now adjusted to give appropriate interfaces
+# Calculate the separation of particles across the density discontinuity
+pcl_separation_2 = np.cbrt(mass / rho2)
+pcl_separation_1 = np.cbrt(mass / rho1)
+boundary_separation = 0.5 * (pcl_separation_2 + pcl_separation_1)
+
+# Shift top lattice
+min_y1 = min(A2_coords1[:, 1])
+max_y2 = max(A2_coords2[:, 1])
+shift_distance = boundary_separation - (min_y1 - max_y2)
+A2_coords1[:, 1] += shift_distance
+
+# Calculate internal energies
+A1_P1 = P0 + g * A1_rho1 * (A2_coords1[:, 1] - 0.5 * boxsize_xy[1])
+A1_P2 = P0 + g * A1_rho2 * (A2_coords2[:, 1] - 0.5 * boxsize_xy[1])
+A1_u1 = woma.A1_Z_rho_Y(A1_rho1, A1_P1, A1_mat1, Z_choice="u", Y_choice="P")
+A1_u2 = woma.A1_Z_rho_Y(A1_rho2, A1_P2, A1_mat2, Z_choice="u", Y_choice="P")
+
+# Now the two lattices can be combined
+A2_coords = np.append(A2_coords1, A2_coords2, axis=0)
+A2_vel = np.append(A2_vel1, A2_vel2, axis=0)
+A1_mat = np.append(A1_mat1, A1_mat2, axis=0)
+A1_m = np.append(A1_m1, A1_m2, axis=0)
+A1_rho = np.append(A1_rho1, A1_rho2, axis=0)
+A1_u = np.append(A1_u1, A1_u2, axis=0)
+A1_h = np.append(A1_h1, A1_h2, axis=0)
+
+# Add velocity perturbation
+mask_perturb = np.logical_and(
+    A2_coords[:, 1] > perturbation_region[0], A2_coords[:, 1] < perturbation_region[1]
+)
+A2_vel[mask_perturb, 1] = (
+    dv
+    * (1 + np.cos(8 * np.pi * (A2_coords[mask_perturb, 0] / (boxsize_factor) + 0.25)))
+    * (1 + np.cos(5 * np.pi * (A2_coords[mask_perturb, 1] / (boxsize_factor) - 0.5)))
+)
+
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [
+        boxsize_xy[0],
+        boxsize_xy[1] + shift_distance,
+        boxsize_depth,
+    ]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 100.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1000.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/RayleighTaylor_EarthLike_3D/plotSnapshots.py b/examples/Planetary/RayleighTaylor_EarthLike_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba9368d500be0a438ac08a12ace68d78eb85847d
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_EarthLike_3D/plotSnapshots.py
@@ -0,0 +1,219 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Rayleigh--Taylor instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax_x = 40
+    n_gs_ax_y = 80
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len_x = 5
+    ax_len_y = 10
+    ax_gap_len = n_gs_ax_gap * ax_len_x / n_gs_ax_x
+    cbar_gap_len = n_gs_cbar_gap * ax_len_x / n_gs_ax_x
+    cbar_len = n_gs_cbar * ax_len_x / n_gs_ax_x
+
+    fig = plt.figure(
+        figsize=(3 * ax_len_x + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len_y)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax_y,
+        ncols=3 * n_gs_ax_x + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar,
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax_y, :n_gs_ax_x])
+    ax_1 = plt.subplot(
+        gs[:n_gs_ax_y, n_gs_ax_x + n_gs_ax_gap : 2 * n_gs_ax_x + n_gs_ax_gap]
+    )
+    ax_2 = plt.subplot(
+        gs[
+            :n_gs_ax_y,
+            2 * n_gs_ax_x + 2 * n_gs_ax_gap : 3 * n_gs_ax_x + 2 * n_gs_ax_gap,
+        ]
+    )
+
+    cax_0 = plt.subplot(
+        gs[
+            : int(0.5 * n_gs_ax_y) - 1,
+            3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+    cax_1 = plt.subplot(
+        gs[
+            int(0.5 * n_gs_ax_y) + 1 :,
+            3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+    caxs = [cax_0, cax_1]
+
+    return axs, caxs
+
+
+def plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2):
+
+    # Load data
+    snap_file = "rayleigh_taylor_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        # Units from file metadata to SI
+        m = float(f["Units"].attrs["Unit mass in cgs (U_M)"][0]) * 1e-3
+        l = float(f["Units"].attrs["Unit length in cgs (U_L)"][0]) * 1e-2
+
+        boxsize_x = f["Header"].attrs["BoxSize"][0] * l
+        boxsize_y = f["Header"].attrs["BoxSize"][1] * l
+        A1_x = f["/PartType0/Coordinates"][:, 0] * l
+        A1_y = f["/PartType0/Coordinates"][:, 1] * l
+        A1_z = f["/PartType0/Coordinates"][:, 2] * l
+        A1_rho = f["/PartType0/Densities"][:] * (m / l ** 3)
+        A1_m = f["/PartType0/Masses"][:] * m
+        A1_mat_id = f["/PartType0/MaterialIDs"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+    A1_mat_id = A1_mat_id[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.1 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+    A1_mat_id_slice = A1_mat_id[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_x ** 2
+
+    mask_mat1 = A1_mat_id_slice == mat_id1
+    mask_mat2 = A1_mat_id_slice == mat_id2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat1],
+        A1_y_slice[mask_mat1],
+        c=A1_rho_slice[mask_mat1],
+        norm=norm1,
+        cmap=cmap1,
+        s=A1_size[mask_mat1],
+        edgecolors="none",
+    )
+
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat2],
+        A1_y_slice[mask_mat2],
+        c=A1_rho_slice[mask_mat2],
+        norm=norm2,
+        cmap=cmap2,
+        s=A1_size[mask_mat2],
+        edgecolors="none",
+    )
+
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0.0, boxsize_x))
+    ax.set_ylim((0.05 * boxsize_y, 0.95 * boxsize_y))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap1 = plt.get_cmap("YlOrRd")
+    mat_id1 = 402
+    rho_min1 = 7950
+    rho_max1 = 10050
+    norm1 = mpl.colors.Normalize(vmin=rho_min1, vmax=rho_max1)
+
+    cmap2 = plt.get_cmap("Blues_r")
+    mat_id2 = 400
+    rho_min2 = 4950
+    rho_max2 = 5550
+    norm2 = mpl.colors.Normalize(vmin=rho_min2, vmax=rho_max2)
+
+    # Generate axes
+    axs, caxs = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [8, 12, 16]
+    times = ["400", "600", "800"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2)
+        ax.text(
+            0.5,
+            -0.05,
+            r"$t =\;$" + time + r"$\,$s",
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm1 = plt.cm.ScalarMappable(cmap=cmap1, norm=norm1)
+    cbar1 = plt.colorbar(sm1, caxs[0])
+    cbar1.ax.tick_params(labelsize=14)
+    cbar1.set_label(r"Iron density (kg/m$^3$)", rotation=90, labelpad=8, fontsize=12)
+
+    sm2 = plt.cm.ScalarMappable(cmap=cmap2, norm=norm2)
+    cbar2 = plt.colorbar(sm2, caxs[1])
+    cbar2.ax.tick_params(labelsize=14)
+    cbar2.set_label(r"Rock density (kg/m$^3$)", rotation=90, labelpad=16, fontsize=12)
+
+    plt.savefig("rayleigh_taylor.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/RayleighTaylor_EarthLike_3D/rayleigh_taylor.yml b/examples/Planetary/RayleighTaylor_EarthLike_3D/rayleigh_taylor.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e86b50a33e8cd9902b1ed350af4051867dc9b133
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_EarthLike_3D/rayleigh_taylor.yml
@@ -0,0 +1,50 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:        5.9724e27   # Grams
+  UnitLength_in_cgs:      6.371e8     # Centimeters
+  UnitVelocity_in_cgs:    6.371e8     # Centimeters per second
+  UnitCurrent_in_cgs:     1           # Amperes
+  UnitTemp_in_cgs:        1           # Kelvin
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.     # The starting time of the simulation (in internal units).
+  time_end:   800    # The end time of the simulation (in internal units).
+  dt_min:     1e-9   # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e2    # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            rayleigh_taylor  # Common part of the name of output files
+  time_first:          0.               # Time of the first output (in internal units)
+  delta_time:          50               # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          50   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./rayleigh_taylor.hdf5     # The file to read
+  periodic:   1
+    
+Scheduler:
+    max_top_level_cells:      100         # Maximal number of top-level cells in any dimension.
+    tasks_per_cell: 100
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_ANEOS_forsterite:   1     # ANEOS forsterite (Stewart et al. 2019), material ID 400
+    planetary_use_ANEOS_Fe85Si15:     1     # ANEOS Fe85Si15 (Stewart 2020), material ID 402
+    # Tablulated EoS file paths.
+    planetary_ANEOS_forsterite_table_file:  ../EoSTables/ANEOS_forsterite_S19.txt
+    planetary_ANEOS_Fe85Si15_table_file:    ../EoSTables/ANEOS_Fe85Si15_S20.txt
+    
+ConstantPotential:
+    g_cgs: [0, -991, 0]	# Gravitation accelertion calculated by GM(<R1) / R1^2
diff --git a/examples/Planetary/RayleighTaylor_EarthLike_3D/run.sh b/examples/Planetary/RayleighTaylor_EarthLike_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..7eb79f9f73a8051bb9603bb489597d4d1c95536a
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_EarthLike_3D/run.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e rayleigh_taylor.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Rayleigh--Taylor test..."
+    python3 makeIC.py
+fi
+
+# Download the equation of state tables if not already present
+if [ ! -e ../EoSTables/ANEOS_forsterite_S19.txt ]
+then
+    cd ../EoSTables
+    ./get_eos_tables.sh
+    cd -
+fi
+
+# Run SWIFT
+../../../swift --hydro --external-gravity --threads=4 rayleigh_taylor.yml 2>&1 | tee output_rayleigh_taylor.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
diff --git a/examples/Planetary/RayleighTaylor_IdealGas_3D/README.md b/examples/Planetary/RayleighTaylor_IdealGas_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..2e44603b58a86e0e2a0ea0d06f85c643fb376eba
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_IdealGas_3D/README.md
@@ -0,0 +1,19 @@
+Rayleigh--Taylor Instabilty (ideal gas, equal mass, 3D)
+--------------
+
+This is a 3D version of the Rayleigh--Taylor instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025), section 4.5.
+
+This test uses particles of equal mass and has a sharp density.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2 --with-ext-potential=constant --with-adiabatic-index=7/5 --enable-boundary-particles=20993`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2 --with-ext-potential=constant --with-adiabatic-index=7/5 --enable-boundary-particles=20993`
diff --git a/examples/Planetary/RayleighTaylor_IdealGas_3D/makeIC.py b/examples/Planetary/RayleighTaylor_IdealGas_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..54b7a9cc8e8d6c6fb3c58ec9b22b2b5e15940675
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_IdealGas_3D/makeIC.py
@@ -0,0 +1,231 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+
+# Generates a swift IC file for the Rayleigh-Taylor instability test
+
+# Parameters
+N2_l = 64  # Number of particles along one edge in lower region
+N2_depth = 18  # Number of particles along in z dimension in lower region
+gamma = 7.0 / 5.0  # Gas adiabatic index
+matID1 = 0  # Upper region material ID: ideal gas
+matID2 = 0  # Lower region material ID: ideal gas
+rho1_approx = 2  # Approximate density of upper region. To be recalculated
+rho2 = 1  # Density of lower region
+g = -0.5  # Constant gravitational acceleration
+dv = 0.025  # Size of velocity perturbation
+boxsize_factor = 1
+boxsize_xy = [
+    0.5 * boxsize_factor,
+    1.0 * boxsize_factor,
+]  # Size of the box in x and y dimensions
+boxsize_depth = boxsize_xy[0] * N2_depth / N2_l  # Size of simulation box in z dimension
+fixed_region = [
+    0.05 * boxsize_factor,
+    0.95 * boxsize_factor,
+]  # y-range of non fixed_region particles
+perturbation_region = [
+    0.3 * boxsize_factor,
+    0.7 * boxsize_factor,
+]  # y-range for the velocity perturbation
+fileOutputName = "rayleigh_taylor.hdf5"
+# ---------------------------------------------------
+
+# Start by generating grids of particles of the two densities
+numPart2 = N2_l * N2_l * N2_depth
+numPart1 = int(numPart2 / rho2 * rho1_approx)
+N1_l = int(np.cbrt(boxsize_xy[0] * numPart1 / boxsize_depth))
+N1_l -= N1_l % 4  # Make RT symmetric across centre of both instability regions
+N1_depth = int(boxsize_depth * N1_l / boxsize_xy[0])
+numPart1 = int(N1_l * N1_l * N1_depth)
+numPart = numPart2 + numPart1
+
+# Calculate particle masses and rho1
+part_volume_l = (boxsize_xy[0] * 0.5 * boxsize_xy[1] * boxsize_depth) / numPart2
+mass = rho2 * part_volume_l
+part_volume_h = (boxsize_xy[0] * 0.5 * boxsize_xy[1] * boxsize_depth) / numPart1
+rho1 = mass / part_volume_h
+
+# Set pressure at interface
+P0 = rho1 / gamma
+
+# Now construct two lattices of particles in the two regions
+A2_coords1 = np.empty((numPart1, 3))
+A2_coords2 = np.empty((numPart2, 3))
+A2_vel1 = np.zeros((numPart1, 3))
+A2_vel2 = np.zeros((numPart2, 3))
+A1_mat1 = np.full(numPart1, matID1)
+A1_mat2 = np.full(numPart2, matID2)
+A1_m1 = np.full(numPart1, mass)
+A1_m2 = np.full(numPart2, mass)
+A1_rho1 = np.full(numPart1, rho1)
+A1_rho2 = np.full(numPart2, rho2)
+A1_u1 = np.empty(numPart1)
+A1_u2 = np.empty(numPart2)
+A1_h1 = np.full(numPart1, boxsize_xy[0] / N1_l)
+A1_h2 = np.full(numPart2, boxsize_xy[0] / N2_l)
+A1_ids = np.zeros(numPart)
+
+# Set up boundary particle counter
+# Boundary particles are set by the N lowest ids of particles, where N is set when configuring swift
+boundary_particles = 1
+
+# Particles in the upper region
+for i in range(N1_depth):
+    for j in range(N1_l):
+        for k in range(N1_l):
+            index = i * N1_l ** 2 + j * N1_l + k
+            A2_coords1[index, 0] = (j / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_xy[
+                0
+            ]
+            A2_coords1[index, 1] = (k / float(N1_l) + 1.0 / (2.0 * N1_l)) * (
+                0.5 * boxsize_xy[1]
+            ) + 0.5 * boxsize_xy[1]
+            A2_coords1[index, 2] = (
+                i / float(N1_depth) + 1.0 / (2.0 * N1_depth)
+            ) * boxsize_depth
+            A1_rho1[index] = rho1
+
+            # If in top and bottom where particles are fixed
+            if (
+                A2_coords1[index, 1] < fixed_region[0]
+                or A2_coords1[index, 1] > fixed_region[1]
+            ):
+                A1_ids[index] = boundary_particles
+                boundary_particles += 1
+
+
+# Particles in the lower region
+for i in range(N2_depth):
+    for j in range(N2_l):
+        for k in range(N2_l):
+            index = i * N2_l ** 2 + j * N2_l + k
+            A2_coords2[index, 0] = (j / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_xy[
+                0
+            ]
+            A2_coords2[index, 1] = (k / float(N2_l) + 1.0 / (2.0 * N2_l)) * (
+                0.5 * boxsize_xy[1]
+            )
+            A2_coords2[index, 2] = (
+                i / float(N2_depth) + 1.0 / (2.0 * N2_depth)
+            ) * boxsize_depth
+            A1_rho2[index] = rho2
+
+            # If in top and bottom where particles are fixed
+            if (
+                A2_coords2[index, 1] < fixed_region[0]
+                or A2_coords2[index, 1] > fixed_region[1]
+            ):
+                A1_ids[index + numPart1] = boundary_particles
+                boundary_particles += 1
+
+print(
+    "You need to compile the code with "
+    "--enable-boundary-particles=%i" % boundary_particles
+)
+
+# Set IDs of non-boundary particles
+A1_ids[A1_ids == 0] = np.linspace(
+    boundary_particles, numPart, numPart - boundary_particles + 1
+)
+
+# The placement of the lattices are now adjusted to give appropriate interfaces
+# Calculate the separation of particles across the density discontinuity
+pcl_separation_2 = np.cbrt(mass / rho2)
+pcl_separation_1 = np.cbrt(mass / rho1)
+boundary_separation = 0.5 * (pcl_separation_2 + pcl_separation_1)
+
+# Shift top lattice
+min_y1 = min(A2_coords1[:, 1])
+max_y2 = max(A2_coords2[:, 1])
+shift_distance = boundary_separation - (min_y1 - max_y2)
+A2_coords1[:, 1] += shift_distance
+
+# Calculate internal energies
+A1_P1 = P0 + g * A1_rho1 * (A2_coords1[:, 1] - 0.5 * boxsize_xy[1])
+A1_P2 = P0 + g * A1_rho2 * (A2_coords2[:, 1] - 0.5 * boxsize_xy[1])
+A1_u1 = A1_P1 / (A1_rho1 * (gamma - 1.0))
+A1_u2 = A1_P2 / (A1_rho2 * (gamma - 1.0))
+
+# Now the two lattices can be combined
+A2_coords = np.append(A2_coords1, A2_coords2, axis=0)
+A2_vel = np.append(A2_vel1, A2_vel2, axis=0)
+A1_mat = np.append(A1_mat1, A1_mat2, axis=0)
+A1_m = np.append(A1_m1, A1_m2, axis=0)
+A1_rho = np.append(A1_rho1, A1_rho2, axis=0)
+A1_u = np.append(A1_u1, A1_u2, axis=0)
+A1_h = np.append(A1_h1, A1_h2, axis=0)
+
+# Add velocity perturbation
+mask_perturb = np.logical_and(
+    A2_coords[:, 1] > perturbation_region[0], A2_coords[:, 1] < perturbation_region[1]
+)
+A2_vel[mask_perturb, 1] = (
+    dv
+    * (1 + np.cos(8 * np.pi * (A2_coords[mask_perturb, 0] / (boxsize_factor) + 0.25)))
+    * (1 + np.cos(5 * np.pi * (A2_coords[mask_perturb, 1] / (boxsize_factor) - 0.5)))
+)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [
+        boxsize_xy[0],
+        boxsize_xy[1] + shift_distance,
+        boxsize_depth,
+    ]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/RayleighTaylor_IdealGas_3D/plotSnapshots.py b/examples/Planetary/RayleighTaylor_IdealGas_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf3c804d83cdcab904e053147555cdad6f1d9a07
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_IdealGas_3D/plotSnapshots.py
@@ -0,0 +1,172 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Rayleigh--Taylor instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax_x = 40
+    n_gs_ax_y = 80
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len_x = 5
+    ax_len_y = 10
+    ax_gap_len = n_gs_ax_gap * ax_len_x / n_gs_ax_x
+    cbar_gap_len = n_gs_cbar_gap * ax_len_x / n_gs_ax_x
+    cbar_len = n_gs_cbar * ax_len_x / n_gs_ax_x
+
+    fig = plt.figure(
+        figsize=(3 * ax_len_x + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len_y)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax_y,
+        ncols=3 * n_gs_ax_x + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar,
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax_y, :n_gs_ax_x])
+    ax_1 = plt.subplot(
+        gs[:n_gs_ax_y, n_gs_ax_x + n_gs_ax_gap : 2 * n_gs_ax_x + n_gs_ax_gap]
+    )
+    ax_2 = plt.subplot(
+        gs[
+            :n_gs_ax_y,
+            2 * n_gs_ax_x + 2 * n_gs_ax_gap : 3 * n_gs_ax_x + 2 * n_gs_ax_gap,
+        ]
+    )
+    cax = plt.subplot(
+        gs[
+            :n_gs_ax_y,
+            3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+
+    return axs, cax
+
+
+def plot_kh(ax, snap, cmap, norm):
+
+    # Load data
+    snap_file = "rayleigh_taylor_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_x = f["Header"].attrs["BoxSize"][0]
+        boxsize_y = f["Header"].attrs["BoxSize"][1]
+        A1_x = f["/PartType0/Coordinates"][:, 0]
+        A1_y = f["/PartType0/Coordinates"][:, 1]
+        A1_z = f["/PartType0/Coordinates"][:, 2]
+        A1_rho = f["/PartType0/Densities"][:]
+        A1_m = f["/PartType0/Masses"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.1 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_x ** 2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice,
+        A1_y_slice,
+        c=A1_rho_slice,
+        norm=norm,
+        cmap=cmap,
+        s=A1_size,
+        edgecolors="none",
+    )
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0.0, boxsize_x))
+    ax.set_ylim((0.05 * boxsize_y, 0.95 * boxsize_y))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap = plt.get_cmap("Spectral_r")
+    vmin, vmax = 0.95, 2.05
+    norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
+
+    # Generate axes
+    axs, cax = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [8, 12, 16]
+    times = ["2.0", "3.0", "4.0"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, cmap, norm)
+        ax.text(
+            0.5,
+            -0.05,
+            r"$t =\;$" + time,
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
+    cbar = plt.colorbar(sm, cax)
+    cbar.ax.tick_params(labelsize=14)
+    cbar.set_label("Density", rotation=90, labelpad=8, fontsize=18)
+
+    plt.savefig("rayleigh_taylor.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/RayleighTaylor_IdealGas_3D/rayleigh_taylor.yml b/examples/Planetary/RayleighTaylor_IdealGas_3D/rayleigh_taylor.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1e0d9cc864d6d16681bba9877a23852039306e8b
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_IdealGas_3D/rayleigh_taylor.yml
@@ -0,0 +1,45 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1   # Grams
+  UnitLength_in_cgs:   1   # Centimeters
+  UnitVelocity_in_cgs: 1   # Centimeters per second
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.0    # The starting time of the simulation (in internal units).
+  time_end:   4.0    # The end time of the simulation (in internal units).
+  dt_min:     1e-9   # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            rayleigh_taylor  # Common part of the name of output files
+  time_first:          0.               # Time of the first output (in internal units)
+  delta_time:          0.25             # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          0.25  # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./rayleigh_taylor.hdf5     # The file to read
+  periodic:   1
+  
+Scheduler:
+    max_top_level_cells:  100         # Maximal number of top-level cells in any dimension.
+    tasks_per_cell: 100
+    
+# Parameters related to the equation of state
+EoS:
+    planetary_use_idg_def:    1     # Default ideal gas, material ID 0
+    
+ConstantPotential:
+    g_cgs: [0, -0.5, 0]
diff --git a/examples/Planetary/RayleighTaylor_IdealGas_3D/run.sh b/examples/Planetary/RayleighTaylor_IdealGas_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..1da853b9a8df7d11cea014d29295fff61f7a6954
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_IdealGas_3D/run.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e rayleigh_taylor.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Rayleigh--Taylor test..."
+    python3 makeIC.py
+fi
+
+# Run SWIFT
+../../../swift --hydro --external-gravity --threads=4 rayleigh_taylor.yml 2>&1 | tee output_rayleigh_taylor.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
diff --git a/examples/Planetary/RayleighTaylor_JupiterLike_3D/README.md b/examples/Planetary/RayleighTaylor_JupiterLike_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..ad2a06214461029d0a7dfdb3117f2ee6da250a70
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_JupiterLike_3D/README.md
@@ -0,0 +1,20 @@
+Rayleigh--Taylor Instabilty (Jupiter-like, equal mass, 3D)
+--------------
+
+This is a 3D version of the Rayleigh--Taylor instability. These initial
+conditions are those used to produce the simulations presented by
+Sandnes et al. (2025b).
+
+This test uses particles of equal mass and has a sharp density. Equations
+of state and conditions are representative of those within Jupiter's interior.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2 --with-ext-potential=constant --enable-boundary-particles=22369`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2 --with-ext-potential=constant --enable-boundary-particles=22369`
diff --git a/examples/Planetary/RayleighTaylor_JupiterLike_3D/makeIC.py b/examples/Planetary/RayleighTaylor_JupiterLike_3D/makeIC.py
new file mode 100644
index 0000000000000000000000000000000000000000..29ccb05eebde606ce5edf190696a9a209ef02455
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_JupiterLike_3D/makeIC.py
@@ -0,0 +1,237 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import h5py
+import numpy as np
+import woma
+
+# Load EoS tables
+woma.load_eos_tables(["CD21_HHe", "AQUA"])
+
+# Generates a swift IC file for the Rayleigh-Taylor instability test
+
+# Constants
+R_earth = 6371000  # Earth radius
+R_jupiter = 11.2089 * R_earth  # Jupiter radius
+
+# Parameters
+N2_l = 64  # Number of particles along one edge in lower region
+N2_depth = 18  # Number of particles along in z dimension in lower region
+matID1 = 304  # Upper region material ID: AQUA
+matID2 = 307  # Lower region material ID: CD21 H--He
+rho1_approx = 9000  # Approximate density of upper region. To be recalculated
+rho2 = 3500  # Density of lower region
+g = -31.44  # Constant gravitational acceleration
+P0 = 3.2e12  # Pressure at interface
+boxsize_factor = 0.1 * R_jupiter
+dv = 0.00025 * boxsize_factor  # Size of velocity perturbation
+boxsize_xy = [
+    0.5 * boxsize_factor,
+    1.0 * boxsize_factor,
+]  # Size of the box in x and y dimensions
+boxsize_depth = boxsize_xy[0] * N2_depth / N2_l  # Size of simulation box in z dimension
+fixed_region = [
+    0.05 * boxsize_factor,
+    0.95 * boxsize_factor,
+]  # y-range of non fixed_region particles
+perturbation_region = [
+    0.3 * boxsize_factor,
+    0.7 * boxsize_factor,
+]  # y-range for the velocity perturbation
+fileOutputName = "rayleigh_taylor.hdf5"
+# ---------------------------------------------------
+
+# Start by generating grids of particles of the two densities
+numPart2 = N2_l * N2_l * N2_depth
+numPart1 = int(numPart2 / rho2 * rho1_approx)
+N1_l = int(np.cbrt(boxsize_xy[0] * numPart1 / boxsize_depth))
+N1_l -= N1_l % 4  # Make RT symmetric across centre of both instability regions
+N1_depth = int(boxsize_depth * N1_l / boxsize_xy[0])
+numPart1 = int(N1_l * N1_l * N1_depth)
+numPart = numPart2 + numPart1
+
+# Calculate particle masses and rho1
+part_volume_l = (boxsize_xy[0] * 0.5 * boxsize_xy[1] * boxsize_depth) / numPart2
+mass = rho2 * part_volume_l
+part_volume_h = (boxsize_xy[0] * 0.5 * boxsize_xy[1] * boxsize_depth) / numPart1
+rho1 = mass / part_volume_h
+
+# Now construct two lattices of particles in the two regions
+A2_coords1 = np.empty((numPart1, 3))
+A2_coords2 = np.empty((numPart2, 3))
+A2_vel1 = np.zeros((numPart1, 3))
+A2_vel2 = np.zeros((numPart2, 3))
+A1_mat1 = np.full(numPart1, matID1)
+A1_mat2 = np.full(numPart2, matID2)
+A1_m1 = np.full(numPart1, mass)
+A1_m2 = np.full(numPart2, mass)
+A1_rho1 = np.full(numPart1, rho1)
+A1_rho2 = np.full(numPart2, rho2)
+A1_u1 = np.empty(numPart1)
+A1_u2 = np.empty(numPart2)
+A1_h1 = np.full(numPart1, boxsize_xy[0] / N1_l)
+A1_h2 = np.full(numPart2, boxsize_xy[0] / N2_l)
+A1_ids = np.zeros(numPart)
+
+# Set up boundary particle counter
+# Boundary particles are set by the N lowest ids of particles, where N is set when configuring swift
+boundary_particles = 1
+
+# Particles in the upper region
+for i in range(N1_depth):
+    for j in range(N1_l):
+        for k in range(N1_l):
+            index = i * N1_l ** 2 + j * N1_l + k
+            A2_coords1[index, 0] = (j / float(N1_l) + 1.0 / (2.0 * N1_l)) * boxsize_xy[
+                0
+            ]
+            A2_coords1[index, 1] = (k / float(N1_l) + 1.0 / (2.0 * N1_l)) * (
+                0.5 * boxsize_xy[1]
+            ) + 0.5 * boxsize_xy[1]
+            A2_coords1[index, 2] = (
+                i / float(N1_depth) + 1.0 / (2.0 * N1_depth)
+            ) * boxsize_depth
+            A1_rho1[index] = rho1
+
+            # If in top and bottom where particles are fixed
+            if (
+                A2_coords1[index, 1] < fixed_region[0]
+                or A2_coords1[index, 1] > fixed_region[1]
+            ):
+                A1_ids[index] = boundary_particles
+                boundary_particles += 1
+
+
+# Particles in the lower region
+for i in range(N2_depth):
+    for j in range(N2_l):
+        for k in range(N2_l):
+            index = i * N2_l ** 2 + j * N2_l + k
+            A2_coords2[index, 0] = (j / float(N2_l) + 1.0 / (2.0 * N2_l)) * boxsize_xy[
+                0
+            ]
+            A2_coords2[index, 1] = (k / float(N2_l) + 1.0 / (2.0 * N2_l)) * (
+                0.5 * boxsize_xy[1]
+            )
+            A2_coords2[index, 2] = (
+                i / float(N2_depth) + 1.0 / (2.0 * N2_depth)
+            ) * boxsize_depth
+            A1_rho2[index] = rho2
+
+            # If in top and bottom where particles are fixed
+            if (
+                A2_coords2[index, 1] < fixed_region[0]
+                or A2_coords2[index, 1] > fixed_region[1]
+            ):
+                A1_ids[index + numPart1] = boundary_particles
+                boundary_particles += 1
+
+print(
+    "You need to compile the code with "
+    "--enable-boundary-particles=%i" % boundary_particles
+)
+
+# Set IDs of non-boundary particles
+A1_ids[A1_ids == 0] = np.linspace(
+    boundary_particles, numPart, numPart - boundary_particles + 1
+)
+
+# The placement of the lattices are now adjusted to give appropriate interfaces
+# Calculate the separation of particles across the density discontinuity
+pcl_separation_2 = np.cbrt(mass / rho2)
+pcl_separation_1 = np.cbrt(mass / rho1)
+boundary_separation = 0.5 * (pcl_separation_2 + pcl_separation_1)
+
+# Shift top lattice
+min_y1 = min(A2_coords1[:, 1])
+max_y2 = max(A2_coords2[:, 1])
+shift_distance = boundary_separation - (min_y1 - max_y2)
+A2_coords1[:, 1] += shift_distance
+
+# Calculate internal energies
+A1_P1 = P0 + g * A1_rho1 * (A2_coords1[:, 1] - 0.5 * boxsize_xy[1])
+A1_P2 = P0 + g * A1_rho2 * (A2_coords2[:, 1] - 0.5 * boxsize_xy[1])
+A1_u1 = woma.A1_Z_rho_Y(A1_rho1, A1_P1, A1_mat1, Z_choice="u", Y_choice="P")
+A1_u2 = woma.A1_Z_rho_Y(A1_rho2, A1_P2, A1_mat2, Z_choice="u", Y_choice="P")
+
+# Now the two lattices can be combined
+A2_coords = np.append(A2_coords1, A2_coords2, axis=0)
+A2_vel = np.append(A2_vel1, A2_vel2, axis=0)
+A1_mat = np.append(A1_mat1, A1_mat2, axis=0)
+A1_m = np.append(A1_m1, A1_m2, axis=0)
+A1_rho = np.append(A1_rho1, A1_rho2, axis=0)
+A1_u = np.append(A1_u1, A1_u2, axis=0)
+A1_h = np.append(A1_h1, A1_h2, axis=0)
+
+# Add velocity perturbation
+mask_perturb = np.logical_and(
+    A2_coords[:, 1] > perturbation_region[0], A2_coords[:, 1] < perturbation_region[1]
+)
+A2_vel[mask_perturb, 1] = (
+    dv
+    * (1 + np.cos(8 * np.pi * (A2_coords[mask_perturb, 0] / (boxsize_factor) + 0.25)))
+    * (1 + np.cos(5 * np.pi * (A2_coords[mask_perturb, 1] / (boxsize_factor) - 0.5)))
+)
+
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [
+        boxsize_xy[0],
+        boxsize_xy[1] + shift_distance,
+        boxsize_depth,
+    ]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFileOutputsPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 100.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1000.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
+    ds[()] = A2_coords
+    ds = grp.create_dataset("Velocities", (numPart, 3), "f")
+    ds[()] = A2_vel
+    ds = grp.create_dataset("Masses", (numPart, 1), "f")
+    ds[()] = A1_m.reshape((numPart, 1))
+    ds = grp.create_dataset("Density", (numPart, 1), "f")
+    ds[()] = A1_rho.reshape((numPart, 1))
+    ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
+    ds[()] = A1_h.reshape((numPart, 1))
+    ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
+    ds[()] = A1_u.reshape((numPart, 1))
+    ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
+    ds[()] = A1_ids.reshape((numPart, 1))
+    ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+    ds[()] = A1_mat.reshape((numPart, 1))
diff --git a/examples/Planetary/RayleighTaylor_JupiterLike_3D/plotSnapshots.py b/examples/Planetary/RayleighTaylor_JupiterLike_3D/plotSnapshots.py
new file mode 100644
index 0000000000000000000000000000000000000000..93bb973d326e0a4475b2fcb5c2cf7edbfcf675c1
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_JupiterLike_3D/plotSnapshots.py
@@ -0,0 +1,219 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+Generate plot of the 3D Rayleigh--Taylor instability.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def make_axes():
+    # Use Gridspec to set up figure
+    n_gs_ax_x = 40
+    n_gs_ax_y = 80
+    n_gs_ax_gap = 1
+    n_gs_cbar_gap = 1
+    n_gs_cbar = 2
+
+    ax_len_x = 5
+    ax_len_y = 10
+    ax_gap_len = n_gs_ax_gap * ax_len_x / n_gs_ax_x
+    cbar_gap_len = n_gs_cbar_gap * ax_len_x / n_gs_ax_x
+    cbar_len = n_gs_cbar * ax_len_x / n_gs_ax_x
+
+    fig = plt.figure(
+        figsize=(3 * ax_len_x + 2 * ax_gap_len + cbar_gap_len + cbar_len, ax_len_y)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax_y,
+        ncols=3 * n_gs_ax_x + 2 * n_gs_ax_gap + n_gs_cbar_gap + n_gs_cbar,
+    )
+
+    ax_0 = plt.subplot(gs[:n_gs_ax_y, :n_gs_ax_x])
+    ax_1 = plt.subplot(
+        gs[:n_gs_ax_y, n_gs_ax_x + n_gs_ax_gap : 2 * n_gs_ax_x + n_gs_ax_gap]
+    )
+    ax_2 = plt.subplot(
+        gs[
+            :n_gs_ax_y,
+            2 * n_gs_ax_x + 2 * n_gs_ax_gap : 3 * n_gs_ax_x + 2 * n_gs_ax_gap,
+        ]
+    )
+
+    cax_0 = plt.subplot(
+        gs[
+            : int(0.5 * n_gs_ax_y) - 1,
+            3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+    cax_1 = plt.subplot(
+        gs[
+            int(0.5 * n_gs_ax_y) + 1 :,
+            3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap : 3 * n_gs_ax_x
+            + 2 * n_gs_ax_gap
+            + n_gs_cbar_gap
+            + n_gs_cbar,
+        ]
+    )
+
+    axs = [ax_0, ax_1, ax_2]
+    caxs = [cax_0, cax_1]
+
+    return axs, caxs
+
+
+def plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2):
+
+    # Load data
+    snap_file = "rayleigh_taylor_%04d.hdf5" % snap
+
+    with h5py.File(snap_file, "r") as f:
+        # Units from file metadata to SI
+        m = float(f["Units"].attrs["Unit mass in cgs (U_M)"][0]) * 1e-3
+        l = float(f["Units"].attrs["Unit length in cgs (U_L)"][0]) * 1e-2
+
+        boxsize_x = f["Header"].attrs["BoxSize"][0] * l
+        boxsize_y = f["Header"].attrs["BoxSize"][1] * l
+        A1_x = f["/PartType0/Coordinates"][:, 0] * l
+        A1_y = f["/PartType0/Coordinates"][:, 1] * l
+        A1_z = f["/PartType0/Coordinates"][:, 2] * l
+        A1_rho = f["/PartType0/Densities"][:] * (m / l ** 3)
+        A1_m = f["/PartType0/Masses"][:] * m
+        A1_mat_id = f["/PartType0/MaterialIDs"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_m = A1_m[sort_indices]
+    A1_mat_id = A1_mat_id[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.1 * (np.max(A1_z) - np.min(A1_z))
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+    A1_mat_id_slice = A1_mat_id[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3) / boxsize_x ** 2
+
+    mask_mat1 = A1_mat_id_slice == mat_id1
+    mask_mat2 = A1_mat_id_slice == mat_id2
+
+    # Plot
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat1],
+        A1_y_slice[mask_mat1],
+        c=A1_rho_slice[mask_mat1],
+        norm=norm1,
+        cmap=cmap1,
+        s=A1_size[mask_mat1],
+        edgecolors="none",
+    )
+
+    scatter = ax.scatter(
+        A1_x_slice[mask_mat2],
+        A1_y_slice[mask_mat2],
+        c=A1_rho_slice[mask_mat2],
+        norm=norm2,
+        cmap=cmap2,
+        s=A1_size[mask_mat2],
+        edgecolors="none",
+    )
+
+    ax.set_xticks([])
+    ax.set_yticks([])
+    ax.set_facecolor((0.9, 0.9, 0.9))
+    ax.set_xlim((0.0, boxsize_x))
+    ax.set_ylim((0.05 * boxsize_y, 0.95 * boxsize_y))
+
+
+if __name__ == "__main__":
+
+    # Set colormap
+    cmap1 = plt.get_cmap("winter_r")
+    mat_id1 = 304
+    rho_min1 = 6800
+    rho_max1 = 8700
+    norm1 = mpl.colors.Normalize(vmin=rho_min1, vmax=rho_max1)
+
+    cmap2 = plt.get_cmap("autumn")
+    mat_id2 = 307
+    rho_min2 = 3450
+    rho_max2 = 4050
+    norm2 = mpl.colors.Normalize(vmin=rho_min2, vmax=rho_max2)
+
+    # Generate axes
+    axs, caxs = make_axes()
+
+    # The three snapshots to be plotted
+    snaps = [10, 15, 20]
+    times = ["500", "750", "1000"]
+
+    # Plot
+    for i, snap in enumerate(snaps):
+        ax = axs[i]
+        time = times[i]
+
+        plot_kh(ax, snap, mat_id1, mat_id2, cmap1, cmap2, norm1, norm2)
+        ax.text(
+            0.5,
+            -0.05,
+            r"$t =\;$" + time + r"$\,$s",
+            horizontalalignment="center",
+            size=18,
+            transform=ax.transAxes,
+        )
+
+    # Colour bar
+    sm1 = plt.cm.ScalarMappable(cmap=cmap1, norm=norm1)
+    cbar1 = plt.colorbar(sm1, caxs[0])
+    cbar1.ax.tick_params(labelsize=14)
+    cbar1.set_label(r"Ice density (kg/m$^3$)", rotation=90, labelpad=8, fontsize=12)
+
+    sm2 = plt.cm.ScalarMappable(cmap=cmap2, norm=norm2)
+    cbar2 = plt.colorbar(sm2, caxs[1])
+    cbar2.ax.tick_params(labelsize=14)
+    cbar2.set_label(r"H--He density (kg/m$^3$)", rotation=90, labelpad=8, fontsize=12)
+
+    plt.savefig("rayleigh_taylor.png", dpi=300, bbox_inches="tight")
diff --git a/examples/Planetary/RayleighTaylor_JupiterLike_3D/rayleigh_taylor.yml b/examples/Planetary/RayleighTaylor_JupiterLike_3D/rayleigh_taylor.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f3cba5586e862bc94b033f7c38a9141e9d9b1b21
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_JupiterLike_3D/rayleigh_taylor.yml
@@ -0,0 +1,50 @@
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:        5.9724e27   # Grams
+  UnitLength_in_cgs:      6.371e8     # Centimeters
+  UnitVelocity_in_cgs:    6.371e8     # Centimeters per second
+  UnitCurrent_in_cgs:     1           # Amperes
+  UnitTemp_in_cgs:        1           # Kelvin
+  
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   1000   # The end time of the simulation (in internal units).
+  dt_min:     1e-9  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e2  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            rayleigh_taylor  # Common part of the name of output files
+  time_first:          0.               # Time of the first output (in internal units)
+  delta_time:          50              # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          50 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./rayleigh_taylor.hdf5     # The file to read
+  periodic:   1
+
+Scheduler:
+    max_top_level_cells:      100         # Maximal number of top-level cells in any dimension.
+    tasks_per_cell: 100
+
+# Parameters related to the equation of state
+EoS:
+    # Select which planetary EoS material(s) to enable for use.
+    planetary_use_CD21_HHe:  1     # Hydrogen--helium (Chabrier and Debras 2021), material ID 307
+    planetary_use_AQUA:      1     # AQUA (Haldemann et al. 2020), material ID 304
+    # Tablulated EoS file paths.
+    planetary_CD21_HHe_table_file: ../EoSTables/CD21_HHe.txt
+    planetary_AQUA_table_file:     ../EoSTables/AQUA_H20.txt
+    
+ConstantPotential:
+    g_cgs: [0, -3144, 0]	# Gravitation accelertion calculated by GM(<R1) / R1^2
diff --git a/examples/Planetary/RayleighTaylor_JupiterLike_3D/run.sh b/examples/Planetary/RayleighTaylor_JupiterLike_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..3d80cac747c4084852ce6b554ce9a3236bad82d8
--- /dev/null
+++ b/examples/Planetary/RayleighTaylor_JupiterLike_3D/run.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e rayleigh_taylor.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D Rayleigh--Taylor test..."
+    python3 makeIC.py
+fi
+
+# Download the equation of state tables if not already present
+if [ ! -e ../EoSTables/CD21_HHe.txt ]
+then
+    cd ../EoSTables
+    ./get_eos_tables.sh
+    cd -
+fi
+
+# Run SWIFT
+../../../swift --hydro --external-gravity --threads=4 rayleigh_taylor.yml 2>&1 | tee output_rayleigh_taylor.log
+
+# Plot the solutions
+python3 ./plotSnapshots.py
diff --git a/examples/Planetary/SodShock_3D/README.md b/examples/Planetary/SodShock_3D/README.md
index 5887d5abf3e7cc956efd1040b4974cacc3ff5c0e..7b3065ec78fa4ec033e119f5489a748b262c2fb8 100644
--- a/examples/Planetary/SodShock_3D/README.md
+++ b/examples/Planetary/SodShock_3D/README.md
@@ -2,13 +2,20 @@ Sod Shock 3D (Planetary)
 ============
 
 This is a copy of `/examples/HydroTests/SodShock_3D` for testing the Planetary 
-hydro scheme with the planetary ideal gas equation of state. 
+and REMIX hydro schemes with the planetary ideal gas equation of state. 
 
-The results should be highly similar to the Minimal hydro scheme, though 
-slightly different because of the higher viscosity beta used here. To recover 
-the Minimal scheme behaviour, edit `const_viscosity_beta` from 4 to 3 in 
-`src/hydro/Planetary/hydro_parameters.h`.
+The Planetary hydro scheme results should be highly similar to the Minimal
+hydro scheme, though  slightly different because of the higher viscosity beta
+used here. To recover  the Minimal scheme behaviour, edit `const_viscosity_beta`
+from 4 to 3 in `src/hydro/Planetary/hydro_parameters.h`.
 
-This requires the code to be configured to use the planetary hydrodynamics 
-scheme and equations of state: 
-`--with-hydro=planetary --with-equation-of-state=planetary`
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/SodShock_3D/makeIC.py b/examples/Planetary/SodShock_3D/makeIC.py
index 1953230c9c11727e377c454e2726f4e56cadb4a5..7f38da3df1b822b460f2f4d963a1ee15940e0947 100644
--- a/examples/Planetary/SodShock_3D/makeIC.py
+++ b/examples/Planetary/SodShock_3D/makeIC.py
@@ -66,6 +66,7 @@ vol_R = 0.25
 v = zeros((numPart, 3))
 ids = linspace(1, numPart, numPart)
 m = zeros(numPart)
+rho = zeros(numPart)
 u = zeros(numPart)
 mat = zeros(numPart)
 
@@ -75,10 +76,12 @@ for i in range(numPart):
     if x < 0:  # left
         u[i] = P_L / (rho_L * (gamma - 1.0))
         m[i] = rho_L * vol_L / numPart_L
+        rho[i] = rho_L
         v[i, 0] = v_L
     else:  # right
         u[i] = P_R / (rho_R * (gamma - 1.0))
         m[i] = rho_R * vol_R / numPart_R
+        rho[i] = rho_R
         v[i, 0] = v_R
 
 # Shift particles
@@ -112,8 +115,9 @@ grp = file.create_group("/PartType0")
 grp.create_dataset("Coordinates", data=pos, dtype="d")
 grp.create_dataset("Velocities", data=v, dtype="f")
 grp.create_dataset("Masses", data=m, dtype="f")
-grp.create_dataset("SmoothingLengths", data=h, dtype="f")
-grp.create_dataset("InternalEnergies", data=u, dtype="f")
+grp.create_dataset("Density", data=rho, dtype="f")
+grp.create_dataset("SmoothingLength", data=h, dtype="f")
+grp.create_dataset("InternalEnergy", data=u, dtype="f")
 grp.create_dataset("ParticleIDs", data=ids, dtype="L")
 grp.create_dataset("MaterialIDs", data=mat, dtype="i")
 
diff --git a/examples/Planetary/SodShock_3D/plotSolution.py b/examples/Planetary/SodShock_3D/plotSolution.py
new file mode 100644
index 0000000000000000000000000000000000000000..72d16a4b3a4ee2aae6342613317d59085891c9c3
--- /dev/null
+++ b/examples/Planetary/SodShock_3D/plotSolution.py
@@ -0,0 +1,259 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2016 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# Computes the analytical solution of the Sod shock and plots the SPH answer
+
+
+# Generates the analytical  solution for the Sod shock test case
+# The script works for a given left (x<0) and right (x>0) state and computes the solution at a later time t.
+# This follows the solution given in (Toro, 2009)
+
+
+# Parameters
+gas_gamma = 5.0 / 3.0  # Polytropic index
+rho_L = 1.0  # Density left state
+rho_R = 0.125  # Density right state
+v_L = 0.0  # Velocity left state
+v_R = 0.0  # Velocity right state
+P_L = 1.0  # Pressure left state
+P_R = 0.1  # Pressure right state
+
+import sys
+
+sys.path.append("../../HydroTests/")
+from riemannSolver import RiemannSolver
+
+import matplotlib
+
+matplotlib.use("Agg")
+from pylab import *
+from scipy import stats
+import h5py
+
+style.use("../../../tools/stylesheets/mnras.mplstyle")
+
+snap = int(sys.argv[1])
+
+
+# Read the simulation data
+sim = h5py.File("sodShock_%04d.hdf5" % snap, "r")
+boxSize = sim["/Header"].attrs["BoxSize"][0]
+time = sim["/Header"].attrs["Time"][0]
+scheme = sim["/HydroScheme"].attrs["Scheme"].decode("utf-8")
+kernel = sim["/HydroScheme"].attrs["Kernel function"].decode("utf-8")
+neighbours = sim["/HydroScheme"].attrs["Kernel target N_ngb"]
+eta = sim["/HydroScheme"].attrs["Kernel eta"]
+git = sim["Code"].attrs["Git Revision"].decode("utf-8")
+
+x = sim["/PartType0/Coordinates"][:, 0]
+v = sim["/PartType0/Velocities"][:, 0]
+u = sim["/PartType0/InternalEnergies"][:]
+S = sim["/PartType0/Entropies"][:]
+P = sim["/PartType0/Pressures"][:]
+rho = sim["/PartType0/Densities"][:]
+
+try:
+    diffusion = sim["/PartType0/DiffusionParameters"][:]
+    plot_diffusion = True
+except:
+    plot_diffusion = False
+
+try:
+    viscosity = sim["/PartType0/ViscosityParameters"][:]
+    plot_viscosity = True
+except:
+    plot_viscosity = False
+
+x_min = -1.0
+x_max = 1.0
+x += x_min
+N = 1000
+
+# Bin the data
+x_bin_edge = np.arange(-0.6, 0.6, 0.02)
+x_bin = 0.5 * (x_bin_edge[1:] + x_bin_edge[:-1])
+rho_bin, _, _ = stats.binned_statistic(x, rho, statistic="mean", bins=x_bin_edge)
+v_bin, _, _ = stats.binned_statistic(x, v, statistic="mean", bins=x_bin_edge)
+P_bin, _, _ = stats.binned_statistic(x, P, statistic="mean", bins=x_bin_edge)
+S_bin, _, _ = stats.binned_statistic(x, S, statistic="mean", bins=x_bin_edge)
+u_bin, _, _ = stats.binned_statistic(x, u, statistic="mean", bins=x_bin_edge)
+rho2_bin, _, _ = stats.binned_statistic(x, rho ** 2, statistic="mean", bins=x_bin_edge)
+v2_bin, _, _ = stats.binned_statistic(x, v ** 2, statistic="mean", bins=x_bin_edge)
+P2_bin, _, _ = stats.binned_statistic(x, P ** 2, statistic="mean", bins=x_bin_edge)
+S2_bin, _, _ = stats.binned_statistic(x, S ** 2, statistic="mean", bins=x_bin_edge)
+u2_bin, _, _ = stats.binned_statistic(x, u ** 2, statistic="mean", bins=x_bin_edge)
+rho_sigma_bin = np.sqrt(rho2_bin - rho_bin ** 2)
+v_sigma_bin = np.sqrt(v2_bin - v_bin ** 2)
+P_sigma_bin = np.sqrt(P2_bin - P_bin ** 2)
+S_sigma_bin = np.sqrt(S2_bin - S_bin ** 2)
+u_sigma_bin = np.sqrt(u2_bin - u_bin ** 2)
+
+if plot_diffusion:
+    alpha_diff_bin, _, _ = stats.binned_statistic(
+        x, diffusion, statistic="mean", bins=x_bin_edge
+    )
+    alpha2_diff_bin, _, _ = stats.binned_statistic(
+        x, diffusion ** 2, statistic="mean", bins=x_bin_edge
+    )
+    alpha_diff_sigma_bin = np.sqrt(alpha2_diff_bin - alpha_diff_bin ** 2)
+
+if plot_viscosity:
+    alpha_visc_bin, _, _ = stats.binned_statistic(
+        x, viscosity, statistic="mean", bins=x_bin_edge
+    )
+    alpha2_visc_bin, _, _ = stats.binned_statistic(
+        x, viscosity ** 2, statistic="mean", bins=x_bin_edge
+    )
+    alpha_visc_sigma_bin = np.sqrt(alpha2_visc_bin - alpha_visc_bin ** 2)
+
+# Prepare reference solution
+solver = RiemannSolver(gas_gamma)
+
+delta_x = (x_max - x_min) / N
+x_s = arange(0.5 * x_min, 0.5 * x_max, delta_x)
+rho_s, v_s, P_s, _ = solver.solve(rho_L, v_L, P_L, rho_R, v_R, P_R, x_s / time)
+
+# Additional arrays
+u_s = P_s / (rho_s * (gas_gamma - 1.0))  # internal energy
+s_s = P_s / rho_s ** gas_gamma  # entropic function
+
+# Plot the interesting quantities
+figure(figsize=(7, 7 / 1.6))
+
+# Velocity profile --------------------------------
+subplot(231)
+plot(x, v, ".", color="r", ms=0.5, alpha=0.2)
+plot(x_s, v_s, "--", color="k", alpha=0.8, lw=1.2)
+errorbar(x_bin, v_bin, yerr=v_sigma_bin, fmt=".", ms=8.0, color="b", lw=1.2)
+xlabel("${\\rm{Position}}~x$", labelpad=0)
+ylabel("${\\rm{Velocity}}~v_x$", labelpad=0)
+xlim(-0.5, 0.5)
+ylim(-0.1, 0.95)
+
+# Density profile --------------------------------
+subplot(232)
+plot(x, rho, ".", color="r", ms=0.5, alpha=0.2)
+plot(x_s, rho_s, "--", color="k", alpha=0.8, lw=1.2)
+errorbar(x_bin, rho_bin, yerr=rho_sigma_bin, fmt=".", ms=8.0, color="b", lw=1.2)
+xlabel("${\\rm{Position}}~x$", labelpad=0)
+ylabel("${\\rm{Density}}~\\rho$", labelpad=0)
+xlim(-0.5, 0.5)
+ylim(0.05, 1.1)
+
+# Pressure profile --------------------------------
+subplot(233)
+plot(x, P, ".", color="r", ms=0.5, alpha=0.2)
+plot(x_s, P_s, "--", color="k", alpha=0.8, lw=1.2)
+errorbar(x_bin, P_bin, yerr=P_sigma_bin, fmt=".", ms=8.0, color="b", lw=1.2)
+xlabel("${\\rm{Position}}~x$", labelpad=0)
+ylabel("${\\rm{Pressure}}~P$", labelpad=0)
+xlim(-0.5, 0.5)
+ylim(0.01, 1.1)
+
+# Internal energy profile -------------------------
+subplot(234)
+plot(x, u, ".", color="r", ms=0.5, alpha=0.2)
+plot(x_s, u_s, "--", color="k", alpha=0.8, lw=1.2)
+errorbar(x_bin, u_bin, yerr=u_sigma_bin, fmt=".", ms=8.0, color="b", lw=1.2)
+xlabel("${\\rm{Position}}~x$", labelpad=0)
+ylabel("${\\rm{Internal~Energy}}~u$", labelpad=0)
+xlim(-0.5, 0.5)
+ylim(0.8, 2.2)
+
+# Entropy profile ---------------------------------
+subplot(235)
+xlim(-0.5, 0.5)
+xlabel("${\\rm{Position}}~x$", labelpad=0)
+
+if plot_diffusion or plot_viscosity:
+    if plot_diffusion:
+        plot(x, diffusion * 100, ".", color="r", ms=0.5, alpha=0.2)
+        errorbar(
+            x_bin,
+            alpha_diff_bin * 100,
+            yerr=alpha_diff_sigma_bin * 100,
+            fmt=".",
+            ms=8.0,
+            color="b",
+            lw=1.2,
+            label="Diffusion (100x)",
+        )
+
+    if plot_viscosity:
+        plot(x, viscosity, ".", color="g", ms=0.5, alpha=0.2)
+        errorbar(
+            x_bin,
+            alpha_visc_bin,
+            yerr=alpha_visc_sigma_bin,
+            fmt=".",
+            ms=8.0,
+            color="y",
+            lw=1.2,
+            label="Viscosity",
+        )
+
+    ylabel("${\\rm{Rate~Coefficient}}~\\alpha$", labelpad=0)
+    legend()
+else:
+    plot(x, S, ".", color="r", ms=0.5, alpha=0.2)
+    plot(x_s, s_s, "--", color="k", alpha=0.8, lw=1.2)
+    errorbar(x_bin, S_bin, yerr=S_sigma_bin, fmt=".", ms=8.0, color="b", lw=1.2)
+    ylabel("${\\rm{Entropy}}~S$", labelpad=0)
+    ylim(0.8, 3.8)
+
+# Information -------------------------------------
+subplot(236, frameon=False)
+
+text_fontsize = 5
+
+text(
+    -0.49,
+    0.9,
+    "Sod shock with  $\\gamma=%.3f$ in 3D at $t=%.2f$" % (gas_gamma, time),
+    fontsize=text_fontsize,
+)
+text(
+    -0.49,
+    0.8,
+    "Left: $(P_L, \\rho_L, v_L) = (%.3f, %.3f, %.3f)$" % (P_L, rho_L, v_L),
+    fontsize=text_fontsize,
+)
+text(
+    -0.49,
+    0.7,
+    "Right: $(P_R, \\rho_R, v_R) = (%.3f, %.3f, %.3f)$" % (P_R, rho_R, v_R),
+    fontsize=text_fontsize,
+)
+plot([-0.49, 0.1], [0.62, 0.62], "k-", lw=1)
+text(-0.49, 0.5, "SWIFT %s" % git, fontsize=text_fontsize)
+text(-0.49, 0.4, scheme, fontsize=text_fontsize)
+text(-0.49, 0.3, kernel, fontsize=text_fontsize)
+text(
+    -0.49,
+    0.2,
+    "$%.2f$ neighbours ($\\eta=%.3f$)" % (neighbours, eta),
+    fontsize=text_fontsize,
+)
+xlim(-0.5, 0.5)
+ylim(0, 1)
+xticks([])
+yticks([])
+
+tight_layout()
+savefig("SodShock.png", dpi=200)
diff --git a/examples/Planetary/SodShock_3D/run.sh b/examples/Planetary/SodShock_3D/run.sh
index 652d760b3023354fa1179a8bc4985664e2c132cc..94199402df1679731c7319ec68d78b15425e74af 100755
--- a/examples/Planetary/SodShock_3D/run.sh
+++ b/examples/Planetary/SodShock_3D/run.sh
@@ -15,4 +15,4 @@ fi
 # Run SWIFT
 ../../../swift --hydro --threads=4 sodShock.yml 2>&1 | tee output.log
 
-python3 ../../HydroTests/SodShock_3D/plotSolution.py 1
+python3 ./plotSolution.py 1
diff --git a/examples/Planetary/SodShock_3D/sodShock.yml b/examples/Planetary/SodShock_3D/sodShock.yml
index 6e7536a82dd97a6356d5da6fd614641dc0cca7a2..e2ab2a2673a57f790ffd664c01a6d741d5a1b4a4 100644
--- a/examples/Planetary/SodShock_3D/sodShock.yml
+++ b/examples/Planetary/SodShock_3D/sodShock.yml
@@ -26,10 +26,9 @@ Statistics:
 
 # Parameters for the hydrodynamics scheme
 SPH:
-  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  resolution_eta:        1.487    # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
   CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
-  viscosity_alpha:       0.8      # Override for the initial value of the artificial viscosity. In schemes that have a fixed AV, this remains as alpha throughout the run.
-
+ 
 # Parameters related to the initial conditions
 InitialConditions:
   file_name:  ./sodShock.hdf5       # The file to read
diff --git a/examples/Planetary/SquareTest_2D/README.md b/examples/Planetary/SquareTest_2D/README.md
deleted file mode 100644
index 69d33bbaa4e9b9af7d27c47d6a5c7ad79d4ec9c4..0000000000000000000000000000000000000000
--- a/examples/Planetary/SquareTest_2D/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-Square Test 2D (Planetary)
-==============
-
-This is a copy of `/examples/HydroTests/SquareTest_2D` for testing the Planetary 
-hydro scheme with the planetary ideal gas equation of state. 
-
-This is a very challenging test that aims to figure out
-if contact discontinuities are properly handled. If there
-is residual surface tension, then the square will quickly
-become a sphere. Otherwise, it will remain a square. For
-more information see Hopkins' 2013 and 2015 papers.
-
-There are two initial condition generation files present.
-For the SWIFT method of finding an un-mass weighted number
-of particles in the kernel, it makes more sense to have
-different mass particles (makeICDifferentMasses.py). For
-comparison to previous methods, we also provide a script
-that creates initial conditions with a different density
-of particles, all with equal masses, in the square and
-outside of the square.
-
-If you do not have the swiftsimio library, you can use
-the plotSolutionLegacy.py to plot the solution.
-
-The results should be highly similar to the Minimal hydro scheme, though 
-slightly different because of the higher viscosity beta used here. To recover 
-the Minimal scheme behaviour, edit `const_viscosity_beta` from 4 to 3 in 
-`src/hydro/Planetary/hydro_parameters.h`.
-
-This requires the code to be configured to use the planetary hydrodynamics 
-scheme and equations of state: 
-`--with-hydro=planetary --with-equation-of-state=planetary --with-hydro-dimension=2`
diff --git a/examples/Planetary/SquareTest_2D/makeIC.py b/examples/Planetary/SquareTest_2D/makeIC.py
deleted file mode 100644
index 6798050a225dd839cb50d11d6ea714c4baf9529a..0000000000000000000000000000000000000000
--- a/examples/Planetary/SquareTest_2D/makeIC.py
+++ /dev/null
@@ -1,128 +0,0 @@
-###############################################################################
-# This file is part of SWIFT.
-# Copyright (c) 2016 Matthieu Schaller (schaller@strw.leidenuniv.nl)
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##############################################################################
-
-import h5py
-from numpy import *
-
-# Generates a swift IC file for the Square test in a periodic box
-
-# Parameters
-L = 64  # Number of particles on the side
-gamma = 5.0 / 3.0  # Gas adiabatic index
-rho0 = 4.0  # Gas central density
-rho1 = 1.0  # Gas outskirt density
-P0 = 2.5  # Gas central pressure
-P1 = 2.5  # Gas central pressure
-vx = 0.0  # Random velocity for all particles
-vy = 0.0
-fileOutputName = "square.hdf5"
-# ---------------------------------------------------
-
-vol = 1.0
-
-numPart_out = L * L
-numPart_in = int(L * L * rho0 / rho1 / 4)
-
-L_out = int(sqrt(numPart_out))
-L_in = int(sqrt(numPart_in))
-
-pos_out = zeros((numPart_out, 3))
-for i in range(L_out):
-    for j in range(L_out):
-        index = i * L_out + j
-        pos_out[index, 0] = i / (float(L_out)) + 1.0 / (2.0 * L_out)
-        pos_out[index, 1] = j / (float(L_out)) + 1.0 / (2.0 * L_out)
-h_out = ones(numPart_out) * (1.0 / L_out) * 1.2348
-m_out = ones(numPart_out) * vol * rho1 / numPart_out
-u_out = ones(numPart_out) * P1 / (rho1 * (gamma - 1.0))
-
-pos_in = zeros((numPart_in, 3))
-for i in range(L_in):
-    for j in range(L_in):
-        index = i * L_in + j
-        pos_in[index, 0] = 0.25 + i / float(2.0 * L_in) + 1.0 / (2.0 * 2.0 * L_in)
-        pos_in[index, 1] = 0.25 + j / float(2.0 * L_in) + 1.0 / (2.0 * 2.0 * L_in)
-h_in = ones(numPart_in) * (1.0 / L_in) * 1.2348
-m_in = ones(numPart_in) * 0.25 * vol * rho0 / numPart_in
-u_in = ones(numPart_in) * P0 / (rho0 * (gamma - 1.0))
-
-# Remove the central particles
-select_out = logical_or(
-    logical_or(pos_out[:, 0] < 0.25, pos_out[:, 0] > 0.75),
-    logical_or(pos_out[:, 1] < 0.25, pos_out[:, 1] > 0.75),
-)
-pos_out = pos_out[select_out, :]
-h_out = h_out[select_out]
-u_out = u_out[select_out]
-m_out = m_out[select_out]
-
-# Add the central region
-pos = append(pos_out, pos_in, axis=0)
-h = append(h_out, h_in, axis=0)
-u = append(u_out, u_in)
-m = append(m_out, m_in)
-numPart = size(h)
-ids = linspace(1, numPart, numPart)
-vel = zeros((numPart, 3))
-vel[:, 0] = vx
-vel[:, 1] = vy
-mat = zeros(numPart)
-
-
-# File
-fileOutput = h5py.File(fileOutputName, "w")
-
-# Header
-grp = fileOutput.create_group("/Header")
-grp.attrs["BoxSize"] = [vol, vol, 0.2]
-grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["Time"] = 0.0
-grp.attrs["NumFilesPerSnapshot"] = 1
-grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
-grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["Dimension"] = 2
-
-# Units
-grp = fileOutput.create_group("/Units")
-grp.attrs["Unit length in cgs (U_L)"] = 1.0
-grp.attrs["Unit mass in cgs (U_M)"] = 1.0
-grp.attrs["Unit time in cgs (U_t)"] = 1.0
-grp.attrs["Unit current in cgs (U_I)"] = 1.0
-grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
-
-# Particle group
-grp = fileOutput.create_group("/PartType0")
-ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
-ds[()] = pos
-ds = grp.create_dataset("Velocities", (numPart, 3), "f")
-ds[()] = vel
-ds = grp.create_dataset("Masses", (numPart, 1), "f")
-ds[()] = m.reshape((numPart, 1))
-ds = grp.create_dataset("SmoothingLengths", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergies", (numPart, 1), "f")
-ds[()] = u.reshape((numPart, 1))
-ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
-ds[()] = ids.reshape((numPart, 1))
-ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
-ds[()] = mat.reshape((numPart, 1))
-
-fileOutput.close()
diff --git a/examples/Planetary/SquareTest_2D/makeICDifferentMasses.py b/examples/Planetary/SquareTest_2D/makeICDifferentMasses.py
deleted file mode 100644
index 63dd2bf91438fa1d3de59e7ed8e915cfa2ac16ce..0000000000000000000000000000000000000000
--- a/examples/Planetary/SquareTest_2D/makeICDifferentMasses.py
+++ /dev/null
@@ -1,111 +0,0 @@
-###############################################################################
-# This file is part of SWIFT.
-# Copyright (c) 2019 Josh Borrow (joshua.borrow@durham.ac.uk)
-#               2016 Matthieu Schaller (schaller@strw.leidenuniv.nl)
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##############################################################################
-
-import h5py
-from numpy import *
-
-# Generates a swift IC file for the Square test in a periodic box
-
-# Parameters
-L = 2 * 64  # Number of particles on the side
-gamma = 5.0 / 3.0  # Gas adiabatic index
-rho0 = 4.0  # Gas central density
-rho1 = 1.0  # Gas outskirt density
-P0 = 2.5  # Gas central pressure
-P1 = 2.5  # Gas central pressure
-vx = 0.0  # Random velocity for all particles
-vy = 0.0
-fileOutputName = "square.hdf5"
-# ---------------------------------------------------
-
-vol = 1.0
-
-numPart = L * L
-
-pos_x = arange(0, 1, 1.0 / L)
-xv, yv = meshgrid(pos_x, pos_x)
-pos = zeros((numPart, 3), dtype=float)
-pos[:, 0] = xv.flatten()
-pos[:, 1] = yv.flatten()
-
-# Now we can get 2d masks!
-inside = logical_and.reduce([xv < 0.75, xv > 0.25, yv < 0.75, yv > 0.25])
-
-mass_in = rho0 / numPart
-mass_out = rho1 / numPart
-
-m = ones_like(xv) * mass_out
-m[inside] = mass_in
-m = m.flatten()
-
-h = ones_like(m) / L
-
-u_in = P0 / ((gamma - 1) * rho0)
-u_out = P1 / ((gamma - 1) * rho1)
-u = ones_like(xv) * u_out
-u[inside] = u_in
-u = u.flatten()
-vel = zeros_like(pos)
-ids = arange(numPart)
-mat = zeros(numPart)
-
-
-# File
-fileOutput = h5py.File(fileOutputName, "w")
-
-# Header
-grp = fileOutput.create_group("/Header")
-grp.attrs["BoxSize"] = [vol, vol, 0.2]
-grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
-grp.attrs["Time"] = 0.0
-grp.attrs["NumFilesPerSnapshot"] = 1
-grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
-grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
-grp.attrs["Dimension"] = 2
-
-# Units
-grp = fileOutput.create_group("/Units")
-grp.attrs["Unit length in cgs (U_L)"] = 1.0
-grp.attrs["Unit mass in cgs (U_M)"] = 1.0
-grp.attrs["Unit time in cgs (U_t)"] = 1.0
-grp.attrs["Unit current in cgs (U_I)"] = 1.0
-grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
-
-# Particle group
-grp = fileOutput.create_group("/PartType0")
-ds = grp.create_dataset("Coordinates", (numPart, 3), "d")
-ds[()] = pos
-ds = grp.create_dataset("Velocities", (numPart, 3), "f")
-ds[()] = vel
-ds = grp.create_dataset("Masses", (numPart, 1), "f")
-ds[()] = m.reshape((numPart, 1))
-ds = grp.create_dataset("SmoothingLengths", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergies", (numPart, 1), "f")
-ds[()] = u.reshape((numPart, 1))
-ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
-ds[()] = ids.reshape((numPart, 1))
-ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
-ds[()] = mat.reshape((numPart, 1))
-
-
-fileOutput.close()
diff --git a/examples/Planetary/SquareTest_2D/run.sh b/examples/Planetary/SquareTest_2D/run.sh
deleted file mode 100755
index c62022fef2ae34ab8331b31a192538bf8a48f0ac..0000000000000000000000000000000000000000
--- a/examples/Planetary/SquareTest_2D/run.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/bash
-
- # Generate the initial conditions if they are not present.
-if [ ! -e square.hdf5 ]
-then
-    echo "Generating initial conditions for the square test ..."
-    python3 makeICDifferentMasses.py
-fi
-
-# Run SWIFT
-../../../swift --hydro --threads=4 square.yml 2>&1 | tee output.log
-
-# Plot the solution
-python3 ../../HydroTests/SquareTest_2D/plotSolution.py 40
-python3 ../../HydroTests/SquareTest_2D/makeMovie.py
diff --git a/examples/Planetary/SquareTest_3D/README.md b/examples/Planetary/SquareTest_3D/README.md
new file mode 100755
index 0000000000000000000000000000000000000000..9f16ddeb92b042e92306122c3498907e74f9e337
--- /dev/null
+++ b/examples/Planetary/SquareTest_3D/README.md
@@ -0,0 +1,26 @@
+Square Test 3D
+--------------
+
+This is a 3D version of the "square test", consisting of a cube of high-density
+material in pressure equilibrium with a surrounding low-density region. These
+initial conditions are those used to produce the simulations presented by
+Sandnes et al. (2025), section 4.1.
+
+This test is used to investigate spurious surface tension-like effects from
+sharp discontinuities in a system that should be in static equilibrium. There
+are two initial condition generation files to test both an equal-spacing
+scenario, i.e., with different particle masses in the two regions, and an
+equal-mass scenario. The significant contributions from both smoothing error
+and discretisation error at the density discontinuity make the equal-mass test
+particularly challenging for SPH.
+
+Note that the default resolution_eta parameter is consistent with the use of a
+Wendland C2 kernel with ~100 neighbours.
+
+Some examples of configuration options with different hydro schemes:
+
+REMIX:
+`--with-hydro=remix --with-equation-of-state=planetary --with-kernel=wendland-C2`
+
+"Traditional" SPH (tSPH):
+`--with-hydro=planetary --with-equation-of-state=planetary --with-kernel=wendland-C2`
diff --git a/examples/Planetary/SquareTest_3D/makeICEqualMass.py b/examples/Planetary/SquareTest_3D/makeICEqualMass.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a584d1cd1e9eba360e06276cd83b1b35c95c516
--- /dev/null
+++ b/examples/Planetary/SquareTest_3D/makeICEqualMass.py
@@ -0,0 +1,136 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#               2019 Josh Borrow (joshua.borrow@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""Make initial conditions for the 3D square test with equal particle mass."""
+
+import h5py
+import numpy as np
+
+# Parameters
+N_l = 40  # Number of particles on one side
+gamma = 5.0 / 3.0  # Gas adiabatic index
+rho_in_approx = 16 ** 3 / 10 ** 3  # Density of inner region
+rho_out = 1.0  # Density of outer region
+P_in = 2.5  # Pressure of inner region
+P_out = 2.5  # Pressure of outer region
+fileOutputName = "square_equal_mass.hdf5"
+
+vol = 1.0
+cube_vol_factor = 1 / 8
+numPart_out = N_l * N_l * N_l
+numPart_in_approx = N_l * N_l * N_l * cube_vol_factor * rho_in_approx / rho_out
+
+# Calculate the number of particles on one side of relevant region
+N_l_out = int(np.cbrt(numPart_out))
+N_l_in = int(np.cbrt(numPart_in_approx))
+numPart_in = int(N_l_in ** 3)
+
+# Set up outer region (cube not yet removed from this lattice)
+pos_out = np.zeros((numPart_out, 3))
+for i in range(N_l_out):
+    for j in range(N_l_out):
+        for k in range(N_l_out):
+            index = i * N_l_out * N_l_out + j * N_l_out + k
+            pos_out[index, 0] = i / (float(N_l_out)) + 1.0 / (2.0 * N_l_out)
+            pos_out[index, 1] = j / (float(N_l_out)) + 1.0 / (2.0 * N_l_out)
+            pos_out[index, 2] = k / (float(N_l_out)) + 1.0 / (2.0 * N_l_out)
+
+h_out = np.ones(numPart_out) * (1.0 / N_l_out)
+m_out = np.ones(numPart_out) * vol * rho_out / numPart_out
+u_out = np.ones(numPart_out) * P_out / (rho_out * (gamma - 1.0))
+rho_out = np.ones(numPart_out) * rho_out
+
+# Set up inner region
+rho_in = m_out[0] * numPart_in / cube_vol_factor
+pos_in = np.zeros((numPart_in, 3))
+for i in range(N_l_in):
+    for j in range(N_l_in):
+        for k in range(N_l_in):
+            index = i * N_l_in * N_l_in + j * N_l_in + k
+            pos_in[index, 0] = 0.25 + i / float(2 * N_l_in) + 1.0 / (2.0 * 2 * N_l_in)
+            pos_in[index, 1] = 0.25 + j / float(2 * N_l_in) + 1.0 / (2.0 * 2 * N_l_in)
+            pos_in[index, 2] = 0.25 + k / float(2 * N_l_in) + 1.0 / (2.0 * 2 * N_l_in)
+
+h_in = np.ones(numPart_in) * (1.0 / N_l_in)
+m_in = np.ones(numPart_in) * m_out[0]
+u_in = np.ones(numPart_in) * P_in / (rho_in * (gamma - 1.0))
+rho_in = np.ones(numPart_in) * rho_in
+
+# Remove the particles within the central cube from the outer region
+mask_out = np.logical_or.reduce(
+    (
+        pos_out[:, 0] < 0.25,
+        pos_out[:, 0] > 0.75,
+        pos_out[:, 1] < 0.25,
+        pos_out[:, 1] > 0.75,
+        pos_out[:, 2] < 0.25,
+        pos_out[:, 2] > 0.75,
+    )
+)
+pos_out = pos_out[mask_out, :]
+h_out = h_out[mask_out]
+u_out = u_out[mask_out]
+m_out = m_out[mask_out]
+rho_out = rho_out[mask_out]
+
+# Combine inner and outer regions
+pos = np.append(pos_out, pos_in, axis=0)
+h = np.append(h_out, h_in, axis=0)
+u = np.append(u_out, u_in)
+m = np.append(m_out, m_in)
+rho = np.append(rho_out, rho_in)
+numPart = np.size(h)
+vel = np.zeros((numPart, 3))
+ids = np.linspace(1, numPart, numPart)
+mat = np.zeros(numPart)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [vol, vol, vol]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFilesPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    grp.create_dataset("Coordinates", data=pos, dtype="d")
+    grp.create_dataset("Velocities", data=vel, dtype="f")
+    grp.create_dataset("Masses", data=m.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("Density", data=rho.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("SmoothingLength", data=h.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("InternalEnergy", data=u.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("ParticleIDs", data=ids.reshape((numPart, 1)), dtype="L")
+    grp.create_dataset("MaterialIDs", data=mat.reshape((numPart, 1)), dtype="i")
diff --git a/examples/Planetary/SquareTest_3D/makeICEqualSpacing.py b/examples/Planetary/SquareTest_3D/makeICEqualSpacing.py
new file mode 100644
index 0000000000000000000000000000000000000000..d148b18efc719fe5ae1ecebe545f738994caa99d
--- /dev/null
+++ b/examples/Planetary/SquareTest_3D/makeICEqualSpacing.py
@@ -0,0 +1,121 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#               2019 Josh Borrow (joshua.borrow@durham.ac.uk)
+#               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""Make initial conditions for the 3D square test with equal particle spacing."""
+
+import h5py
+import numpy as np
+
+# Parameters
+N_l = 40  # Number of particles on one side
+gamma = 5.0 / 3.0  # Gas adiabatic index
+rho_in = 4.0  # Density of inner region
+rho_out = 1.0  # Density of outer region
+P_in = 2.5  # Pressure of inner region
+P_out = 2.5  # Pressure of outer region
+fileOutputName = "square_equal_spacing.hdf5"
+
+vol = 1.0
+numPart = N_l * N_l * N_l
+
+# Set particle masses
+A1_pos_l = np.arange(0, 1, 1.0 / N_l)
+A3_pos_x, A3_pos_y, A3_pos_z = np.meshgrid(A1_pos_l, A1_pos_l, A1_pos_l)
+pos = np.zeros((numPart, 3), dtype=float)
+pos[:, 0] = A3_pos_x.flatten()
+pos[:, 1] = A3_pos_y.flatten()
+pos[:, 2] = A3_pos_z.flatten()
+
+# 3d mask
+mask_inside = np.logical_and.reduce(
+    [
+        A3_pos_x < 0.75,
+        A3_pos_x > 0.25,
+        A3_pos_y < 0.75,
+        A3_pos_y > 0.25,
+        A3_pos_z < 0.75,
+        A3_pos_z > 0.25,
+    ]
+)
+
+# Set particle masses
+mass_in = rho_in * vol / numPart
+mass_out = rho_out * vol / numPart
+m = np.ones_like(A3_pos_x) * mass_out
+m[mask_inside] = mass_in
+m = m.flatten()
+
+# Set approximate particle smoothing lengths
+h = np.ones_like(m) / N_l
+
+# Set particle specific internal energies
+u_in = P_in / ((gamma - 1) * rho_in)
+u_out = P_out / ((gamma - 1) * rho_out)
+u = np.ones_like(A3_pos_x) * u_out
+u[mask_inside] = u_in
+u = u.flatten()
+
+# Set particle densities
+rho = np.ones_like(A3_pos_x) * rho_out
+rho[mask_inside] = rho_in
+rho = rho.flatten()
+
+# Set particle velocities
+vel = np.zeros_like(pos)
+
+# Set particle IDs
+ids = np.arange(numPart)
+
+# Set particle material IDs
+mat = np.zeros(numPart)
+
+# Write ICs to file
+with h5py.File(fileOutputName, "w") as f:
+    # Header
+    grp = f.create_group("/Header")
+    grp.attrs["BoxSize"] = [vol, vol, vol]
+    grp.attrs["NumPart_Total"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [numPart, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFilesPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["Dimension"] = 3
+
+    # Units
+    grp = f.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = f.create_group("/PartType0")
+    grp.create_dataset("Coordinates", data=pos, dtype="d")
+    grp.create_dataset("Velocities", data=vel, dtype="f")
+    grp.create_dataset("Masses", data=m.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("Density", data=rho.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("SmoothingLength", data=h.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("InternalEnergy", data=u.reshape((numPart, 1)), dtype="f")
+    grp.create_dataset("ParticleIDs", data=ids.reshape((numPart, 1)), dtype="L")
+    grp.create_dataset("MaterialIDs", data=mat.reshape((numPart, 1)), dtype="i")
diff --git a/examples/Planetary/SquareTest_3D/plotSolution.py b/examples/Planetary/SquareTest_3D/plotSolution.py
new file mode 100644
index 0000000000000000000000000000000000000000..856c2b6b884b8582e30a1b72c8ed3f3cd4ebddbd
--- /dev/null
+++ b/examples/Planetary/SquareTest_3D/plotSolution.py
@@ -0,0 +1,155 @@
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2025 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+#               2025 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""Plot a central slice from a 3D square test snapshot.
+
+Parameters
+----------
+type : str
+    Either "equal_spacing" or "equal_mass".
+
+snap : int
+    The snapshot ID to plot.
+"""
+
+import sys
+import h5py
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def plot_square(A1_x, A1_y, A1_rho, A1_u, A1_P, A1_size, boxsize_l):
+    # Use Gridspec to set up figure
+    n_gs_ax = 40
+    n_gs_ax_gap = 10
+    n_gs_cbar_gap = 2
+    n_gs_cbar = 2
+
+    ax_len = 5
+    ax_gap_len = n_gs_ax_gap * ax_len / n_gs_ax
+    cbar_gap_len = n_gs_cbar_gap * ax_len / n_gs_ax
+    cbar_len = n_gs_cbar * ax_len / n_gs_ax
+
+    fig = plt.figure(
+        figsize=(3 * ax_len + 2 * ax_gap_len + 3 * cbar_gap_len + 3 * cbar_len, ax_len)
+    )
+    gs = mpl.gridspec.GridSpec(
+        nrows=n_gs_ax,
+        ncols=3 * n_gs_ax + 2 * n_gs_ax_gap + 3 * n_gs_cbar_gap + 3 * n_gs_cbar,
+    )
+
+    # Quantities to plot
+    plot_quantity = ["Density", "Internal Energy", "Pressure"]
+    plot_vectors = [A1_rho, A1_u, A1_P]
+    cmaps = ["Spectral_r", "inferno", "viridis"]
+
+    for i in range(3):
+        # Set up subfig and color bar axes
+        y0 = 0
+        y1 = n_gs_ax
+        x0 = i * (n_gs_ax + n_gs_cbar_gap + n_gs_cbar + n_gs_ax_gap)
+        x1 = x0 + n_gs_ax
+        ax = plt.subplot(gs[y0:y1, x0:x1])
+        x0 = x1 + n_gs_cbar_gap
+        x1 = x0 + n_gs_cbar
+        cax = plt.subplot(gs[y0:y1, x0:x1])
+
+        # Colour map
+        cmap = plt.get_cmap(cmaps[i])
+        norm = mpl.colors.Normalize(
+            vmin=np.min(plot_vectors[i]), vmax=np.max(plot_vectors[i])
+        )
+
+        # Plot
+        scatter = ax.scatter(
+            A1_x,
+            A1_y,
+            c=plot_vectors[i],
+            norm=norm,
+            cmap=cmap,
+            s=A1_size,
+            edgecolors="none",
+        )
+        ax.set_xticks([])
+        ax.set_yticks([])
+        ax.set_facecolor((0.9, 0.9, 0.9))
+        ax.set_xlim((0, boxsize_l))
+        ax.set_ylim((0, boxsize_l))
+
+        # Colour bar
+        cbar = plt.colorbar(scatter, cax)
+        cbar.ax.tick_params(labelsize=14)
+        cbar.set_label(plot_quantity[i], rotation=90, labelpad=8, fontsize=18)
+
+    plt.savefig("square_%s_%04d.png" % (type, snap), dpi=300, bbox_inches="tight")
+
+
+if __name__ == "__main__":
+    # Load snapshot data
+    mpl.use("Agg")
+    type = sys.argv[1]
+    snap = int(sys.argv[2])
+    assert type in ["equal_spacing", "equal_mass"]
+    snap_file = "square_%s_%04d.hdf5" % (type, snap)
+
+    with h5py.File(snap_file, "r") as f:
+        boxsize_l = f["Header"].attrs["BoxSize"][0]
+        A1_x = f["/PartType0/Coordinates"][:, 0]
+        A1_y = f["/PartType0/Coordinates"][:, 1]
+        A1_z = f["/PartType0/Coordinates"][:, 2]
+        A1_rho = f["/PartType0/Densities"][:]
+        A1_u = f["/PartType0/InternalEnergies"][:]
+        A1_P = f["/PartType0/Pressures"][:]
+        A1_m = f["/PartType0/Masses"][:]
+
+    # Sort arrays based on z position
+    sort_indices = np.argsort(A1_z)
+    A1_x = A1_x[sort_indices]
+    A1_y = A1_y[sort_indices]
+    A1_z = A1_z[sort_indices]
+    A1_rho = A1_rho[sort_indices]
+    A1_u = A1_u[sort_indices]
+    A1_P = A1_P[sort_indices]
+    A1_m = A1_m[sort_indices]
+
+    # Mask to select slice
+    slice_thickness = 0.1
+    slice_pos_z = 0.5 * (np.max(A1_z) + np.min(A1_z))
+    mask_slice = np.logical_and(
+        A1_z > slice_pos_z - 0.5 * slice_thickness,
+        A1_z < slice_pos_z + 0.5 * slice_thickness,
+    )
+
+    # Select particles to plot
+    A1_x_slice = A1_x[mask_slice]
+    A1_y_slice = A1_y[mask_slice]
+    A1_rho_slice = A1_rho[mask_slice]
+    A1_u_slice = A1_u[mask_slice]
+    A1_P_slice = A1_P[mask_slice]
+    A1_m_slice = A1_m[mask_slice]
+
+    # Size of plotted particles
+    size_factor = 5e4
+    A1_size = size_factor * (A1_m_slice / A1_rho_slice) ** (2 / 3)
+
+    # Plot figure
+    plot_square(
+        A1_x_slice, A1_y_slice, A1_rho_slice, A1_u_slice, A1_P_slice, A1_size, boxsize_l
+    )
diff --git a/examples/Planetary/SquareTest_3D/run.sh b/examples/Planetary/SquareTest_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5fc4c0f0b649def1dcc804086494764f41561a56
--- /dev/null
+++ b/examples/Planetary/SquareTest_3D/run.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e square_equal_spacing.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D square test (equal spacing)..."
+    python3 makeICEqualSpacing.py
+fi
+if [ ! -e square_equal_mass.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D square test (equal mass)..."
+    python3 makeICEqualMass.py
+fi
+
+# Run SWIFT
+../../../swift --hydro --threads=4 square_equal_spacing.yml 2>&1 | tee output_equal_spacing.log
+../../../swift --hydro --threads=4 square_equal_mass.yml 2>&1 | tee output_equal_mass.log
+
+# Plot the solutions
+python3 ./plotSolution.py "equal_spacing" 20
+python3 ./plotSolution.py "equal_mass" 20
diff --git a/examples/Planetary/SquareTest_3D/square_equal_mass.yml b/examples/Planetary/SquareTest_3D/square_equal_mass.yml
new file mode 100644
index 0000000000000000000000000000000000000000..280f31d83099d015e0133fdf119815f35d30fbf3
--- /dev/null
+++ b/examples/Planetary/SquareTest_3D/square_equal_mass.yml
@@ -0,0 +1,38 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1            # Grams
+  UnitLength_in_cgs:   1            # Centimeters
+  UnitVelocity_in_cgs: 1            # Centimeters per second
+  UnitCurrent_in_cgs:  1            # Amperes
+  UnitTemp_in_cgs:     1            # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.                    # The starting time of the simulation (in internal units).
+  time_end:   4.                    # The end time of the simulation (in internal units).
+  dt_min:     1e-6                  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-2                  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            square_equal_mass  # Common part of the name of output files
+  time_first:          0.           # Time of the first output (in internal units)
+  delta_time:          2e-1         # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          2e-1         # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:      1.487        # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:       0.1          # Courant-Friedrich-Levy condition for time integration.
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./square_equal_mass.hdf5  # The file to read
+  periodic:   1                     # Are we running with periodic ICs?
+
+# Parameters related to the equation of state
+EoS:
+    planetary_use_idg_def:    1     # Default ideal gas, material ID 0
diff --git a/examples/Planetary/SquareTest_3D/square_equal_spacing.yml b/examples/Planetary/SquareTest_3D/square_equal_spacing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..78e8ddd0d83e1aed73b8decec11bc3b33adbdec9
--- /dev/null
+++ b/examples/Planetary/SquareTest_3D/square_equal_spacing.yml
@@ -0,0 +1,38 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1            # Grams
+  UnitLength_in_cgs:   1            # Centimeters
+  UnitVelocity_in_cgs: 1            # Centimeters per second
+  UnitCurrent_in_cgs:  1            # Amperes
+  UnitTemp_in_cgs:     1            # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.                    # The starting time of the simulation (in internal units).
+  time_end:   4.                    # The end time of the simulation (in internal units).
+  dt_min:     1e-6                  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-2                  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            square_equal_spacing  # Common part of the name of output files
+  time_first:          0.           # Time of the first output (in internal units)
+  delta_time:          2e-1         # Time difference between consecutive outputs (in internal units)
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:          2e-1         # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:      1.487        # Target smoothing length in units of the mean inter-particle separation (1.487 == 100Ngbs with the Wendland C2 kernel).
+  CFL_condition:       0.1          # Courant-Friedrich-Levy condition for time integration.
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./square_equal_spacing.hdf5  # The file to read
+  periodic:   1                     # Are we running with periodic ICs?
+
+# Parameters related to the equation of state
+EoS:
+    planetary_use_idg_def:    1     # Default ideal gas, material ID 0
diff --git a/examples/RadiativeTransferTests/AdvectionDifferentTimeStepSizes_1D/plotSolution.py b/examples/RadiativeTransferTests/AdvectionDifferentTimeStepSizes_1D/plotSolution.py
index fb6a962976e3e94f1fa37fb9dbe88d889f6eabd8..6485c420bca090c037491714c426ff07458e5dfd 100755
--- a/examples/RadiativeTransferTests/AdvectionDifferentTimeStepSizes_1D/plotSolution.py
+++ b/examples/RadiativeTransferTests/AdvectionDifferentTimeStepSizes_1D/plotSolution.py
@@ -69,7 +69,7 @@ except IndexError:
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -98,9 +98,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -112,7 +112,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8"))
 
     boxsize = meta.boxsize[0]
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
 
     for g in range(ngroups):
         # workaround to access named columns data with swiftsimio visualisaiton
diff --git a/examples/RadiativeTransferTests/Advection_1D/README b/examples/RadiativeTransferTests/Advection_1D/README
index 13dd7ce4caeeb00bd17b4998dffdbd92eacaee9c..9c14089e4b81c8acd2c3b8aa68ba56ad90b93da8 100644
--- a/examples/RadiativeTransferTests/Advection_1D/README
+++ b/examples/RadiativeTransferTests/Advection_1D/README
@@ -14,10 +14,14 @@ There are no stars to act as sources. Also make sure that you choose your
 photon frequencies in a way that doesn't interact with gas!
 
 The ICs are created to be compatible with GEAR_RT and SPHM1RT. Recommended configuration:
-GEAR_RT:
+GEAR_RT (with gizmo-mfv solver):
     --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro-dimension=1 --with-hydro=gizmo-mfv \
      --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
 
+GEAR_RT (with sphenix SPH solver):
+    --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro-dimension=1 --with-hydro=sphenix  
+     --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
+
 SPHM1RT:
     --with-rt=SPHM1RT_4 --with-hydro-dimension=1 --with-stars=basic  --with-sundials=$SUNDIALS_ROOT
     
diff --git a/examples/RadiativeTransferTests/Advection_1D/plotSolution.py b/examples/RadiativeTransferTests/Advection_1D/plotSolution.py
index 47e94a669a6502a16daa0b0345e2944c1f1093b6..99eae9b20fe0278fd8636804242f43fa8d7184ce 100755
--- a/examples/RadiativeTransferTests/Advection_1D/plotSolution.py
+++ b/examples/RadiativeTransferTests/Advection_1D/plotSolution.py
@@ -70,7 +70,7 @@ except IndexError:
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -99,9 +99,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -114,7 +114,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
 
     boxsize = meta.boxsize[0]
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     # Currently, SPHM1RT only works for frequency group = 4 in the code
     # However, we only plot 3 frequency groups here, so
     # we set ngroups = 3:
@@ -319,7 +319,7 @@ def get_minmax_vals(snaplist):
         data = swiftsimio.load(filename)
         meta = data.metadata
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml b/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml
index a528dbeb1624e60679f23aecbde2de7a8376c296..e7bece757404d0bbd082e2de3622443a3e26828c 100644
--- a/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml
+++ b/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml
@@ -28,6 +28,9 @@ Statistics:
   time_first:          0.
   delta_time:          4.e-2 # Time between statistics output
 
+Scheduler:
+  tasks_per_cell:      100
+
 # Parameters for the hydrodynamics scheme
 SPH:
   resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
diff --git a/examples/RadiativeTransferTests/Advection_1D/run.sh b/examples/RadiativeTransferTests/Advection_1D/run.sh
index 0b64a9b773f87b40c1237b91960bd09311da6c38..b277965b1be3b1e5392c31c067fae135c7191ffa 100755
--- a/examples/RadiativeTransferTests/Advection_1D/run.sh
+++ b/examples/RadiativeTransferTests/Advection_1D/run.sh
@@ -10,6 +10,7 @@ if [ ! -f advection_1D.hdf5 ]; then
 fi
 
 # Run SWIFT with RT
+# mpirun -n 4 ../../../swift_mpi \
 ../../../swift \
     --hydro \
     --threads=4 \
@@ -18,7 +19,6 @@ fi
     --stars \
     --feedback \
     --external-gravity \
-    --fpe \
     ./rt_advection1D.yml 2>&1 | tee output.log
 
 python3 ./plotSolution.py
diff --git a/examples/RadiativeTransferTests/Advection_2D/README b/examples/RadiativeTransferTests/Advection_2D/README
index 5914f149225aa447a5bf3d651cf88a408de30722..c7eba421c52b218fab397b5489ab6046b2625c80 100644
--- a/examples/RadiativeTransferTests/Advection_2D/README
+++ b/examples/RadiativeTransferTests/Advection_2D/README
@@ -16,9 +16,15 @@ There are no stars to act as sources. Also make sure that you choose your
 photon frequencies in a way that doesn't interact with gas!
 
 The ICs are created to be compatible with GEAR_RT. Recommended configuration:
+
+GEAR_RT (with gizmo-mfv solver):
     --with-rt=GEAR_4 --with-rt-riemann-solver=GLF --with-hydro-dimension=2 --with-hydro=gizmo-mfv \
      --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
 
+GEAR_RT (with sphenix SPH solver):
+    --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro-dimension=2 --with-hydro=sphenix  
+     --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
+
 SPHM1RT:
     --with-rt=SPHM1RT_4 --with-hydro-dimension=2 --with-stars=basic   --with-sundials=$SUNDIALS_ROOT
     
diff --git a/examples/RadiativeTransferTests/Advection_2D/plotSolution.py b/examples/RadiativeTransferTests/Advection_2D/plotSolution.py
index d0ebe4bf62817d989a72471246965cd461245d2e..89da6740c770b10efaaf2d60d03b4cd81bd85ade 100755
--- a/examples/RadiativeTransferTests/Advection_2D/plotSolution.py
+++ b/examples/RadiativeTransferTests/Advection_2D/plotSolution.py
@@ -60,7 +60,7 @@ mpl.rcParams["text.usetex"] = True
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -96,9 +96,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -108,7 +108,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     data = swiftsimio.load(filename)
     meta = data.metadata
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     xlabel_units_str = meta.boxsize.units.latex_representation()
 
     global imshow_kwargs
@@ -257,7 +257,7 @@ def get_minmax_vals(snaplist):
         data = swiftsimio.load(filename)
         meta = data.metadata
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/Advection_2D/plotSolutionScatter.py b/examples/RadiativeTransferTests/Advection_2D/plotSolutionScatter.py
index 3a4c37b3027720eb270283ef834f5296d9777c84..23c812f153d5854b20302bb93f8d81bf614dd5d6 100755
--- a/examples/RadiativeTransferTests/Advection_2D/plotSolutionScatter.py
+++ b/examples/RadiativeTransferTests/Advection_2D/plotSolutionScatter.py
@@ -63,7 +63,7 @@ mpl.rcParams["text.usetex"] = True
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -100,7 +100,7 @@ def plot_photons(filename):
     data = swiftsimio.load(filename)
     meta = data.metadata
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     xlabel_units_str = meta.boxsize.units.latex_representation()
     x_coordinates = data.gas.coordinates[:, 0]
 
diff --git a/examples/RadiativeTransferTests/Advection_2D/run.sh b/examples/RadiativeTransferTests/Advection_2D/run.sh
index 56851dab7bec3104c985b77dde565557dc6ec995..99cbac0f89265ec0306d11d3dbfb568a84a3a2df 100755
--- a/examples/RadiativeTransferTests/Advection_2D/run.sh
+++ b/examples/RadiativeTransferTests/Advection_2D/run.sh
@@ -17,6 +17,7 @@ then
 fi
 
 # Run SWIFT with RT
+# mpirun -n 4 ../../../swift_mpi \
 ../../../swift \
     --hydro \
     --threads=4 \
@@ -25,7 +26,6 @@ fi
     --stars \
     --feedback \
     --external-gravity \
-    --fpe \
     ./rt_advection2D.yml 2>&1 | tee output.log
 
 python3 ./plotSolution.py
diff --git a/examples/RadiativeTransferTests/CollidingBeams_1D/plotEnergies.py b/examples/RadiativeTransferTests/CollidingBeams_1D/plotEnergies.py
index 4d5285467455e775e225a02a05378060070599c0..fbcba68658be79085c4678f1b4ab4f6da3d93d86 100755
--- a/examples/RadiativeTransferTests/CollidingBeams_1D/plotEnergies.py
+++ b/examples/RadiativeTransferTests/CollidingBeams_1D/plotEnergies.py
@@ -51,7 +51,7 @@ except IndexError:
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list """
 
     snaplist = []
@@ -119,13 +119,13 @@ def get_photon_energies(snaplist):
     returns:
 
         snap_nrs : list of integers of snapshot numbers
-        energy_sums: np.array(shape=(len snaplist, ngroups)) of 
+        energy_sums: np.array(shape=(len snaplist, ngroups)) of
             total photon energies per group per snapshot
     """
 
     data = swiftsimio.load(snaplist[0])
     meta = data.metadata
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
 
     energy_sums = np.zeros((len(snaplist), ngroups))
     snap_nrs = np.zeros(len(snaplist), dtype=int)
diff --git a/examples/RadiativeTransferTests/CollidingBeams_1D/plotSolution.py b/examples/RadiativeTransferTests/CollidingBeams_1D/plotSolution.py
index 5e372d67976336086dac3579a524790242d238e8..d81a3ed3d7cc4e81823f029c75f4bc5f4ec8c5ad 100755
--- a/examples/RadiativeTransferTests/CollidingBeams_1D/plotSolution.py
+++ b/examples/RadiativeTransferTests/CollidingBeams_1D/plotSolution.py
@@ -61,7 +61,7 @@ except IndexError:
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -90,9 +90,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -103,7 +103,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     meta = data.metadata
     scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8"))
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
 
     for g in range(ngroups):
         # workaround to access named columns data with swiftsimio visualisaiton
@@ -219,7 +219,7 @@ def get_minmax_vals(snaplist):
         data = swiftsimio.load(filename)
         meta = data.metadata
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/CollidingBeams_2D/plotSolution.py b/examples/RadiativeTransferTests/CollidingBeams_2D/plotSolution.py
index 8f100e5fae99c15fb18d63b3f572db6f045d802f..e834317237080fb4e1dfbfd4a6df74183a8aa50d 100755
--- a/examples/RadiativeTransferTests/CollidingBeams_2D/plotSolution.py
+++ b/examples/RadiativeTransferTests/CollidingBeams_2D/plotSolution.py
@@ -60,7 +60,7 @@ mpl.rcParams["text.usetex"] = True
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -96,9 +96,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -108,7 +108,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     data = swiftsimio.load(filename)
     meta = data.metadata
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     xlabel_units_str = meta.boxsize.units.latex_representation()
 
     global imshow_kwargs
@@ -257,7 +257,7 @@ def get_minmax_vals(snaplist):
         data = swiftsimio.load(filename)
         meta = data.metadata
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/CoolingTest/plotSolution.py b/examples/RadiativeTransferTests/CoolingTest/plotSolution.py
index 5ea851990a6d0284839b723ccb4467da1e52a8db..dd1a511dd4a04e52139878ebe6b37e1ec28203e0 100755
--- a/examples/RadiativeTransferTests/CoolingTest/plotSolution.py
+++ b/examples/RadiativeTransferTests/CoolingTest/plotSolution.py
@@ -184,7 +184,7 @@ def get_snapshot_data(snaplist):
     firstdata = swiftsimio.load(snaplist[0])
     with_rt = True
     try:
-        ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"][0])
     except KeyError:
         # allow to read in solutions with only cooling, without RT
         with_rt = False
diff --git a/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml b/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml
index e24893955b6b0c21095c252457ea178540cc6a96..3d2ec83aac0aca09a487814dc4654145301f0eb5 100644
--- a/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml
+++ b/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml
@@ -65,6 +65,7 @@ GrackleCooling:
   self_shielding_method: 0                    # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
   primordial_chemistry: 1
   thermal_time_myr: 5
+  maximal_density_Hpcm3: -1                    # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
 
 Scheduler:
   tasks_per_cell: 128
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/README b/examples/RadiativeTransferTests/CosmoAdvection_1D/README
new file mode 100644
index 0000000000000000000000000000000000000000..44b286830c0ca8bf3d1b599582dfd5835b898ba1
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/README
@@ -0,0 +1,29 @@
+1D advection test for cosmological radiative transfer.
+
+Test that your method is TVD and the propagation speed of the photons is
+correct. The ICs set up three photon groups: 
+- The first is a top hat function initial distribution where outside values
+  are zero.
+- The second is a top hat function initial distribution where outside values
+  are nonzero. This distinction is important to test because photon energies 
+  can't be negative, so these cases need to be tested individually.
+- the third is a smooth Gaussian. 
+
+This way, you can test multiple initial condition scenarios simultaneously. 
+There are no stars to act as sources. Also make sure that you choose your
+photon frequencies in a way that doesn't interact with gas!
+
+There are 3 parameter files available:
+- rt_advection1D_low_redshift.yml for a test at very low redshift (almost no cosmological expansion)
+- rt_advection1D_medium_redshift.yml for a test at moderate redshift (z=10)
+- rt_advection1D_high_redshift.yml for a test at high redshift (z=110)
+
+Select one and give it to the run.sh script via command line argument:
+    ./run.sh rt_advection1D_[redshift_of_your_choice].yml
+
+
+The ICs are created to be compatible with GEAR_RT and SPHM1RT. Recommended configuration:
+GEAR_RT:
+    --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro-dimension=1 --with-hydro=gizmo-mfv \
+     --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
+
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/makeIC.py b/examples/RadiativeTransferTests/CosmoAdvection_1D/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..d4428ba9c7ba27b4dbfec93cebcea68355822287
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/makeIC.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2021 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2022 Tsang Keung Chan (chantsangkeung@gmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+# -----------------------------------------------------------
+# Add initial conditions for photon energies and fluxes
+# for 1D advection of photons.
+# First photon group: Top hat function with zero as the
+#       baseline.
+# Second photon group: Top hat function with nonzero value
+#       as the baseline.
+# Third photon group: Gaussian.
+# -----------------------------------------------------------
+
+import h5py
+import numpy as np
+import unyt
+from swiftsimio import Writer
+from swiftsimio.units import cosmo_units
+
+# define unit system to use
+unitsystem = cosmo_units
+
+# Box is 260 Mpc
+boxsize = 260 * unyt.Mpc
+boxsize = boxsize.to(unitsystem["length"])
+
+reduced_speed_of_light_fraction = 1.0
+
+
+# number of photon groups
+nPhotonGroups = 3
+
+# number of particles in each dimension
+n_p = 1000
+
+# filename of ICs to be generated
+outputfilename = "advection_1D.hdf5"
+
+
+def initial_condition(x, unitsystem):
+    """
+    The initial conditions that will be advected
+
+    x: particle position. 3D unyt array
+    unitsystem: Currently used unitsystem.
+
+    returns:
+    E: photon energy density for each photon group. List of scalars with size of nPhotonGroups
+    F: photon flux for each photon group. List with size of nPhotonGroups of numpy arrays of shape (3,)
+    """
+
+    # you can make the photon quantities unitless, the units will
+    # already have been written down in the writer.
+    # However, that means that you need to convert them manually.
+
+    unit_energy = (
+        unitsystem["mass"] * unitsystem["length"] ** 2 / unitsystem["time"] ** 2
+    )
+    unit_velocity = unitsystem["length"] / unitsystem["time"]
+    unit_flux = unit_energy * unit_velocity
+
+    c_internal = (unyt.c * reduced_speed_of_light_fraction).to(unit_velocity)
+
+    E_list = []
+    F_list = []
+
+    # Group 1 Photons:
+    # -------------------
+
+    if x[0] < 0.33 * boxsize:
+        E = 0.0 * unit_energy
+    elif x[0] < 0.66 * boxsize:
+        E = 1.0 * unit_energy
+    else:
+        E = 0.0 * unit_energy
+
+    # Assuming all photons flow in only one direction
+    # (optically thin regime, "free streaming limit"),
+    #  we have that |F| = c * E
+    F = np.zeros(3, dtype=np.float64)
+    F[0] = (E * c_internal).to(unit_flux)
+
+    E1 = E
+    F1 = F[0]
+
+    E_list.append(E)
+    F_list.append(F)
+
+    # Group 2 Photons:
+    # -------------------
+
+    if x[0] < 0.33 * boxsize:
+        E = 1.0 * unit_energy
+    elif x[0] < 0.66 * boxsize:
+        E = 3.0 * unit_energy
+    else:
+        E = 1.0 * unit_energy
+
+    F = np.zeros(3, dtype=np.float64)
+
+    F[0] = (E * c_internal).to(unit_flux)
+
+    E_list.append(E)
+    F_list.append(F)
+
+    E2 = E
+    F2 = F[0]
+
+    # Group 3 Photons:
+    # -------------------
+    sigma = 0.1 * boxsize
+    mean = 0.5 * boxsize
+    amplitude = 2.0
+
+    E = amplitude * np.exp(-(x[0] - mean) ** 2 / (2 * sigma ** 2)) * unit_energy
+
+    F = np.zeros(3, dtype=np.float64)
+    F[0] = (E * c_internal).to(unit_flux)
+
+    E_list.append(E)
+    F_list.append(F)
+
+    E3 = E
+    F3 = F[0]
+
+    return E_list, F_list
+
+
+if __name__ == "__main__":
+
+    xp = unyt.unyt_array(np.zeros((n_p, 3), dtype=np.float64), boxsize.units)
+
+    dx = boxsize / n_p
+
+    for i in range(n_p):
+        xp[i, 0] = (i + 0.5) * dx
+
+    w = Writer(unitsystem, boxsize, dimension=1)
+
+    w.gas.coordinates = xp
+    w.gas.velocities = np.zeros(xp.shape) * (unyt.cm / unyt.s)
+    mpart = 1e20 * unyt.M_Sun
+    mpart = mpart.to(unitsystem["mass"])
+
+    w.gas.masses = np.ones(xp.shape[0], dtype=np.float64) * mpart
+    w.gas.internal_energy = (
+        np.ones(xp.shape[0], dtype=np.float64) * (300.0 * unyt.kb * unyt.K) / unyt.g
+    )
+
+    # Generate initial guess for smoothing lengths based on MIPS
+    w.gas.generate_smoothing_lengths(boxsize=boxsize, dimension=1)
+
+    # If IDs are not present, this automatically generates
+    w.write(outputfilename)
+
+    # Now open file back up again and add photon groups
+    # you can make them unitless, the units have already been
+    # written down in the writer. In this case, it's in cgs.
+
+    F = h5py.File(outputfilename, "r+")
+    header = F["Header"]
+    nparts = header.attrs["NumPart_ThisFile"][0]
+    parts = F["/PartType0"]
+
+    for grp in range(nPhotonGroups):
+        dsetname = "PhotonEnergiesGroup{0:d}".format(grp + 1)
+        energydata = np.zeros((nparts), dtype=np.float32)
+        parts.create_dataset(dsetname, data=energydata)
+
+        dsetname = "PhotonFluxesGroup{0:d}".format(grp + 1)
+        fluxdata = np.zeros((nparts, 3), dtype=np.float32)
+        parts.create_dataset(dsetname, data=fluxdata)
+
+    for p in range(nparts):
+        E, Flux = initial_condition(xp[p], unitsystem)
+        for g in range(nPhotonGroups):
+            Esetname = "PhotonEnergiesGroup{0:d}".format(g + 1)
+            parts[Esetname][p] = E[g]
+            Fsetname = "PhotonFluxesGroup{0:d}".format(g + 1)
+            parts[Fsetname][p] = Flux[g]
+
+    #  from matplotlib import pyplot as plt
+    #  plt.figure()
+    #  for g in range(nPhotonGroups):
+    #      #  Esetname = "PhotonEnergiesGroup{0:d}".format(g+1)
+    #      #  plt.plot(xp[:,0], parts[Esetname], label="E "+str(g+1))
+    #      Fsetname = "PhotonFluxesGroup{0:d}".format(g+1)
+    #      plt.plot(xp[:,0], parts[Fsetname][:,0], label="F "+str(g+1))
+    #  plt.legend()
+    #  plt.show()
+
+    F.close()
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/plotEnergy.py b/examples/RadiativeTransferTests/CosmoAdvection_1D/plotEnergy.py
new file mode 100755
index 0000000000000000000000000000000000000000..7dc1bff0186f740a01a5a2d6ddfe547a757f9e79
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/plotEnergy.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Stan Verhoeve (s06verhoeve@gmail.com)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import os
+import sys
+import argparse
+
+import matplotlib as mpl
+import numpy as np
+import swiftsimio
+import unyt
+from matplotlib import pyplot as plt
+
+# Parameters users should/may tweak
+snapshot_base = "output"  # Snapshot basename
+plot_physical_quantities = True  # Plot physical or comoving quantities
+
+time_first = 0  # Time of first snapshot
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-z", "--redshift", help="Redshift domain to plot advection for", default="high"
+    )
+
+    args = parser.parse_args()
+    return args
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshot(s) that are to be plotted
+    and return their names as list
+    """
+
+    snaplist = []
+
+    dirlist = os.listdir()
+    for f in dirlist:
+        if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+            snaplist.append(f)
+
+    snaplist = sorted(snaplist)
+    if len(snaplist) == 0:
+        print(f"No snapshots with base {snapshot_basename} found!")
+        sys.exit(1)
+
+    return snaplist
+
+
+def plot_param_over_time(snapshot_list, param="energy density", redshift_domain="high"):
+    print(f"Now plotting {param} over time")
+    # Grab number of photon groups
+    data = swiftsimio.load(snapshot_list[0])
+    meta = data.metadata
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
+
+    # Number of rows and columns
+    nrows = 1 + int(plot_physical_quantities)
+    ncols = ngroups
+    # Create figure and axes
+    fig, axs = plt.subplots(nrows, ncols, figsize=(5.04 * ncols, 5.4 * nrows), dpi=200)
+
+    # Iterate over all photon groups
+    for n in range(ngroups):
+        # Arrays to keep track of plot_param and scale factor
+        plot_param = [[], []]
+        scale_factor = []
+        analytic_exponent = [0, 0]
+
+        # Functions to convert between scale factor and redshift
+        a2z = lambda a: 1 / a - 1
+        z2a = lambda z: 1 / (z + 1)
+
+        for file in snapshot_list:
+            data = swiftsimio.load(file)
+            meta = data.metadata
+
+            # Read comoving variables
+            energy = getattr(data.gas.photon_energies, f"group{n+1}")
+            mass = data.gas.masses
+            rho = data.gas.densities
+            volume = mass / rho
+
+            energy_density = energy / volume
+
+            if plot_physical_quantities:
+                # The SWIFT cosmology module assumes 3-dimensional lengths and volumes,
+                # so multiply by a**2 to get the correct relations
+                physical_energy_density = (
+                    energy_density.to_physical() * meta.scale_factor ** 2
+                )
+                physical_energy = energy.to_physical()
+
+                if param == "energy density":
+                    plot_param[1].append(
+                        1
+                        * physical_energy_density.sum()
+                        / physical_energy_density.shape[0]
+                    )
+                    analytic_exponent[1] = -1.0
+                elif param == "total energy":
+                    plot_param[1].append(1 * physical_energy.sum())
+                    analytic_exponent[1] = 0.0
+
+            if param == "energy density":
+                plot_param[0].append(1 * energy_density.sum() / energy_density.shape[0])
+                analytic_exponent[0] = 0.0
+            elif param == "total energy":
+                plot_param[0].append(1 * energy.sum())
+                analytic_exponent[0] = 0.0
+
+            scale_factor.append(meta.scale_factor)
+
+        if param == "energy density":
+            titles = [
+                "Comoving energy density",
+                "Physical energy density $\\times a^2$",
+            ]
+            ylabel = "Average energy density"
+            figname = "output_energy_density_over_time"
+        elif param == "total energy":
+            titles = ["Comoving total energy", "Physical total energy"]
+            ylabel = "Total energy"
+            figname = "output_total_energy_over_time"
+
+        # Analytic scale factor
+        analytic_scale_factor = np.linspace(min(scale_factor), max(scale_factor), 1000)
+
+        for i in range(nrows):
+            ax = axs[i, n]
+            ax.scatter(scale_factor, plot_param[i], label="Simulation")
+
+            # Analytic scale factor relation
+            analytic = analytic_scale_factor ** analytic_exponent[i]
+
+            # Scale solution to correct offset
+            analytic = analytic / analytic[0] * plot_param[i][0]
+            ax.plot(
+                analytic_scale_factor,
+                analytic,
+                c="r",
+                label=f"Analytic solution $\propto a^{{{analytic_exponent[i]}}}$",
+            )
+
+            ax.legend()
+            ax.set_title(titles[i] + f" group {n+1}")
+
+            ax.set_xlabel("Scale factor")
+            secax = ax.secondary_xaxis("top", functions=(a2z, z2a))
+            secax.set_xlabel("Redshift")
+
+            ax.yaxis.get_offset_text().set_position((-0.05, 1))
+
+            if analytic_exponent[i] == 0.0:
+                ax.set_ylim(plot_param[i][0] * 0.95, plot_param[i][0] * 1.05)
+                ylabel_scale = ""
+            else:
+                ylabel_scale = "$\\times a^2$"
+            if n == 0:
+                units = plot_param[i][0].units.latex_representation()
+                ax.set_ylabel(f"{ylabel} [${units}$] {ylabel_scale}")
+    plt.tight_layout()
+    plt.savefig(f"{figname}-{redshift_domain}.png")
+    plt.close()
+
+
+if __name__ in ("__main__"):
+    # Get command line args
+    args = parse_args()
+    domain = args.redshift.lower()
+    if domain in ("low", "l", "low_redshift", "low redshift", "low-redshift"):
+        redshift_domain = "low_redshift"
+    elif domain in (
+        "medium",
+        "m",
+        "medium_redshift",
+        "medium redshift",
+        "medium-redshift",
+    ):
+        redshift_domain = "medium_redshift"
+    elif domain in ("high", "h", "high_redshift", "high redshift", "high-redshift"):
+        redshift_domain = "high_redshift"
+    else:
+        print("Redshift domain not recognised!")
+        sys.exit(1)
+
+    print(snapshot_base + f"_{redshift_domain}")
+    snaplist = get_snapshot_list(snapshot_base + f"_{redshift_domain}")
+
+    for param in ["energy density", "total energy"]:
+        plot_param_over_time(snaplist, param, redshift_domain)
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/plotSolution.py b/examples/RadiativeTransferTests/CosmoAdvection_1D/plotSolution.py
new file mode 100755
index 0000000000000000000000000000000000000000..797f34d9c2401397205f237fa092ffb54116c544
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/plotSolution.py
@@ -0,0 +1,421 @@
+#!/usr/bin/env python3
+
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2021 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+# ----------------------------------------------
+# plot photon data assuming a 1D problem
+# give snapshot number as cmdline arg to plot
+# single snapshot, otherwise this script plots
+# all snapshots available in the workdir
+# ----------------------------------------------
+
+import os
+import sys
+import argparse
+
+import matplotlib as mpl
+import numpy as np
+import swiftsimio
+import unyt
+from matplotlib import pyplot as plt
+
+# Parameters users should/may tweak
+plot_all_data = True  # plot all groups and all photon quantities
+snapshot_base = "output"  # snapshot basename
+fancy = True  # fancy up the plots a bit
+plot_analytical_solutions = True  # overplot analytical solution
+plot_physical_quantities = True  # Plot physiical or comoving energy densities
+
+time_first = 0
+
+# properties for all scatterplots
+scatterplot_kwargs = {
+    "facecolor": "red",
+    "s": 4,
+    "alpha": 0.6,
+    "linewidth": 0.0,
+    "marker": ".",
+}
+
+# properties for all analytical solution plots
+analytical_solution_kwargs = {"linewidth": 1.0, "ls": "--", "c": "k", "alpha": 0.5}
+
+# -----------------------------------------------------------------------
+
+if plot_analytical_solutions:
+    from makeIC import initial_condition
+
+mpl.rcParams["text.usetex"] = True
+plot_all = False  # plot all snapshots
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-n", "--snapshot-number", help="Number of snaphot to plot", type=int
+    )
+    parser.add_argument(
+        "-z",
+        "--redshift",
+        help="Redshift domain to plot advection for",
+        default="high_redshift",
+    )
+
+    args = parser.parse_args()
+    return args
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshot(s) that are to be plotted
+    and return their names as list
+    """
+
+    snaplist = []
+
+    if plot_all:
+        dirlist = os.listdir()
+        for f in dirlist:
+            if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+                snaplist.append(f)
+
+        snaplist = sorted(snaplist)
+        if len(snaplist) == 0:
+            print(f"No snapshots with base {snapshot_basename} found!")
+            sys.exit(1)
+
+    else:
+        fname = snapshot_basename + "_" + str(snapnr).zfill(4) + ".hdf5"
+        if not os.path.exists(fname):
+            print("Didn't find file", fname)
+            quit(1)
+        snaplist.append(fname)
+
+    return snaplist
+
+
+def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
+    """
+    Create the actual plot.
+
+    filename: file to work with
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
+                        If none, limits are set automatically.
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
+                        If none, limits are set automatically.
+    """
+    global time_first
+    print("working on", filename)
+
+    # Read in data firt
+    data = swiftsimio.load(filename)
+    meta = data.metadata
+    cosmology = meta.cosmology_raw
+
+    scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8"))
+
+    boxsize = meta.boxsize[0]
+
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
+    # Currently, SPHM1RT only works for frequency group = 4 in the code
+    # However, we only plot 3 frequency groups here, so
+    # we set ngroups = 3:
+    if scheme.startswith("SPH M1closure"):
+        ngroups = 3
+
+    for g in range(ngroups):
+        # workaround to access named columns data with swiftsimio visualisaiton
+        new_attribute_str = "radiation_energy" + str(g + 1)
+        en = getattr(data.gas.photon_energies, "group" + str(g + 1))
+        setattr(data.gas, new_attribute_str, en)
+
+        if plot_all_data:
+            # prepare also the fluxes
+            for direction in ["X"]:
+                new_attribute_str = "radiation_flux" + str(g + 1) + direction
+                f = getattr(data.gas.photon_fluxes, "Group" + str(g + 1) + direction)
+                setattr(data.gas, new_attribute_str, f)
+
+    part_positions = data.gas.coordinates[:, 0].copy()
+
+    # get analytical solutions
+    if plot_analytical_solutions:
+        # Grab unit system used in IC
+        IC_units = swiftsimio.units.cosmo_units
+
+        # Grab unit system used in snapshot
+        snapshot_units = meta.units
+
+        snapshot_unit_energy = (
+            snapshot_units.mass * snapshot_units.length ** 2 / snapshot_units.time ** 2
+        )
+
+        time = meta.time.copy()
+
+        # Take care of simulation time not starting at 0
+        if time_first == 0:
+            time_first = time
+            time = 0 * snapshot_units.time
+        else:
+            time -= time_first
+
+        speed = meta.reduced_lightspeed
+
+        advected_positions = data.gas.coordinates[:].copy()
+        advected_positions[:, 0] -= speed * time
+        nparts = advected_positions.shape[0]
+        # add periodicity corrections
+        negatives = advected_positions < 0.0
+        if negatives.any():
+            while advected_positions.min() < 0.0:
+                advected_positions[negatives] += boxsize
+        overshooters = advected_positions > boxsize
+        if overshooters.any():
+            while advected_positions.max() > boxsize:
+                advected_positions[overshooters] -= boxsize
+
+        analytical_solutions = np.zeros((nparts, ngroups), dtype=np.float64)
+        for p in range(part_positions.shape[0]):
+            E, F = initial_condition(advected_positions[p], IC_units)
+            for g in range(ngroups):
+                analytical_solutions[p, g] = E[g] / snapshot_unit_energy
+
+    # Plot plot plot!
+    if plot_all_data:
+
+        fig = plt.figure(figsize=(5.05 * ngroups, 5.4), dpi=200)
+        figname = filename[:-5] + f"-all-quantities.png"
+
+        for g in range(ngroups):
+
+            # plot energy
+            new_attribute_str = "radiation_energy" + str(g + 1)
+            photon_energy = getattr(data.gas, new_attribute_str)
+
+            # physical_photon_energy = photon_energy.to_physical()
+            ax = fig.add_subplot(2, ngroups, g + 1)
+            s = np.argsort(part_positions)
+            if plot_analytical_solutions:
+                ax.plot(
+                    part_positions[s],
+                    analytical_solutions[s, g],
+                    **analytical_solution_kwargs,
+                    label="analytical solution",
+                )
+            ax.scatter(
+                part_positions, photon_energy, **scatterplot_kwargs, label="simulation"
+            )
+            ax.legend()
+
+            ax.set_title("Group {0:2d}".format(g + 1))
+            if g == 0:
+                ax.set_ylabel(
+                    "Energies [$" + photon_energy.units.latex_representation() + "$]"
+                )
+            ax.set_xlabel("x [$" + part_positions.units.latex_representation() + "$]")
+
+            if energy_boundaries is not None:
+
+                if abs(energy_boundaries[g][1]) > abs(energy_boundaries[g][0]):
+                    fixed_min = energy_boundaries[g][0] - 0.1 * abs(
+                        energy_boundaries[g][1]
+                    )
+                    fixed_max = energy_boundaries[g][1] * 1.1
+                else:
+                    fixed_min = energy_boundaries[g][0] * 1.1
+                    fixed_max = energy_boundaries[g][1] + 0.1 * abs(
+                        energy_boundaries[g][0]
+                    )
+                ax.set_ylim(fixed_min, fixed_max)
+
+            # plot flux X
+            new_attribute_str = "radiation_flux" + str(g + 1) + "X"
+            photon_flux = getattr(data.gas, new_attribute_str)
+            physical_photon_flux = photon_flux.to_physical()
+
+            if scheme.startswith("GEAR M1closure"):
+                photon_flux = photon_flux.to("erg/cm**2/s")
+            elif scheme.startswith("SPH M1closure"):
+                photon_flux = photon_flux.to("erg*cm/s")
+            else:
+                print("Error: Unknown RT scheme " + scheme)
+                exit()
+            ax = fig.add_subplot(2, ngroups, g + 1 + ngroups)
+            ax.scatter(part_positions, photon_flux, **scatterplot_kwargs)
+
+            if g == 0:
+                ax.set_ylabel(
+                    "Flux X [$" + photon_flux.units.latex_representation() + "$]"
+                )
+            ax.set_xlabel("x [$" + part_positions.units.latex_representation() + "$]")
+
+            if flux_boundaries is not None:
+
+                if abs(flux_boundaries[g][1]) > abs(flux_boundaries[g][0]):
+                    fixed_min = flux_boundaries[g][0] - 0.1 * abs(flux_boundaries[g][1])
+                    fixed_max = flux_boundaries[g][1] * 1.1
+                else:
+                    fixed_min = flux_boundaries[g][0] * 1.1
+                    fixed_max = flux_boundaries[g][1] + 0.1 * abs(flux_boundaries[g][0])
+
+                ax.set_ylim(fixed_min, fixed_max)
+
+    else:  # plot just energies
+
+        fig = plt.figure(figsize=(5 * ngroups, 5), dpi=200)
+        figname = filename[:-5] + ".png"
+
+        for g in range(ngroups):
+
+            ax = fig.add_subplot(1, ngroups, g + 1)
+
+            new_attribute_str = "radiation_energy" + str(g + 1)
+            photon_energy = getattr(data.gas, new_attribute_str).to_physical()
+
+            s = np.argsort(part_positions)
+            if plot_analytical_solutions:
+                ax.plot(
+                    part_positions[s],
+                    analytical_solutions[s, g],
+                    **analytical_solution_kwargs,
+                    label="analytical solution",
+                )
+            ax.scatter(
+                part_positions, photon_energy, **scatterplot_kwargs, label="simulation"
+            )
+            ax.set_title("Group {0:2d}".format(g + 1))
+            if g == 0:
+                ax.set_ylabel(
+                    "Energies [$" + photon_energy.units.latex_representation() + "$]"
+                )
+            ax.set_xlabel("x [$" + part_positions.units.latex_representation() + "$]")
+
+            if energy_boundaries is not None:
+
+                if abs(energy_boundaries[g][1]) > abs(energy_boundaries[g][0]):
+                    fixed_min = energy_boundaries[g][0] - 0.1 * abs(
+                        energy_boundaries[g][1]
+                    )
+                    fixed_max = energy_boundaries[g][1] * 1.1
+                else:
+                    fixed_min = energy_boundaries[g][0] * 1.1
+                    fixed_max = energy_boundaries[g][1] + 0.1 * abs(
+                        energy_boundaries[g][0]
+                    )
+                ax.set_ylim(fixed_min, fixed_max)
+
+    # add title
+    title = filename.replace("_", r"\_")  # exception handle underscore for latex
+    if meta.cosmology is not None:
+        title += ", $z$ = {0:.2e}".format(meta.z)
+    title += ", $t$ = {0:.3e}".format(1 * meta.time)
+    fig.suptitle(title)
+
+    plt.tight_layout()
+    plt.savefig(figname)
+    plt.close()
+    return
+
+
+def get_minmax_vals(snaplist):
+    """
+    Find minimal and maximal values for energy and flux,
+    so you can fix axes limits over all snapshots
+
+    snaplist: list of snapshot filenames
+
+    returns:
+
+    energy_boundaries: list of [E_min, E_max] for each photon group
+    flux_boundaries: list of [Fx_min, Fy_max] for each photon group
+    """
+
+    emins = []
+    emaxs = []
+    fmins = []
+    fmaxs = []
+
+    for filename in snaplist:
+
+        data = swiftsimio.load(filename)
+        meta = data.metadata
+
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
+        emin_group = []
+        emax_group = []
+        fluxmin_group = []
+        fluxmax_group = []
+
+        for g in range(ngroups):
+            en = getattr(data.gas.photon_energies, "group" + str(g + 1))
+            emin_group.append((1 * en.min()).value)
+            emax_group.append((1 * en.max()).value)
+
+            for direction in ["X"]:
+                #  for direction in ["X", "Y", "Z"]:
+                f = getattr(data.gas.photon_fluxes, "Group" + str(g + 1) + direction)
+                fluxmin_group.append(
+                    (1 * f.min()).to(unyt.erg / unyt.cm ** 2 / unyt.s).value
+                )
+                fluxmax_group.append(
+                    (1 * f.max()).to(unyt.erg / unyt.cm ** 2 / unyt.s).value
+                )
+
+        emins.append(emin_group)
+        emaxs.append(emax_group)
+        fmins.append(fluxmin_group)
+        fmaxs.append(fluxmax_group)
+
+    energy_boundaries = []
+    flux_boundaries = []
+    for g in range(ngroups):
+        emin = min([emins[f][g] for f in range(len(snaplist))])
+        emax = max([emaxs[f][g] for f in range(len(snaplist))])
+        energy_boundaries.append([emin, emax])
+        fmin = min([fmins[f][g] for f in range(len(snaplist))])
+        fmax = max([fmaxs[f][g] for f in range(len(snaplist))])
+        flux_boundaries.append([fmin, fmax])
+
+    return energy_boundaries, flux_boundaries
+
+
+if __name__ == "__main__":
+    # Get command line arguments
+    args = parse_args()
+
+    if args.snapshot_number:
+        plot_all = False
+        snapnr = int(args.snapshot_number)
+    else:
+        plot_all = True
+
+    domain = args.redshift
+    snaplist = get_snapshot_list(snapshot_base + f"_{domain}")
+
+    if fancy:
+        energy_boundaries, flux_boundaries = get_minmax_vals(snaplist)
+    else:
+        energy_boundaries, flux_boundaries = (None, None)
+
+    for f in snaplist:
+        plot_photons(
+            f, energy_boundaries=energy_boundaries, flux_boundaries=flux_boundaries
+        )
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_high_redshift.yml b/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_high_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e99c1370c6f4d9988e8f08c4e4958f39b077983f
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_high_redshift.yml
@@ -0,0 +1,69 @@
+MetaData:
+  run_name: RT_advection-1D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # kpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_high_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_high_redshift
+  scale_factor_first:  0.00990099  # Time of the first output (in internal units)
+  delta_time:          1.06
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.00990099
+  delta_time:          1.06   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advection_1D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [0., 1., 2.]  # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0., 0., 0.]  # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                  # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 0                       # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17 # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  skip_thermochemistry: 1                        # ignore thermochemistry.
+  set_initial_ionization_mass_fractions: 1       # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                        # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                        # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.00990099  # z~100
+  a_end:          0.01408451  # z~70
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_low_redshift.yml b/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_low_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..14da127f741675fbea8ad7825ec11e24b9df4885
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_low_redshift.yml
@@ -0,0 +1,69 @@
+MetaData:
+  run_name: RT_advection-1D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_low_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_low_redshift
+  scale_factor_first:  0.93  # Time of the first output (in internal units)
+  delta_time:          1.005
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.93
+  delta_time:          1.005   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advection_1D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [0., 1., 2.]  # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0., 0., 0.]  # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                  # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 0                       # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17 # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  skip_thermochemistry: 1                        # ignore thermochemistry.
+  set_initial_ionization_mass_fractions: 1       # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                        # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                        # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.93
+  a_end:          1.
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_medium_redshift.yml b/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_medium_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9b5f742fd762cec2877f59f9c4b8acad6608b71d
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/rt_advection1D_medium_redshift.yml
@@ -0,0 +1,71 @@
+MetaData:
+  run_name: RT_advection-1D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2   # The maximal time-step size of the simulation (in internal units).
+  time_begin: 0
+  time_end:   1.
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_medium_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_medium_redshift
+  scale_factor_first:  0.0909
+  delta_time:          1.03
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.0909
+  delta_time:          1.02   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advection_1D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [0., 1., 2.]  # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0., 0., 0.]  # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                  # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 0                       # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17 # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  skip_thermochemistry: 1                        # ignore thermochemistry.
+  set_initial_ionization_mass_fractions: 1       # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                        # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                        # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.0909    # z=10
+  a_end:          0.1428571 # z=6
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/run.sh b/examples/RadiativeTransferTests/CosmoAdvection_1D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c1a6a43cc6cd754e86b66d1dcd3f15dfe0766aee
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/run.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -f advection_1D.hdf5 ]; then
+    echo "Generating ICs"
+    python3 makeIC.py
+fi
+
+# Default run
+ymlfile=rt_advection1D_high_redshift.yml
+zdomain="h"
+
+# Do we have a cmdline argument provided?
+if [ $# -gt 0 ]; then
+    case "$1" in
+    -l | -low | --l | --low | l | ./rt_advection1D_low_redshift.yml | rt_advection1D_low_redshift | rt_advection1D_low_redshift.yml )
+        ymlfile=rt_advection1D_low_redshift.yml
+	zdomain="l"
+        ;;
+    -m | -mid | --m | --mid | m | ./rt_advection1D_medium_redshift.yml | rt_advection1D_medium_redshift | rt_advection1D_medium_redshift.yml )
+        ymlfile=rt_advection1D_medium_redshift.yml
+	zdomain="m"
+        ;;
+    -h | -high | -hi | --h | --hi | --high | h | ./rt_advection1D_high_redshift.yml | rt_advection1D_high_redshift | rt_advection1D_high_redshift.yml )
+        ymlfile=rt_advection1D_high_redshift.yml
+	zdomain="h"
+        ;;
+    *)
+        echo unknown cmdline param, running default $ymlfile
+        ;;
+    esac
+fi
+
+
+
+# Run SWIFT with RT
+../../../swift \
+    --hydro \
+    --cosmology \
+    --threads=4 \
+    --verbose=0  \
+    --radiation \
+    --stars \
+    --feedback \
+    --external-gravity \
+    $ymlfile 2>&1 | tee output.log
+
+python3 ./plotSolution.py -z $zdomain
+python3 ./plotEnergy.py -z $zdomain
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_high_redshift b/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_high_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e504014177837a7b2c1d134d1c4a0d5a785d0044
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_high_redshift
@@ -0,0 +1,12 @@
+# Redshift
+99.98460572382712
+96.47031888378449
+93.23367859034735
+90.25299848922855
+87.49735694548497
+84.9407463291136
+82.56112036003019
+80.33965676782498
+78.26018022924342
+76.30870609169932
+74.47307621234665
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_low_redshift b/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_low_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e58febac09ba12e5677c6fc8abafe94cc31824ad
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_low_redshift
@@ -0,0 +1,12 @@
+# Redshift
+0.0751932785953775
+0.06869649828543634
+0.06225945012746337
+0.055881161353223074
+0.04956068264358926
+0.04329708741463545
+0.03708947113706218
+0.030936950674366415
+0.02483866364616727
+0.018793767817153695
+0.01280144050017884
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_medium_redshift b/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_medium_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..958165894af540df676443cfdf3fbf20ce46dd1c
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_1D/snapshot_times_medium_redshift
@@ -0,0 +1,12 @@
+# Redshift
+9.983090485639307
+9.499283920730043
+8.92297287774699
+8.419654113369706
+7.975570397565402
+7.580294600085116
+7.225767198868688
+6.905651913777566
+6.614892571630509
+6.349401127100822
+6.1058334302500645
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/README b/examples/RadiativeTransferTests/CosmoAdvection_2D/README
new file mode 100644
index 0000000000000000000000000000000000000000..d08a30340bc6122bc675fba4e927365ae5c5378d
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/README
@@ -0,0 +1,26 @@
+2D advection test for radiative transfer.
+
+Test that your method is TVD and the propagation speed of the photons is
+correct. The ICs set up four photon groups: 
+- The first is a top hat function initial distribution where outside values
+  are zero, advecting along the x direction
+- The second is a top hat function initial distribution where outside values
+  are nonzero. This distinction is important to test because photon energies 
+  can't be negative, so these cases need to be tested individually. This
+  group advects along the y direction
+- the third is a smooth Gaussian advecting diagonally.
+- the fourth is a circle in the center advecting radially.
+
+This way, you can test multiple initial condition scenarios simultaneously. 
+There are no stars to act as sources. Also make sure that you choose your
+photon frequencies in a way that doesn't interact with gas!
+
+The ICs are created to be compatible with GEAR_RT. Recommended configuration:
+    --with-rt=GEAR_4 --with-rt-riemann-solver=GLF --with-hydro-dimension=2 --with-hydro=gizmo-mfv \
+     --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
+
+    
+
+Note that if you want to use a reduced speed of light for this test, you also 
+need to adapt the fluxes in the initial conditions! They are generated assuming
+that the speed of light is not reduced.
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/getGlass.sh b/examples/RadiativeTransferTests/CosmoAdvection_2D/getGlass.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ae3c977064f5e7a408aa249c5fd9089b3c52ecb1
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/getGlass.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/glassPlane_128.hdf5
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/makeIC.py b/examples/RadiativeTransferTests/CosmoAdvection_2D/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..d3769ab5498cc445e7e5822836a2cc6df8de3c7f
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/makeIC.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2021 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2022 Tsang Keung Chan (chantsangkeung@gmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+# -------------------------------------------------------------
+# Add initial conditions for photon energies and fluxes
+# for 2D advection of photons.
+# First photon group: Top hat function with zero as the
+#       baseline, advects in x direction
+# Second photon group: Top hat function with nonzero value
+#       as the baseline, advcts in y direction.
+# Third photon group: Gaussian advecting diagonally
+# Fourth photon group: Circle moving radially from the center
+# -------------------------------------------------------------
+
+import h5py
+import numpy as np
+import unyt
+from swiftsimio import Writer
+from swiftsimio.units import cosmo_units
+
+# define unit system to use
+unitsystem = cosmo_units
+
+# define box size
+boxsize = 260 * unyt.Mpc
+boxsize = boxsize.to(unitsystem["length"])
+
+reduced_speed_of_light_fraction = 1.0
+
+# number of photon groups
+nPhotonGroups = 4
+
+# filename of ICs to be generated
+outputfilename = "advection_2D.hdf5"
+
+
+def initial_condition(x, unitsystem):
+    """
+    The initial conditions that will be advected
+
+    x: particle position. 3D unyt array
+
+    returns: 
+    E: photon energy for each photon group. List of scalars with size of nPhotonGroups
+    F: photon flux for each photon group. List with size of nPhotonGroups of numpy arrays of shape (3,)
+    """
+
+    # you can make the photon quantities unitless, the units will
+    # already have been written down in the writer.
+    # However, that means that you need to convert them manually.
+
+    unit_energy = (
+        unitsystem["mass"] * unitsystem["length"] ** 2 / unitsystem["time"] ** 2
+    )
+    unit_velocity = unitsystem["length"] / unitsystem["time"]
+    unit_flux = unit_energy * unit_velocity
+
+    c_internal = (unyt.c * reduced_speed_of_light_fraction).to(unit_velocity)
+
+    E_list = []
+    F_list = []
+
+    # Group 1 Photons:
+    # -------------------
+
+    in_x = 0.33 * boxsize < x[0] < 0.66 * boxsize
+    in_y = 0.33 * boxsize < x[1] < 0.66 * boxsize
+    if in_x and in_y:
+        E = 1.0 * unit_energy
+    else:
+        E = 0.0 * unit_energy
+
+    # Assuming all photons flow in only one direction
+    # (optically thin regime, "free streaming limit"),
+    #  we have that |F| = c * E
+    F = np.zeros(3, dtype=np.float64)
+    F[0] = (c_internal * E).to(unit_flux)
+
+    E_list.append(E)
+    F_list.append(F)
+
+    # Group 2 Photons:
+    # -------------------
+
+    in_x = 0.33 * boxsize < x[0] < 0.66 * boxsize
+    in_y = 0.33 * boxsize < x[1] < 0.66 * boxsize
+    if in_x and in_y:
+        E = 2.0 * unit_energy
+    else:
+        E = 1.0 * unit_energy
+
+    F = np.zeros(3, dtype=np.float64)
+    F[1] = (c_internal * E).to(unit_flux)
+
+    E_list.append(E)
+    F_list.append(F)
+
+    # Group 3 Photons:
+    # -------------------
+    sigma = 0.1 * boxsize
+    mean = 0.5 * boxsize
+    amplitude = 2.0
+    baseline = 1.0
+
+    E = (
+        amplitude
+        * np.exp(-((x[0] - mean) ** 2 + (x[1] - mean) ** 2) / (2 * sigma ** 2))
+        + baseline
+    ) * unit_energy
+
+    F = np.zeros(3, dtype=np.float64)
+    F[0] = (c_internal * E / 1.414213562).to(unit_flux)  # sqrt(2)
+    F[1] = (c_internal * E / 1.414213562).to(unit_flux)  # sqrt(2)
+
+    E_list.append(E)
+    F_list.append(F)
+
+    # Group 4 Photons:
+    # -------------------
+
+    circle_radius = 0.15 * boxsize
+    center = 0.5 * boxsize
+    dx = x[0] - center
+    dy = x[1] - center
+    r = np.sqrt(dx ** 2 + dy ** 2)
+    if r <= circle_radius:
+        unit_vector = (dx / r, dy / r)
+
+        E = 1.0 * unit_energy
+        F = np.zeros(3, dtype=np.float64)
+        F[0] = (unit_vector[0] * c_internal * E).to(unit_flux)
+        F[1] = (unit_vector[1] * c_internal * E).to(unit_flux)
+
+    else:
+        E = 0.0 * unit_energy
+        F = np.zeros(3, dtype=np.float64)
+
+    E_list.append(E)
+    F_list.append(F)
+
+    return E_list, F_list
+
+
+if __name__ == "__main__":
+    glass = h5py.File("glassPlane_128.hdf5", "r")
+
+    # Read particle positions and h from the glass
+    pos = glass["/PartType0/Coordinates"][:, :]
+    h = glass["/PartType0/SmoothingLength"][:]
+    glass.close()
+
+    pos *= boxsize
+    h *= boxsize
+    numPart = np.size(h)
+
+    w = Writer(unitsystem, boxsize, dimension=2)
+
+    w.gas.coordinates = pos
+    w.gas.velocities = np.zeros((numPart, 3)) * (unyt.cm / unyt.s)
+    mpart = 1e20 * unyt.M_Sun
+    mpart = mpart.to(unitsystem["mass"])
+    w.gas.masses = np.ones(numPart, dtype=np.float64) * mpart
+    w.gas.internal_energy = (
+        np.ones(numPart, dtype=np.float64) * (300.0 * unyt.kb * unyt.K) / unyt.g
+    )
+
+    # Generate initial guess for smoothing lengths based on MIPS
+    w.gas.smoothing_length = h
+
+    # If IDs are not present, this automatically generates
+    w.write(outputfilename)
+
+    # Now open file back up again and add photon groups
+    # you can make them unitless, the units have already been
+    # written down in the writer. In this case, it's in cgs.
+
+    F = h5py.File(outputfilename, "r+")
+    header = F["Header"]
+    nparts = header.attrs["NumPart_ThisFile"][0]
+    parts = F["/PartType0"]
+
+    for grp in range(nPhotonGroups):
+        dsetname = "PhotonEnergiesGroup{0:d}".format(grp + 1)
+        energydata = np.zeros(nparts, dtype=np.float32)
+        parts.create_dataset(dsetname, data=energydata)
+
+        dsetname = "PhotonFluxesGroup{0:d}".format(grp + 1)
+        #  if dsetname not in parts.keys():
+        fluxdata = np.zeros((nparts, 3), dtype=np.float32)
+        parts.create_dataset(dsetname, data=fluxdata)
+
+    for p in range(nparts):
+        E, Flux = initial_condition(pos[p], unitsystem)
+        for g in range(nPhotonGroups):
+            Esetname = "PhotonEnergiesGroup{0:d}".format(g + 1)
+            parts[Esetname][p] = E[g]
+            Fsetname = "PhotonFluxesGroup{0:d}".format(g + 1)
+            parts[Fsetname][p] = Flux[g]
+
+    F.close()
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/plotEnergy.py b/examples/RadiativeTransferTests/CosmoAdvection_2D/plotEnergy.py
new file mode 100755
index 0000000000000000000000000000000000000000..c32ad26515e698d401003621cc47c4020c95b3d7
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/plotEnergy.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python3
+
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import os
+import sys
+import argparse
+
+import matplotlib as mpl
+import numpy as np
+import swiftsimio
+import unyt
+from matplotlib import pyplot as plt
+
+# Parameters users should/may tweak
+snapshot_base = "output"  # Snapshot basename
+plot_physical_quantities = True  # Plot physical or comoving quantities
+
+time_first = 0  # Time of first snapshot
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-z", "--redshift", help="Redshift domain to plot advection for", default="high"
+    )
+
+    args = parser.parse_args()
+    return args
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshot(s) that are to be plotted
+    and return their names as list
+    """
+
+    snaplist = []
+
+    dirlist = os.listdir()
+    for f in dirlist:
+        if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+            snaplist.append(f)
+
+    snaplist = sorted(snaplist)
+    if len(snaplist) == 0:
+        print(f"No snapshots with base {snapshot_basename} found!")
+        sys.exit(1)
+    return snaplist
+
+
+def plot_param_over_time(snapshot_list, param="energy density", redshift_domain="high"):
+    print(f"Now plotting {param} over time")
+    # Grab number of photon groups
+    data = swiftsimio.load(snapshot_list[0])
+    meta = data.metadata
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
+
+    # Number of rows and columns
+    nrows = 1 + int(plot_physical_quantities)
+    ncols = ngroups
+    # Create figure and axes
+    fig, axs = plt.subplots(nrows, ncols, figsize=(5.04 * ncols, 5.4 * nrows), dpi=200)
+
+    # Iterate over all photon groups
+    for n in range(ngroups):
+        # Arrays to keep track of plot_param and scale factor
+        plot_param = [[], []]
+        scale_factor = []
+        analytic_exponent = [0, 0]
+
+        # Functions to convert between scale factor and redshift
+        a2z = lambda a: 1 / a - 1
+        z2a = lambda z: 1 / (z + 1)
+
+        for file in snapshot_list:
+            data = swiftsimio.load(file)
+            meta = data.metadata
+
+            # Read comoving variables
+            energy = getattr(data.gas.photon_energies, f"group{n+1}")
+            mass = data.gas.masses
+            rho = data.gas.densities
+            volume = mass / rho
+
+            energy_density = energy / volume
+
+            if plot_physical_quantities:
+                # The SWIFT cosmology module assumes 3-dimensional lengths and volumes,
+                # so multiply by a to get the correct relations
+                physical_energy_density = (
+                    energy_density.to_physical() * meta.scale_factor
+                )
+                physical_energy = energy.to_physical()
+
+                if param == "energy density":
+                    plot_param[1].append(
+                        1
+                        * physical_energy_density.sum()
+                        / physical_energy_density.shape[0]
+                    )
+                    analytic_exponent[1] = -2.0
+                elif param == "total energy":
+                    plot_param[1].append(1 * physical_energy.sum())
+                    analytic_exponent[1] = 0.0
+
+            if param == "energy density":
+                plot_param[0].append(1 * energy_density.sum() / energy_density.shape[0])
+                analytic_exponent[0] = 0.0
+            elif param == "total energy":
+                plot_param[0].append(1 * energy.sum())
+                analytic_exponent[0] = 0.0
+
+            scale_factor.append(meta.scale_factor)
+
+        if param == "energy density":
+            titles = ["Comoving energy density", "Physical energy density $\\times a$"]
+            ylabel = "Average energy density"
+            figname = "output_energy_density_over_time"
+        elif param == "total energy":
+            titles = ["Comoving total energy", "Physical total energy"]
+            ylabel = "Total energy"
+            figname = "output_total_energy_over_time"
+
+        # Analytic scale factor
+        analytic_scale_factor = np.linspace(min(scale_factor), max(scale_factor), 1000)
+
+        for i in range(nrows):
+            ax = axs[i, n]
+            ax.scatter(scale_factor, plot_param[i], label="Simulation")
+
+            # Analytic scale factor relation
+            analytic = analytic_scale_factor ** analytic_exponent[i]
+
+            # Scale solution to correct offset
+            analytic = analytic / analytic[0] * plot_param[i][0]
+            ax.plot(
+                analytic_scale_factor,
+                analytic,
+                c="r",
+                label=f"Analytic solution $\propto a^{{{analytic_exponent[i]}}}$",
+            )
+
+            ax.legend()
+            ax.set_title(titles[i] + f" group {n+1}")
+
+            ax.set_xlabel("Scale factor")
+            secax = ax.secondary_xaxis("top", functions=(a2z, z2a))
+            secax.set_xlabel("Redshift")
+
+            ax.yaxis.get_offset_text().set_position((-0.05, 1))
+
+            if analytic_exponent[i] == 0.0:
+                ax.set_ylim(plot_param[i][0] * 0.95, plot_param[i][0] * 1.05)
+                ylabel_scale = ""
+            else:
+                ylabel_scale = "$\\times a$"
+            if n == 0:
+                units = plot_param[i][0].units.latex_representation()
+                ax.set_ylabel(f"{ylabel} [${units}$] {ylabel_scale}")
+    plt.tight_layout()
+    plt.savefig(f"{figname}-{redshift_domain}.png")
+    plt.close()
+
+
+if __name__ in ("__main__"):
+    # Get command line args
+    args = parse_args()
+    domain = args.redshift.lower()
+    if domain in ("low", "l", "low_redshift", "low redshift", "low-redshift"):
+        redshift_domain = "low_redshift"
+    elif domain in (
+        "medium",
+        "m",
+        "medium_redshift",
+        "medium redshift",
+        "medium-redshift",
+    ):
+        redshift_domain = "medium_redshift"
+    elif domain in ("high", "h", "high_redshift", "high redshift", "high-redshift"):
+        redshift_domain = "high_redshift"
+    else:
+        print("Redshift domain not recognised!")
+        sys.exit(1)
+
+    snaplist = get_snapshot_list(snapshot_base + f"_{redshift_domain}")
+
+    for param in ["energy density", "total energy"]:
+        plot_param_over_time(snaplist, param, redshift_domain)
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/plotSolution.py b/examples/RadiativeTransferTests/CosmoAdvection_2D/plotSolution.py
new file mode 100755
index 0000000000000000000000000000000000000000..a30195718674ba79cbc857a93c742802272df154
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/plotSolution.py
@@ -0,0 +1,354 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2021 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+# ----------------------------------------------------
+# plot photon data for 2D problems
+# give snapshot number as cmdline arg to plot
+# single snapshot, otherwise this script plots
+# all snapshots available in the workdir
+# ----------------------------------------------------
+
+import gc
+import os
+import sys
+import argparse
+
+import unyt
+import numpy as np
+import matplotlib as mpl
+import swiftsimio
+from matplotlib import pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+
+# Parameters users should/may tweak
+plot_all_data = True  # plot all groups and all photon quantities
+snapshot_base = "output"  # snapshot basename
+fancy = True  # fancy up the plots a bit?
+plot_physical_quantities = True
+
+# parameters for imshow plots
+imshow_kwargs = {"origin": "lower", "cmap": "viridis"}
+
+
+projection_kwargs = {"resolution": 1024, "parallel": True}
+# -----------------------------------------------------------------------
+
+
+plot_all = False
+mpl.rcParams["text.usetex"] = True
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-n", "--snapshot-number", help="Number of snapshot to plot", type=int
+    )
+    parser.add_argument(
+        "-z",
+        "--redshift",
+        help="Redshift domain to plot advection for",
+        default="high_redshift",
+    )
+
+    args = parser.parse_args()
+    return args
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshot(s) that are to be plotted
+    and return their names as list
+    """
+
+    snaplist = []
+
+    if plot_all:
+        dirlist = os.listdir()
+        for f in dirlist:
+            if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+                snaplist.append(f)
+
+        snaplist = sorted(snaplist)
+        if len(snaplist) == 0:
+            print(f"No snapshots with base {snapshot_basename} found!")
+            sys.exit(1)
+
+    else:
+        fname = snapshot_basename + "_" + str(snapnr).zfill(4) + ".hdf5"
+        if not os.path.exists(fname):
+            print("Didn't find file", fname)
+            quit(1)
+        snaplist.append(fname)
+
+    return snaplist
+
+
+def set_colorbar(ax, im):
+    divider = make_axes_locatable(ax)
+    cax = divider.append_axes("right", size="5%", pad=0.05)
+    plt.colorbar(im, cax=cax)
+    return
+
+
+def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
+    """
+    Create the actual plot.
+
+    filename: file to work with
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
+                        If none, limits are set automatically.
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
+                        If none, limits are set automatically.
+    """
+
+    print("working on", filename)
+
+    # Read in data first
+    data = swiftsimio.load(filename)
+    meta = data.metadata
+
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
+    xlabel_units_str = meta.boxsize.units.latex_representation()
+
+    global imshow_kwargs
+    imshow_kwargs["extent"] = [0, meta.boxsize[0].v, 0, meta.boxsize[1].v]
+
+    for g in range(ngroups):
+        # workaround to access named columns data with swiftsimio visualisaiton
+        # add mass weights to remove surface density dependence in images
+        new_attribute_str = "mass_weighted_radiation_energy" + str(g + 1)
+        en = getattr(data.gas.photon_energies, "group" + str(g + 1))
+        en = en * data.gas.masses
+        setattr(data.gas, new_attribute_str, en)
+
+        if plot_all_data:
+            # prepare also the fluxes
+            #  for direction in ["X", "Y", "Z"]:
+            for direction in ["X", "Y"]:
+                new_attribute_str = (
+                    "mass_weighted_radiation_flux" + str(g + 1) + direction
+                )
+                f = getattr(data.gas.photon_fluxes, "Group" + str(g + 1) + direction)
+                # f *= data.gas.masses
+                f = f * data.gas.masses
+                setattr(data.gas, new_attribute_str, f)
+
+    # get mass surface density projection that we'll use to remove density dependence in image
+    mass_map = swiftsimio.visualisation.projection.project_gas(
+        data, project="masses", **projection_kwargs
+    )
+
+    if plot_all_data:
+        fig = plt.figure(figsize=(5 * 3, 5.05 * ngroups), dpi=200)
+        figname = filename[:-5] + f"-all-quantities.png"
+
+        for g in range(ngroups):
+
+            # get energy projection
+            new_attribute_str = "mass_weighted_radiation_energy" + str(g + 1)
+            photon_map = swiftsimio.visualisation.projection.project_gas(
+                data, project=new_attribute_str, **projection_kwargs
+            )
+            photon_map = photon_map / mass_map
+
+            ax = fig.add_subplot(ngroups, 3, g * 3 + 1)
+            if energy_boundaries is not None:
+                imshow_kwargs["vmin"] = energy_boundaries[g][0]
+                imshow_kwargs["vmax"] = energy_boundaries[g][1]
+            im = ax.imshow(photon_map.T, **imshow_kwargs)
+            set_colorbar(ax, im)
+            ax.set_ylabel("Group {0:2d}".format(g + 1))
+            ax.set_xlabel("x [$" + xlabel_units_str + "$]")
+            if g == 0:
+                ax.set_title("Energies")
+
+            # get flux X projection
+            new_attribute_str = "mass_weighted_radiation_flux" + str(g + 1) + "X"
+            photon_map = swiftsimio.visualisation.projection.project_gas(
+                data, project=new_attribute_str, **projection_kwargs
+            )
+            photon_map = photon_map / mass_map
+
+            ax = fig.add_subplot(ngroups, 3, g * 3 + 2)
+            if flux_boundaries is not None:
+                imshow_kwargs["vmin"] = flux_boundaries[g][0]
+                imshow_kwargs["vmax"] = flux_boundaries[g][1]
+            im = ax.imshow(photon_map.T, **imshow_kwargs)
+            set_colorbar(ax, im)
+            ax.set_xlabel("x [$" + xlabel_units_str + "$]")
+            ax.set_ylabel("y [$" + xlabel_units_str + "$]")
+            if g == 0:
+                ax.set_title("Flux X")
+
+            # get flux Y projection
+            new_attribute_str = "mass_weighted_radiation_flux" + str(g + 1) + "Y"
+            photon_map = swiftsimio.visualisation.projection.project_gas(
+                data, project=new_attribute_str, **projection_kwargs
+            )
+            photon_map = photon_map / mass_map
+
+            ax = fig.add_subplot(ngroups, 3, g * 3 + 3)
+            im = ax.imshow(photon_map.T, **imshow_kwargs)
+            set_colorbar(ax, im)
+            ax.set_xlabel("x [$" + xlabel_units_str + "$]")
+            ax.set_ylabel("y [$" + xlabel_units_str + "$]")
+            if g == 0:
+                ax.set_title("Flux Y")
+
+    else:  # plot just energies
+
+        fig = plt.figure(figsize=(5 * ngroups, 5), dpi=200)
+        figname = filename[:-5] + ".png"
+
+        for g in range(ngroups):
+
+            # get projection
+            new_attribute_str = "mass_weighted_radiation_energy" + str(g + 1)
+            photon_map = swiftsimio.visualisation.projection.project_gas(
+                data, project=new_attribute_str, **projection_kwargs
+            )
+            photon_map = photon_map / mass_map
+
+            ax = fig.add_subplot(1, ngroups, g + 1)
+            if energy_boundaries is not None:
+                imshow_kwargs["vmin"] = energy_boundaries[g][0]
+                imshow_kwargs["vmax"] = energy_boundaries[g][1]
+            im = ax.imshow(photon_map.T, **imshow_kwargs)
+            set_colorbar(ax, im)
+            ax.set_title("Group {0:2d}".format(g + 1))
+            if g == 0:
+                ax.set_ylabel("Energies")
+
+    # Add title
+    title = filename.replace("_", r"\_")  # exception handle underscore for latex
+    if meta.cosmology is not None:
+        title += ", $z$ = {0:.2e}".format(meta.z)
+    title += ", $t$ = {0:.2e}".format(1 * meta.time)
+    fig.suptitle(title)
+
+    plt.tight_layout()
+    plt.savefig(figname)
+    plt.close()
+    gc.collect()
+
+    return
+
+
+def get_minmax_vals(snaplist):
+    """
+    Find minimal and maximal values for energy and flux,
+    so you can fix axes limits over all snapshots
+
+    snaplist: list of snapshot filenames
+
+    returns:
+
+    energy_boundaries: list of [E_min, E_max] for each photon group
+    flux_boundaries: list of [Fx_min, Fy_max] for each photon group
+    """
+
+    emins = []
+    emaxs = []
+    fmins = []
+    fmaxs = []
+
+    for filename in snaplist:
+
+        data = swiftsimio.load(filename)
+        meta = data.metadata
+
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
+        emin_group = []
+        emax_group = []
+        fluxmin_group = []
+        fluxmax_group = []
+
+        for g in range(ngroups):
+            en = getattr(data.gas.photon_energies, "group" + str(g + 1))
+            emin_group.append((1 * en.min()).value)
+            emax_group.append((1 * en.max()).value)
+
+            dirmin = []
+            dirmax = []
+            for direction in ["X", "Y"]:
+                f = getattr(data.gas.photon_fluxes, "Group" + str(g + 1) + direction)
+                dirmin.append((1 * f.min()).value)
+                dirmax.append((1 * f.max()).value)
+            fluxmin_group.append(min(dirmin))
+            fluxmax_group.append(max(dirmax))
+
+        emins.append(emin_group)
+        emaxs.append(emax_group)
+        fmins.append(fluxmin_group)
+        fmaxs.append(fluxmax_group)
+
+    energy_boundaries = []
+    flux_boundaries = []
+    for g in range(ngroups):
+        emin = min([emins[f][g] for f in range(len(snaplist))])
+        emax = max([emaxs[f][g] for f in range(len(snaplist))])
+        energy_boundaries.append([emin, emax])
+        fmin = min([fmins[f][g] for f in range(len(snaplist))])
+        fmax = max([fmaxs[f][g] for f in range(len(snaplist))])
+        flux_boundaries.append([fmin, fmax])
+
+    return energy_boundaries, flux_boundaries
+
+
+if __name__ == "__main__":
+    # Get command line arguments
+    args = parse_args()
+
+    if args.snapshot_number:
+        plot_all = False
+        snapnr = int(args.snapshot_number)
+    else:
+        plot_all = True
+
+    domain = args.redshift
+    if domain in ("low", "l", "low_redshift", "low redshift", "low-redshift"):
+        redshift_domain = "low_redshift"
+    elif domain in (
+        "medium",
+        "m",
+        "medium_redshift",
+        "medium redshift",
+        "medium-redshift",
+    ):
+        redshift_domain = "medium_redshift"
+    elif domain in ("high", "h", "high_redshift", "high redshift", "high-redshift"):
+        redshift_domain = "high_redshift"
+    else:
+        print("Redshift domain not recognised!")
+        sys.exit(1)
+
+    snaplist = get_snapshot_list(snapshot_base + f"_{domain}")
+    if fancy:
+        energy_boundaries, flux_boundaries = get_minmax_vals(snaplist)
+    else:
+        energy_boundaries = None
+        flux_boundaries = None
+
+    for f in snaplist:
+        plot_photons(
+            f, energy_boundaries=energy_boundaries, flux_boundaries=flux_boundaries
+        )
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_high_redshift.yml b/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_high_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1e257683a84c1379eb623300e06c592360701b39
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_high_redshift.yml
@@ -0,0 +1,69 @@
+MetaData:
+  run_name: cosmo_RT_advection-2D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # kpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_high_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_high_redshift
+  scale_factor_first:  0.00990099  # Time of the first output (in internal units)
+  delta_time:          1.06
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.00990099
+  delta_time:          1.06   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advection_2D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0                       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99                             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [1., 2., 3., 4.]              # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                 # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [1e-32, 1e-32, 1e-32, 1e-32]   # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                   # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 0                        # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17  # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  set_initial_ionization_mass_fractions: 1        # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+  skip_thermochemistry: 1                         # Skip thermochemsitry.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.00990099  # z~100
+  a_end:          0.01408451  # z~70
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_low_redshift.yml b/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_low_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..64db5443d404607066faea7513f188dd7cb6fa03
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_low_redshift.yml
@@ -0,0 +1,69 @@
+MetaData:
+  run_name: cosmo_RT_advection-2D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_low_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_low_redshift
+  scale_factor_first:  0.93  # Time of the first output (in internal units)
+  delta_time:          1.005
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.93
+  delta_time:          1.005   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advection_2D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0                       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99                             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [1., 2., 3., 4.]              # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                 # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [1e-32, 1e-32, 1e-32, 1e-32]   # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                   # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 0                        # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17  # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  set_initial_ionization_mass_fractions: 1        # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+  skip_thermochemistry: 1                         # Skip thermochemsitry.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.93
+  a_end:          1.
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_medium_redshift.yml b/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_medium_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e4cc8b2d0f227cad0e8211ab5833a129e28962fe
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/rt_advection2D_medium_redshift.yml
@@ -0,0 +1,71 @@
+MetaData:
+  run_name: cosmo_RT_advection-2D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2   # The maximal time-step size of the simulation (in internal units).
+  time_begin: 0
+  time_end:   1.
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_medium_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_medium_redshift
+  scale_factor_first:  0.0909
+  delta_time:          1.01
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.0909
+  delta_time:          1.02   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advection_2D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0                       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99                             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [1., 2., 3., 4.]              # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                 # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [1e-32, 1e-32, 1e-32, 1e-32]   # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                   # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 0                        # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17  # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  set_initial_ionization_mass_fractions: 1        # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+  skip_thermochemistry: 1                         # Skip thermochemsitry.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.0909    # z=10
+  a_end:          0.1428571 # z=6
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/run.sh b/examples/RadiativeTransferTests/CosmoAdvection_2D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c0c7bacab484cabd06fa0c2eefbce8357c4e74ad
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/run.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+# exit if anything fails
+set -e
+set -o pipefail
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e glassPlane_128.hdf5 ]
+then
+    echo "Fetching initial glass file for the 2D RT advection example..."
+    ./getGlass.sh
+fi
+if [ ! -e advection_2D.hdf5 ]
+then
+    echo "Generating initial conditions for the 2D RT advection example..."
+    python3 makeIC.py
+fi
+
+# Default run
+ymlfile=rt_advection2D_high_redshift.yml
+zdomain="h"
+
+# Do we have a cmdline argument provided?
+if [ $# -gt 0 ]; then
+    case "$1" in
+    -l | -low | --l | --low | l | ./rt_advection2D_low_redshift.yml | rt_advection2D_low_redshift | rt_advection2D_low_redshift.yml )
+        ymlfile=rt_advection2D_low_redshift.yml
+	zdomain="l"
+        ;;
+    -m | -mid | --m | --mid | m | ./rt_advection2D_medium_redshift.yml | rt_advection2D_medium_redshift | rt_advection2D_medium_redshift.yml )
+        ymlfile=rt_advection2D_medium_redshift.yml
+	zdomain="m"
+        ;;
+    -h | -high | -hi | --h | --hi | --high | h | ./rt_advection2D_high_redshift.yml | rt_advection2D_high_redshift | rt_advection2D_high_redshift.yml )
+        ymlfile=rt_advection2D_high_redshift.yml
+	zdomain="h"
+        ;;
+    *)
+        echo unknown cmdline param, running default $ymlfile
+        ;;
+    esac
+fi
+
+# Run SWIFT with RT
+../../../swift \
+    --hydro \
+    --cosmology \
+    --threads=4 \
+    --verbose=0  \
+    --radiation \
+    --stars \
+    --feedback \
+    --external-gravity \
+    $ymlfile 2>&1 | tee output.log
+
+python3 ./plotSolution.py -z $zdomain
+python3 ./plotEnergy.py -z $zdomain
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_high_redshift b/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_high_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e504014177837a7b2c1d134d1c4a0d5a785d0044
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_high_redshift
@@ -0,0 +1,12 @@
+# Redshift
+99.98460572382712
+96.47031888378449
+93.23367859034735
+90.25299848922855
+87.49735694548497
+84.9407463291136
+82.56112036003019
+80.33965676782498
+78.26018022924342
+76.30870609169932
+74.47307621234665
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_low_redshift b/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_low_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e58febac09ba12e5677c6fc8abafe94cc31824ad
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_low_redshift
@@ -0,0 +1,12 @@
+# Redshift
+0.0751932785953775
+0.06869649828543634
+0.06225945012746337
+0.055881161353223074
+0.04956068264358926
+0.04329708741463545
+0.03708947113706218
+0.030936950674366415
+0.02483866364616727
+0.018793767817153695
+0.01280144050017884
diff --git a/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_medium_redshift b/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_medium_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..958165894af540df676443cfdf3fbf20ce46dd1c
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoAdvection_2D/snapshot_times_medium_redshift
@@ -0,0 +1,12 @@
+# Redshift
+9.983090485639307
+9.499283920730043
+8.92297287774699
+8.419654113369706
+7.975570397565402
+7.580294600085116
+7.225767198868688
+6.905651913777566
+6.614892571630509
+6.349401127100822
+6.1058334302500645
diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/README b/examples/RadiativeTransferTests/CosmoCoolingTest/README
new file mode 100644
index 0000000000000000000000000000000000000000..eb50535a856ff2a0a09f1440272ee9f22601d7e5
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoCoolingTest/README
@@ -0,0 +1,7 @@
+Check on a simple case whether the cooling works correctly without any
+radiation being present: Use a uniform 3D box of hot gas and let it cool down.
+
+The reference solution assumes case A recombination.
+
+To run with GEAR-RT, compile swift with:
+    --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none  --with-grackle=$GRACKLE_ROOT 
diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/getReference.sh b/examples/RadiativeTransferTests/CosmoCoolingTest/getReference.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a6c154ac1594e94374f468f3059c6e26fb58db3d
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoCoolingTest/getReference.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ReferenceSolutions/CosmoRTCoolingTestReference.txt
diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/makeIC.py b/examples/RadiativeTransferTests/CosmoCoolingTest/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..b727376480e8267bd36dd15e6c0eb044a921814d
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoCoolingTest/makeIC.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# -----------------------------------------------------------
+# Use 10 particles in each dimension to generate a uniform
+# box with high temperatures.
+# -----------------------------------------------------------
+
+import h5py
+import numpy as np
+import unyt
+from swiftsimio import Writer
+from swiftsimio.units import cosmo_units
+import yaml
+
+with open(r"rt_cooling_test.yml") as paramfile:
+    params = yaml.load(paramfile, Loader=yaml.FullLoader)
+    a_begin = params["Cosmology"]["a_begin"]
+    a_begin = float(a_begin)
+
+
+# number of particles in each dimension
+n_p = 10
+nparts = n_p ** 3
+# filename of ICs to be generated
+outputfilename = "cooling_test.hdf5"
+# adiabatic index
+gamma = 5.0 / 3.0
+# total hydrogen mass fraction
+XH = 0.76
+# total helium mass fraction
+XHe = 0.24
+# boxsize
+boxsize = 1 * unyt.kpc
+# initial gas temperature
+initial_temperature = 1e6 * unyt.K
+
+# Initial particle density and mass
+gas_density_phys_cgs = 1.6756058890024518e-25 * unyt.g / unyt.cm ** 3
+
+# Include a^3 to convert physical density to comoving
+pmass = (gas_density_phys_cgs) * (boxsize ** 3 / nparts) * a_begin ** 3
+pmass = pmass.to("Msun")
+# -----------------------------------------------
+
+
+def internal_energy(T, mu):
+    """
+    Compute the internal energy of the gas for a given
+    temperature and mean molecular weight
+    """
+    # Using u = 1 / (gamma - 1) * p / rho
+    #   and p = N/V * kT = rho / (mu * m_u) * kT
+
+    u = unyt.boltzmann_constant * T / (gamma - 1) / (mu * unyt.atomic_mass_unit)
+    return u
+
+
+def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp):
+    """
+    Determines the mean molecular weight for given 
+    mass fractions of
+        hydrogen:   XH0
+        H+:         XHp
+        He:         XHe0
+        He+:        XHep
+        He++:       XHepp
+
+    returns:
+        mu: mean molecular weight [in atomic mass units]
+        NOTE: to get the actual mean mass, you still need
+        to multiply it by m_u, as is tradition in the formulae
+    """
+
+    # 1/mu = sum_j X_j / A_j * (1 + E_j)
+    # A_H    = 1, E_H    = 0
+    # A_Hp   = 1, E_Hp   = 1
+    # A_He   = 4, E_He   = 0
+    # A_Hep  = 4, E_Hep  = 1
+    # A_Hepp = 4, E_Hepp = 2
+    one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp
+
+    return 1.0 / one_over_mu
+
+
+# assume everything is ionized initially
+mu = mean_molecular_weight(0.0, XH, 0.0, 0.0, XHe)
+u_part = internal_energy(initial_temperature, mu)
+pmass = pmass.to("Msun")
+
+
+xp = unyt.unyt_array(np.zeros((nparts, 3), dtype=np.float32), boxsize.units)
+dx = boxsize / n_p
+ind = 0
+for i in range(n_p):
+    x = (i + 0.5) * dx
+    for j in range(n_p):
+        y = (j + 0.5) * dx
+        for k in range(n_p):
+            z = (k + 0.5) * dx
+
+            xp[ind] = (x, y, z)
+            ind += 1
+
+w = Writer(cosmo_units, boxsize, dimension=3)
+
+w.gas.coordinates = xp
+w.gas.velocities = np.zeros(xp.shape, dtype=np.float32) * (unyt.km / unyt.s)
+w.gas.masses = np.ones(nparts, dtype=np.float32) * pmass
+w.gas.internal_energy = np.ones(nparts, dtype=np.float32) * u_part
+
+# Generate initial guess for smoothing lengths based on MIPS
+w.gas.generate_smoothing_lengths(boxsize=boxsize, dimension=3)
+
+# If IDs are not present, this automatically generates
+w.write(outputfilename)
+
+
+# Now open file back up again and add RT data.
+F = h5py.File(outputfilename, "r+")
+header = F["Header"]
+parts = F["/PartType0"]
+
+# Create initial ionization species mass fractions.
+# Assume everything is ionized initially
+# NOTE: grackle doesn't really like exact zeroes, so
+# use something very small instead.
+HIdata = np.ones(nparts, dtype=np.float32) * 1e-12
+HIIdata = np.ones(nparts, dtype=np.float32) * XH
+HeIdata = np.ones(nparts, dtype=np.float32) * 1e-12
+HeIIdata = np.ones(nparts, dtype=np.float32) * 1e-12
+HeIIIdata = np.ones(nparts, dtype=np.float32) * XHe
+
+parts.create_dataset("MassFractionHI", data=HIdata)
+parts.create_dataset("MassFractionHII", data=HIIdata)
+parts.create_dataset("MassFractionHeI", data=HeIdata)
+parts.create_dataset("MassFractionHeII", data=HeIIdata)
+parts.create_dataset("MassFractionHeIII", data=HeIIIdata)
+
+# close up, and we're done!
+F.close()
diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/plotSolution.py b/examples/RadiativeTransferTests/CosmoCoolingTest/plotSolution.py
new file mode 100755
index 0000000000000000000000000000000000000000..0b74d8605f2a94401847adc1b2ea1ed5c56145ca
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoCoolingTest/plotSolution.py
@@ -0,0 +1,475 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# -------------------------------------------
+# Plot the gas temperature, mean molecular
+# weight, and mass fractions
+# -------------------------------------------
+
+import copy
+import os
+
+import numpy as np
+import swiftsimio
+import unyt
+from matplotlib import pyplot as plt
+
+# arguments for plots of results
+plotkwargs = {"alpha": 0.5}
+# arguments for plot of references
+referenceplotkwargs = {"color": "grey", "lw": 4, "alpha": 0.6}
+# arguments for legends
+legendprops = {"size": 8}
+# snapshot basenames
+snapshot_base = "output"
+# Reference file name
+reference_file = "CosmoRTCoolingTestReference.txt"
+# Plot time on x-axis
+plot_time = False
+
+
+# -----------------------------------------------------------------------
+energy_units = unyt.Msun * unyt.kpc ** 2 / unyt.kyr ** 2
+mass_units = unyt.Msun
+length_units = unyt.kpc
+
+
+def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp):
+    """
+    Determines the mean molecular weight for given
+    mass fractions of
+        hydrogen:   XH0
+        H+:         XHp
+        He:         XHe0
+        He+:        XHep
+        He++:       XHepp
+
+    returns:
+        mu: mean molecular weight [in atomic mass units]
+        NOTE: to get the actual mean mass, you still need
+        to multiply it by m_u, as is tradition in the formulae
+    """
+
+    # 1/mu = sum_j X_j / A_j * (1 + E_j)
+    # A_H    = 1, E_H    = 0
+    # A_Hp   = 1, E_Hp   = 1
+    # A_He   = 4, E_He   = 0
+    # A_Hep  = 4, E_Hep  = 1
+    # A_Hepp = 4, E_Hepp = 2
+    one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp
+
+    return 1.0 / one_over_mu
+
+
+def gas_temperature(u, mu, gamma):
+    """
+    Compute the gas temperature given the specific internal
+    energy u and the mean molecular weight mu
+    """
+
+    # Using u = 1 / (gamma - 1) * p / rho
+    #   and p = N/V * kT = rho / (mu * m_u) * kT
+
+    T = u * (gamma - 1) * mu * unyt.atomic_mass_unit / unyt.boltzmann_constant
+
+    return T
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshot(s) that are to be plotted
+    and return their names as list
+    """
+
+    snaplist = []
+
+    dirlist = os.listdir()
+    for f in dirlist:
+        if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+            snaplist.append(f)
+
+    if len(snaplist) == 0:
+        raise FileNotFoundError(
+            "Didn't find any snapshots with basename '" + snapshot_basename + "'"
+        )
+
+    snaplist = sorted(snaplist)
+
+    return snaplist
+
+
+def get_ion_mass_fractions(swiftsimio_loaded_data):
+    """
+    Returns the ion mass fractions according to
+    the used scheme.
+
+    swiftsimio_loaded_data: the swiftsimio.load() object
+    """
+
+    data = swiftsimio_loaded_data
+    meta = data.metadata
+    gas = data.gas
+    with_rt = True
+    scheme = None
+    try:
+        scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8"))
+    except KeyError:
+        # allow to read in solutions with only cooling, without RT
+        with_rt = False
+
+    if with_rt:
+        if scheme.startswith("GEAR M1closure"):
+            imf = data.gas.ion_mass_fractions
+        elif scheme.startswith("SPH M1closure"):
+            # atomic mass
+            mamu = {
+                "e": 0.0,
+                "HI": 1.0,
+                "HII": 1.0,
+                "HeI": 4.0,
+                "HeII": 4.0,
+                "HeIII": 4.0,
+            }
+            mass_function_hydrogen = data.gas.rt_element_mass_fractions.hydrogen
+            imf = copy.deepcopy(data.gas.rt_species_abundances)
+            named_columns = data.gas.rt_species_abundances.named_columns
+            for column in named_columns:
+                # abundance is in n_X/n_H unit. We convert it to mass fraction by multipling mass fraction of H
+                mass_function = (
+                    getattr(data.gas.rt_species_abundances, column)
+                    * mass_function_hydrogen
+                    * mamu[column]
+                )
+                setattr(imf, column, mass_function)
+        else:
+            raise ValueError("Unknown scheme", scheme)
+    else:
+        # try to find solutions for cooling only runs
+        imf = {
+            "HI": gas.hi[:],
+            "HII": gas.hii[:],
+            "HeI": gas.he_i[:],
+            "HeII": gas.he_ii[:],
+            "HeIII": gas.he_iii[:],
+        }
+
+    return imf
+
+
+def get_snapshot_data(snaplist):
+    """
+    Extract the relevant data from the list of snapshots.
+
+    Returns:
+        numpy arrays of:
+            time
+            temperatures
+            mean molecular weights
+            mass fractions
+    """
+
+    nsnaps = len(snaplist)
+    firstdata = swiftsimio.load(snaplist[0])
+    with_rt = True
+    try:
+        ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"][0])
+    except KeyError:
+        # allow to read in solutions with only cooling, without RT
+        with_rt = False
+
+    # Read internal units
+    unit_system = firstdata.metadata.units
+    mass_units = unit_system.mass
+    length_units = unit_system.length
+    time_units = unit_system.time
+
+    # Units derived from base
+    velocity_units = length_units / time_units
+    energy_units = mass_units * velocity_units ** 2
+    density_units = mass_units / length_units ** 3
+
+    scale_factors = np.zeros(nsnaps)
+    times = np.zeros(nsnaps) * unyt.Myr
+    temperatures = np.zeros(nsnaps) * unyt.K
+    mean_molecular_weights = np.zeros(nsnaps)
+    mass_fractions = np.zeros((nsnaps, 5))
+    internal_energies = np.zeros(nsnaps) * energy_units
+    specific_internal_energies = np.zeros(nsnaps) * energy_units / mass_units
+    densities = np.zeros(nsnaps) * density_units
+
+    if with_rt:
+        photon_energies = np.zeros((ngroups, nsnaps)) * energy_units
+    else:
+        photon_energies = None
+
+    for i, snap in enumerate(snaplist):
+
+        data = swiftsimio.load(snap)
+        gamma = data.gas.metadata.gas_gamma[0]
+        time = data.metadata.time.copy()
+        scale_factor = data.metadata.scale_factor.copy()
+
+        gas = data.gas
+        u = gas.internal_energies[:].to(energy_units / mass_units)
+        u.convert_to_physical()
+
+        rho = gas.densities
+        rho.convert_to_physical()
+        rho.to(density_units)
+        rho.convert_to_physical()
+
+        masses = gas.masses[:].to(mass_units)
+        masses.convert_to_physical()
+
+        imf = get_ion_mass_fractions(data)
+        mu = mean_molecular_weight(imf.HI, imf.HII, imf.HeI, imf.HeII, imf.HeIII)
+        mu.convert_to_physical()
+
+        T = gas_temperature(u, mu, gamma).to("K")
+        um = u.to(energy_units / mass_units) * masses
+        um.to(energy_units)
+
+        scale_factors[i] = scale_factor
+        times[i] = time.to("Myr")
+        temperatures[i] = np.mean(T)
+        mean_molecular_weights[i] = np.mean(mu)
+        internal_energies[i] = np.mean(um)
+        specific_internal_energies[i] = u.to(energy_units / mass_units).sum() / len(u)
+        densities[i] = rho.sum() / len(rho)
+
+        mass_fractions[i, 0] = np.mean(imf.HI)
+        mass_fractions[i, 1] = np.mean(imf.HII)
+        mass_fractions[i, 2] = np.mean(imf.HeI)
+        mass_fractions[i, 3] = np.mean(imf.HeII)
+        mass_fractions[i, 4] = np.mean(imf.HeIII)
+
+        if with_rt:
+            for g in range(ngroups):
+                en = getattr(data.gas.photon_energies, "group" + str(g + 1))
+                en = en[:].to_physical().to(energy_units)
+                photon_energies[g, i] = en.sum() / en.shape[0]
+
+    return (
+        scale_factors,
+        times,
+        temperatures,
+        mean_molecular_weights,
+        mass_fractions,
+        internal_energies,
+        photon_energies,
+        specific_internal_energies,
+        densities,
+    )
+
+
+def get_reference_data(reference_file="CosmoRTCoolingTestReference.txt"):
+    # Grab lines containing units
+    with open(reference_file) as f:
+        # Skip first line
+        f.readline()
+        mass_units_line = f.readline()
+        length_units_line = f.readline()
+        velocity_units_line = f.readline()
+
+    # Extract numbers from lines
+    mass_units = float(mass_units_line[18:-4]) * unyt.g
+    length_units = float(length_units_line[20:-5]) * unyt.cm
+    velocity_units = float(velocity_units_line[22:-7]) * unyt.cm / unyt.s
+
+    # Derived units
+    energy_units = mass_units * velocity_units ** 2
+    density_units = mass_units / length_units ** 3
+
+    # Read in data
+    refdata = np.loadtxt(reference_file, dtype=np.float64)
+
+    a_ref = refdata[:, 1]
+    t_ref = refdata[:, 3] * unyt.yr
+    dt_ref = refdata[:, 4] * unyt.yr
+    T_ref = refdata[:, 5] * unyt.K
+    mu_ref = refdata[:, 6]
+    rho_ref = refdata[:, 7] * density_units
+    rhoHI_ref = refdata[:, 8] * density_units
+    rhoHII_ref = refdata[:, 9] * density_units
+    rhoHeI_ref = refdata[:, 10] * density_units
+    rhoHeII_ref = refdata[:, 11] * density_units
+    rhoHeIII_ref = refdata[:, 12] * density_units
+    rhoe_ref = refdata[:, 13] * density_units
+    u_ref = refdata[:, -1] * energy_units / mass_units
+    mass_fraction_ref = np.empty((t_ref.shape[0], 5))
+    mass_fraction_ref[:, 0] = rhoHI_ref / rho_ref
+    mass_fraction_ref[:, 1] = rhoHII_ref / rho_ref
+    mass_fraction_ref[:, 2] = rhoHeI_ref / rho_ref
+    mass_fraction_ref[:, 3] = rhoHeII_ref / rho_ref
+    mass_fraction_ref[:, 4] = rhoHeIII_ref / rho_ref
+
+    return (
+        a_ref,
+        t_ref,
+        T_ref,
+        mu_ref,
+        mass_fraction_ref,
+        dt_ref,
+        rho_ref,
+        rhoHI_ref,
+        rhoHII_ref,
+        rhoHeI_ref,
+        rhoHeII_ref,
+        rhoHeIII_ref,
+        rhoe_ref,
+        u_ref,
+    )
+
+
+if __name__ == "__main__":
+
+    # Get list of shapshots
+    snaplist = get_snapshot_list(snapshot_base)
+
+    # Read snapshot data
+    a, t, T, mu, mass_fraction, u, photon_energies, us, rho = get_snapshot_data(
+        snaplist
+    )
+
+    with_rt = photon_energies is not None
+    if with_rt:
+        ngroups = photon_energies.shape[0]
+
+    # Read reference solution data
+    a_ref, t_ref, T_ref, mu_ref, mass_fraction_ref, *__ = get_reference_data(
+        reference_file
+    )
+
+    # Convert t_ref to Myr
+    t_ref.convert_to_units("Myr")
+
+    # Translate snapshot times to start at t=0
+    t -= t[0]
+
+    # Grab x-coordinate
+    if plot_time:
+        xcoords = t
+        xcoords_ref = t_ref
+        xlabel = "Time [Myr]"
+        xscale = "log"
+        xlims = (0.1, max(t))
+    else:
+        xcoords = a
+        xcoords_ref = a_ref
+        xlabel = "Scale factor [1]"
+        xscale = "linear"
+        xlims = (min(a), max(a))
+
+    # ------------------
+    # Plot figures
+    # ------------------
+
+    fig = plt.figure(figsize=(8, 8), dpi=300)
+    ax1 = fig.add_subplot(2, 2, 1)
+    ax2 = fig.add_subplot(2, 2, 2)
+    ax3 = fig.add_subplot(2, 2, 3)
+    ax4 = fig.add_subplot(2, 2, 4)
+
+    ax1.set_xscale(xscale)
+    ax2.set_xscale(xscale)
+    ax3.set_xscale(xscale)
+    ax4.set_xscale(xscale)
+
+    ax1.set_xlim(*xlims)
+    ax2.set_xlim(*xlims)
+    ax3.set_xlim(*xlims)
+    ax4.set_xlim(*xlims)
+
+    ax1.semilogy(xcoords_ref, T_ref, label="reference", **referenceplotkwargs)
+    ax1.semilogy(xcoords, T, label="obtained results")
+
+    ax1.set_yscale("log")
+    ax1.set_xlabel(xlabel)
+    ax1.set_ylabel("gas temperature [K]")
+    ax1.legend(prop=legendprops)
+    ax1.grid()
+
+    ax2.plot(xcoords_ref, mu_ref, label="reference", **referenceplotkwargs)
+    ax2.plot(xcoords, mu, label="obtained results")
+
+    ax2.set_xlabel(xlabel)
+    ax2.set_ylabel("mean molecular weight")
+    ax2.legend(prop=legendprops)
+    ax2.grid()
+
+    total_mass_fraction = np.sum(mass_fraction, axis=1)
+    ax3.plot(xcoords, total_mass_fraction, "k", label="total", ls="-")
+
+    ax3.plot(
+        xcoords_ref[1:],
+        mass_fraction_ref[1:, 0],
+        label="reference",
+        **referenceplotkwargs,
+        zorder=0,
+    )
+    ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 1], **referenceplotkwargs, zorder=0)
+    ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 2], **referenceplotkwargs, zorder=0)
+    ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 3], **referenceplotkwargs, zorder=0)
+    ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 4], **referenceplotkwargs, zorder=0)
+
+    ax3.plot(xcoords, mass_fraction[:, 0], label="HI", ls=":", **plotkwargs, zorder=1)
+    ax3.plot(xcoords, mass_fraction[:, 1], label="HII", ls="-.", **plotkwargs, zorder=1)
+    ax3.plot(xcoords, mass_fraction[:, 2], label="HeI", ls=":", **plotkwargs, zorder=1)
+    ax3.plot(
+        xcoords, mass_fraction[:, 3], label="HeII", ls="-.", **plotkwargs, zorder=1
+    )
+    ax3.plot(
+        xcoords, mass_fraction[:, 4], label="HeIII", ls="--", **plotkwargs, zorder=1
+    )
+    ax3.legend(loc="upper right", prop=legendprops)
+    ax3.set_xlabel(xlabel)
+    ax3.set_ylabel("gas mass fractions [1]")
+    ax3.grid()
+
+    if with_rt:
+        u.convert_to_units(energy_units)
+        photon_energies.convert_to_units(energy_units)
+        tot_energy = u + np.sum(photon_energies, axis=0)
+        ax4.plot(
+            xcoords,
+            tot_energy,
+            label=f"total energy budget",
+            color="k",
+            ls="--",
+            **plotkwargs,
+        )
+        for g in range(ngroups):
+            ax4.plot(
+                xcoords,
+                photon_energies[g, :],
+                label=f"photon energies group {g + 1}",
+                **plotkwargs,
+            )
+        ax4.plot(xcoords, u, label="gas internal energy", **plotkwargs)
+        ax4.set_xlabel(xlabel)
+        ax4.set_ylabel(
+            r"energy budget [$" + u.units.latex_representation() + "$]", usetex=True
+        )
+        ax4.legend(prop=legendprops)
+        ax4.grid()
+
+    plt.tight_layout()
+    #  plt.show()
+    plt.savefig("cooling_test.png")
diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/rt_cooling_test.yml b/examples/RadiativeTransferTests/CosmoCoolingTest/rt_cooling_test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..56b652072f5a8175a61992e9c6c934cd4e2a844e
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoCoolingTest/rt_cooling_test.yml
@@ -0,0 +1,81 @@
+MetaData:
+  run_name: RT Cooling Test
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98848e43    # 10^10 M_sun in grams
+  UnitLength_in_cgs:   3.08567758e21 # 1 kpc in cm
+  UnitVelocity_in_cgs: 1e5           # 1 km/s in cm/s
+  UnitCurrent_in_cgs:  1             # Amperes
+  UnitTemp_in_cgs:     1             # Kelvin
+
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  time_begin: 0.     # The starting time of the simulation (in internal units).
+  time_end:   0.100  # The end time of the simulation (in internal units).
+  dt_min:     1.e-8  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     4.882814e-03  # The maximal time-step size of the simulation (in internal units).
+
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output # Common part of the name of output files
+  output_list_on:      0
+  output_list:         snaplist_cooling
+  scale_factor_first:  0.047601     # Time of the first output (in internal units)
+  delta_time:          1.006
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.047601
+  delta_time:          1.006 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   0.       # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./cooling_test.hdf5  # The file to read
+  periodic:   1                    # periodic ICs
+
+GEARRT:
+  f_reduce_c: 1.e-9                                 # This test is without actual radiation, so we don't care about this
+  CFL_condition: 0.9                                # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [3.288e15, 5.945e15, 13.157e15] # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                   # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0., 0., 0.]     # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:   0.76                    # total hydrogen mass fraction
+  stellar_spectrum_type: 0                          # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17    # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  case_B_recombination: 0                           # reference solution assumes case A recombination
+
+
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5       # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 0                        # Enable or not the UV background
+  redshift: 0                                  # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 0                        # Enable or not the metal cooling
+  provide_volumetric_heating_rates: 0          # (optional) User provide volumetric heating rates
+  provide_specific_heating_rates: 0            # (optional) User provide specific heating rates
+  max_steps: 10000                             # (optional) Max number of step when computing the initial composition
+  convergence_limit: 1                      # (optional) Convergence threshold (relative) for initial composition
+  self_shielding_method: 0                    # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
+  primordial_chemistry: 1
+  thermal_time_myr: 5
+  maximal_density_Hpcm3: -1                   # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+Scheduler:
+  tasks_per_cell: 128
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.0476     # z~20
+  a_end:          0.2        # z~4
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/run.sh b/examples/RadiativeTransferTests/CosmoCoolingTest/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..890a4b745e973c212ac5b7ddb1b13d932607c2f4
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoCoolingTest/run.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -f ./cooling_test.hdf5 ]; then
+    echo "creating ICs"
+    python3 makeIC.py
+fi
+
+# Run SWIFT with RT
+../../../swift \
+    --hydro \
+    --cosmology \
+    --threads=4 \
+    --verbose=0  \
+    --radiation \
+    --external-gravity \
+    --stars \
+    --feedback \
+./rt_cooling_test.yml 2>&1 | tee output.log
+
+# Wanna run with cooling, but no RT? This should do the trick
+# ../../../swift \
+#     --hydro \
+#     --threads=4 \
+#     --verbose=0  \
+#     --cooling \
+#     ./rt_cooling_test.yml 2>&1 | tee output.log
+
+if [ ! -f "CosmoRTCoolingTestReference.txt" ]; then
+    ./getReference.sh
+fi
+python3 plotSolution.py
diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/README b/examples/RadiativeTransferTests/CosmoHeatingTest/README
new file mode 100644
index 0000000000000000000000000000000000000000..18ba3a98b493955657afb9b6cdc309af72fbb4cd
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoHeatingTest/README
@@ -0,0 +1,6 @@
+Runs a uniform box with radiation initially present. No further radiation 
+sources are present, and the gas should heat up and ionize, while the
+radiation fields should decrease at the same rate.
+
+To run with GEAR-RT, compile swift with:
+    --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/makeIC.py b/examples/RadiativeTransferTests/CosmoHeatingTest/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..813c0d8a4b9f66e3274a1f574212e0adbd3bd068
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoHeatingTest/makeIC.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+# -----------------------------------------------------------
+# Use 10 particles in each dimension to generate a uniform box
+# with a fixed amount of radiation.
+# -----------------------------------------------------------
+
+from swiftsimio import Writer
+from swiftsimio.units import cosmo_units
+
+import unyt
+import numpy as np
+import h5py
+import yaml
+
+# Grab scale factor at start of run
+with open(r"rt_heating_test.yml") as paramfile:
+    params = yaml.load(paramfile, Loader=yaml.FullLoader)
+    a_begin = params["Cosmology"]["a_begin"]
+    a_begin = float(a_begin)
+
+# Reduced speed of light
+f_reduce_speed_of_light = 1.0e-6
+
+# number of particles in each dimension
+n_p = 10
+nparts = n_p ** 3
+
+# filename of ICs to be generated
+outputfilename = "heating_test.hdf5"
+# adiabatic index
+gamma = 5.0 / 3.0
+# total hydrogen mass fraction
+XH = 0.76
+# total helium mass fraction
+XHe = 0.24
+# boxsize
+boxsize = 1 * unyt.kpc
+# initial gas temperature
+initial_temperature = 1e3 * unyt.K
+# particle mass
+pmass = (unyt.atomic_mass_unit / unyt.cm ** 3) * (boxsize ** 3 / nparts) * a_begin ** 3
+pmass = pmass.to("Msun")
+
+# -----------------------------------------------
+
+
+def internal_energy(T, mu):
+    """
+    Compute the internal energy of the gas for a given
+    temperature and mean molecular weight
+    """
+    # Using u = 1 / (gamma - 1) * p / rho
+    #   and p = N/V * kT = rho / (mu * m_u) * kT
+
+    u = unyt.boltzmann_constant * T / (gamma - 1) / (mu * unyt.atomic_mass_unit)
+    return u
+
+
+def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp):
+    """
+    Determines the mean molecular weight for given 
+    mass fractions of
+        hydrogen:   XH0
+        H+:         XHp
+        He:         XHe0
+        He+:        XHep
+        He++:       XHepp
+
+    returns:
+        mu: mean molecular weight [in atomic mass units]
+        NOTE: to get the actual mean mass, you still need
+        to multiply it by m_u, as is tradition in the formulae
+    """
+
+    # 1/mu = sum_j X_j / A_j * (1 + E_j)
+    # A_H    = 1, E_H    = 0
+    # A_Hp   = 1, E_Hp   = 1
+    # A_He   = 4, E_He   = 0
+    # A_Hep  = 4, E_Hep  = 1
+    # A_Hepp = 4, E_Hepp = 2
+    one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp
+
+    return 1.0 / one_over_mu
+
+
+# assume everything is neutral initially
+mu = mean_molecular_weight(XH, 0, XHe, 0.0, 0.0)
+u_part = internal_energy(initial_temperature, mu)
+pmass = pmass.to("Msun")
+
+
+xp = unyt.unyt_array(np.zeros((nparts, 3), dtype=np.float32), boxsize.units)
+dx = boxsize / n_p
+ind = 0
+for i in range(n_p):
+    x = (i + 0.5) * dx
+    for j in range(n_p):
+        y = (j + 0.5) * dx
+        for k in range(n_p):
+            z = (k + 0.5) * dx
+
+            xp[ind] = (x, y, z)
+            ind += 1
+
+w = Writer(cosmo_units, boxsize, dimension=3)
+
+
+w.gas.coordinates = xp
+w.gas.velocities = np.zeros(xp.shape, dtype=np.float32) * (unyt.km / unyt.s)
+w.gas.masses = np.ones(nparts, dtype=np.float32) * pmass
+w.gas.internal_energy = np.ones(nparts, dtype=np.float32) * u_part
+
+# Generate initial guess for smoothing lengths based on MIPS
+w.gas.generate_smoothing_lengths(boxsize=boxsize, dimension=3)
+
+# If IDs are not present, this automatically generates
+w.write(outputfilename)
+
+# Now open file back up again and add RT data.
+
+F = h5py.File(outputfilename, "r+")
+header = F["Header"]
+nparts = header.attrs["NumPart_ThisFile"][0]
+parts = F["/PartType0"]
+
+# Create initial ionization species mass fractions.
+# Assume everything neutral initially
+# NOTE: grackle doesn't really like exact zeroes, so
+# use something very small instead.
+HIdata = np.ones(nparts, dtype=np.float32) * XH
+HIIdata = np.ones(nparts, dtype=np.float32) * 1e-12
+HeIdata = np.ones(nparts, dtype=np.float32) * XHe
+HeIIdata = np.ones(nparts, dtype=np.float32) * 1e-12
+HeIIIdata = np.ones(nparts, dtype=np.float32) * 1e-12
+
+parts.create_dataset("MassFractionHI", data=HIdata)
+parts.create_dataset("MassFractionHII", data=HIIdata)
+parts.create_dataset("MassFractionHeI", data=HeIdata)
+parts.create_dataset("MassFractionHeII", data=HeIIdata)
+parts.create_dataset("MassFractionHeIII", data=HeIIIdata)
+
+
+# Add photon groups
+nPhotonGroups = 3
+
+# with this IC, the radiative cooling is negligible.
+#  photon_energy = u_part * pmass * 5.0
+#  photon_energy = np.arange(1, nPhotonGroups+1) * photon_energy
+
+# Fluxes from the Iliev Test0 part3
+fluxes_iliev = np.array([1.350e1, 2.779e1, 6.152e0]) * unyt.erg / unyt.s / unyt.cm ** 2
+energy_density = fluxes_iliev / unyt.c
+photon_energy = energy_density * boxsize ** 3 / nparts * a_begin ** 3
+photon_energy = photon_energy * f_reduce_speed_of_light
+
+photon_energy.convert_to_units(cosmo_units["energy"])
+photon_fluxes = 0.333333 * unyt.c * photon_energy
+photon_fluxes.convert_to_units(
+    cosmo_units["energy"] * cosmo_units["length"] / cosmo_units["time"]
+)
+
+
+for grp in range(nPhotonGroups):
+    dsetname = "PhotonEnergiesGroup{0:d}".format(grp + 1)
+    energydata = np.ones(nparts, dtype=np.float32) * photon_energy[grp]
+    parts.create_dataset(dsetname, data=energydata)
+
+    dsetname = "PhotonFluxesGroup{0:d}".format(grp + 1)
+    fluxdata = np.zeros((nparts, 3), dtype=np.float32)
+    fluxdata[:, 0] = photon_fluxes[grp]
+    parts.create_dataset(dsetname, data=fluxdata)
+
+# close up, and we're done!
+F.close()
diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/plotSolution.py b/examples/RadiativeTransferTests/CosmoHeatingTest/plotSolution.py
new file mode 100755
index 0000000000000000000000000000000000000000..c596c24946d0e8e25fb23b18024c9f5290a95155
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoHeatingTest/plotSolution.py
@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# -------------------------------------------
+# Plot the gas temperature, mean molecular
+# weight, and mass fractions
+# -------------------------------------------
+
+import copy
+import os
+
+import numpy as np
+import swiftsimio
+import unyt
+from matplotlib import pyplot as plt
+from matplotlib import ticker as mticker
+
+# arguments for plots of results
+plotkwargs = {"alpha": 0.5}
+# arguments for plot of references
+referenceplotkwargs = {"color": "grey", "lw": 4, "alpha": 0.6}
+# arguments for legends
+legendprops = {"size": 8}
+# snapshot basenames
+snapshot_base = "output"
+# Plot time on x-axis
+plot_time = False
+
+# -----------------------------------------------------------------------
+
+energy_units = unyt.Msun * unyt.kpc ** 2 / unyt.kyr ** 2
+mass_units = unyt.Msun
+
+
+def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp):
+    """
+    Determines the mean molecular weight for given 
+    mass fractions of
+        hydrogen:   XH0
+        H+:         XHp
+        He:         XHe0
+        He+:        XHep
+        He++:       XHepp
+
+    returns:
+        mu: mean molecular weight [in atomic mass units]
+        NOTE: to get the actual mean mass, you still need
+        to multiply it by m_u, as is tradition in the formulae
+    """
+
+    # 1/mu = sum_j X_j / A_j * (1 + E_j)
+    # A_H    = 1, E_H    = 0
+    # A_Hp   = 1, E_Hp   = 1
+    # A_He   = 4, E_He   = 0
+    # A_Hep  = 4, E_Hep  = 1
+    # A_Hepp = 4, E_Hepp = 2
+    one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp
+
+    return 1.0 / one_over_mu
+
+
+def gas_temperature(u, mu, gamma):
+    """
+    Compute the gas temperature given the specific internal 
+    energy u and the mean molecular weight mu
+    """
+
+    # Using u = 1 / (gamma - 1) * p / rho
+    #   and p = N/V * kT = rho / (mu * m_u) * kT
+
+    T = u * (gamma - 1) * mu * unyt.atomic_mass_unit / unyt.boltzmann_constant
+
+    return T
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshot(s) that are to be plotted 
+    and return their names as list
+    """
+
+    snaplist = []
+
+    dirlist = os.listdir()
+    for f in dirlist:
+        if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+            snaplist.append(f)
+
+    if len(snaplist) == 0:
+        raise FileNotFoundError(
+            "Didn't find any snapshots with basename '" + snapshot_basename + "'"
+        )
+
+    snaplist = sorted(snaplist)
+
+    return snaplist
+
+
+def get_ion_mass_fractions(swiftsimio_loaded_data):
+    """
+    Returns the ion mass fractions according to
+    the used scheme.
+
+    swiftsimio_loaded_data: the swiftsimio.load() object
+    """
+
+    data = swiftsimio_loaded_data
+    meta = data.metadata
+    try:
+        scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8"))
+    except KeyError:
+        raise ValueError("This test needs to be run with RT on.")
+
+    if scheme.startswith("GEAR M1closure"):
+        imf = data.gas.ion_mass_fractions
+    elif scheme.startswith("SPH M1closure"):
+        # atomic mass
+        mamu = {"e": 0.0, "HI": 1.0, "HII": 1.0, "HeI": 4.0, "HeII": 4.0, "HeIII": 4.0}
+        mass_function_hydrogen = data.gas.rt_element_mass_fractions.hydrogen
+        imf = copy.deepcopy(data.gas.rt_species_abundances)
+        named_columns = data.gas.rt_species_abundances.named_columns
+        for column in named_columns:
+            # abundance is in n_X/n_H unit. We convert it to mass fraction by multipling mass fraction of H
+            mass_function = (
+                getattr(data.gas.rt_species_abundances, column)
+                * mass_function_hydrogen
+                * mamu[column]
+            )
+            setattr(imf, column, mass_function)
+    else:
+        raise ValueError("Unknown scheme", scheme)
+
+    return imf
+
+
+def get_snapshot_data(snaplist):
+    """
+    Extract the relevant data from the list of snapshots.
+
+    Returns:
+        numpy arrays of:
+            time
+            temperatures 
+            mean molecular weights
+            mass fractions
+    """
+
+    nsnaps = len(snaplist)
+    firstdata = swiftsimio.load(snaplist[0])
+    ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"])
+
+    scale_factors = np.zeros(nsnaps)
+    times = np.zeros(nsnaps) * unyt.Myr
+    temperatures = np.zeros(nsnaps) * unyt.K
+    mean_molecular_weights = np.zeros(nsnaps) * unyt.atomic_mass_unit
+    mass_fractions = np.zeros((nsnaps, 5))
+    internal_energies = np.zeros(nsnaps) * energy_units
+    photon_energies = np.zeros((ngroups, nsnaps)) * energy_units
+
+    time_first = firstdata.metadata.time.copy()
+    for i, snap in enumerate(snaplist):
+
+        data = swiftsimio.load(snap)
+        gamma = data.gas.metadata.gas_gamma[0]
+        time = data.metadata.time.copy()
+        scale_factor = data.metadata.scale_factor
+        gas = data.gas
+
+        u = gas.internal_energies.to(energy_units / mass_units)
+        u.convert_to_physical()
+        u_phys_cgs = u.to(unyt.erg / unyt.g)
+        masses = gas.masses.to(mass_units)
+        imf = get_ion_mass_fractions(data)
+        mu = mean_molecular_weight(imf.HI, imf.HII, imf.HeI, imf.HeII, imf.HeIII)
+        mu.convert_to_physical()
+        T = gas_temperature(u_phys_cgs, mu, gamma).to("K")
+        um = u.to(energy_units / mass_units) * masses
+
+        scale_factors[i] = scale_factor
+        times[i] = time.to("Myr")
+        temperatures[i] = np.mean(T)
+        mean_molecular_weights[i] = np.mean(mu)
+        internal_energies[i] = np.mean(um)
+
+        mass_fractions[i, 0] = np.mean(imf.HI)
+        mass_fractions[i, 1] = np.mean(imf.HII)
+        mass_fractions[i, 2] = np.mean(imf.HeI)
+        mass_fractions[i, 3] = np.mean(imf.HeII)
+        mass_fractions[i, 4] = np.mean(imf.HeIII)
+
+        for g in range(ngroups):
+            en = getattr(data.gas.photon_energies, "group" + str(g + 1))
+            en = en.to(energy_units)
+            photon_energies[g, i] = en.sum() / en.shape[0]
+
+    return (
+        scale_factors,
+        times,
+        temperatures,
+        mean_molecular_weights,
+        mass_fractions,
+        internal_energies,
+        photon_energies,
+    )
+
+
+if __name__ == "__main__":
+    # ------------------
+    # Plot figures
+    # ------------------
+
+    snaplist = get_snapshot_list(snapshot_base)
+    a, t, T, mu, mass_fraction, u, photon_energies = get_snapshot_data(snaplist)
+    ngroups = photon_energies.shape[0]
+
+    if plot_time:
+        xcoords = t
+        xlabel = "Time [Myr]"
+        xscale = "log"
+    else:
+        xcoords = a
+        xlabel = "Scale factor [1]"
+        xscale = "linear"
+
+    fig = plt.figure(figsize=(8, 8), dpi=300)
+    ax1 = fig.add_subplot(2, 2, 1)
+    ax2 = fig.add_subplot(2, 2, 2)
+    ax3 = fig.add_subplot(2, 2, 3)
+    ax4 = fig.add_subplot(2, 2, 4)
+
+    ax1.semilogy(xcoords, T, label="obtained results")
+    ax1.set_xlabel(xlabel)
+    ax1.set_ylabel("gas temperature [K]")
+    ax1.legend(prop=legendprops)
+    ax1.grid()
+
+    ax2.plot(xcoords, mu, label="obtained results")
+    ax2.set_xlabel(xlabel)
+    ax2.set_ylabel("mean molecular weight")
+    ax2.legend(prop=legendprops)
+    ax2.grid()
+
+    total_mass_fraction = np.sum(mass_fraction, axis=1)
+    ax3.plot(xcoords, total_mass_fraction, "k", label="total", ls="-")
+
+    ax3.plot(xcoords, mass_fraction[:, 0], label="HI", ls=":", **plotkwargs, zorder=1)
+    ax3.plot(xcoords, mass_fraction[:, 1], label="HII", ls="-.", **plotkwargs, zorder=1)
+    ax3.plot(xcoords, mass_fraction[:, 2], label="HeI", ls=":", **plotkwargs, zorder=1)
+    ax3.plot(
+        xcoords, mass_fraction[:, 3], label="HeII", ls="-.", **plotkwargs, zorder=1
+    )
+    ax3.plot(
+        xcoords, mass_fraction[:, 4], label="HeIII", ls="--", **plotkwargs, zorder=1
+    )
+    ax3.legend(loc="upper right", prop=legendprops)
+    ax3.set_xlabel(xlabel)
+    ax3.set_ylabel("gas mass fractions [1]")
+    ax3.grid()
+
+    tot_energy = u + np.sum(photon_energies, axis=0)
+    ax4.plot(
+        xcoords,
+        tot_energy,
+        label=f"total energy budget",
+        color="k",
+        ls="--",
+        **plotkwargs,
+    )
+    for g in range(ngroups):
+        ax4.plot(
+            xcoords,
+            photon_energies[g, :],
+            label=f"photon energies group {g+1}",
+            **plotkwargs,
+        )
+    ax4.plot(xcoords, u, label=r"gas internal energy", **plotkwargs)
+
+    ax4.set_yscale("log")
+    ax4.set_xlabel(xlabel)
+    ax4.set_ylabel(
+        r"energy budget [$" + energy_units.units.latex_representation() + "$]",
+        usetex=True,
+    )
+    ax4.legend(prop=legendprops)
+    ax4.grid()
+
+    for ax in fig.axes:
+        ax.set_xscale(xscale)
+        ax.xaxis.set_minor_formatter(mticker.ScalarFormatter())
+
+    plt.tight_layout()
+    plt.savefig("heating_test.png")
diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/rt_heating_test.yml b/examples/RadiativeTransferTests/CosmoHeatingTest/rt_heating_test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e0b91d8b80f7e86986fbcc6d84ab8895541f8b08
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoHeatingTest/rt_heating_test.yml
@@ -0,0 +1,65 @@
+MetaData:
+  run_name: Heating Test
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98848e33    # 1 M_sun in grams
+  UnitLength_in_cgs:   3.08567758e18 # 1 pc in cm
+  UnitVelocity_in_cgs: 1e5           # 1 km/s in cm/s
+  UnitCurrent_in_cgs:  1             # Amperes
+  UnitTemp_in_cgs:     1             # Kelvin
+
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  time_begin: 0.     # The starting time of the simulation (in internal units).
+  dt_min:     1.0208453377e-08  # 0.01 yr
+  dt_max:     0.0010208453      # 1 kyr
+  time_end:   4.10084           # 4 Myr
+
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output # Common part of the name of output files
+  scale_factor_first:  0.00990099     # Time of the first output (in internal units)
+  output_list_on:      0  # (Optional) Enable the output list
+  output_list:         snaplist.txt # (Optional) File containing the output times (see documentation in "Parameter File" section)
+  delta_time:          1.001
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  time_first:          0.00990099
+  delta_time:          1.001
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./heating_test.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+GEARRT:
+  f_reduce_c: 1e-6                                  # We don't care about the radiation propagation in this test, so let's speed things up
+  CFL_condition: 0.9                                # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [3.288e15, 5.945e15, 13.157e15] # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                   # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [1., 1., 1.]     # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  set_equilibrium_initial_ionization_mass_fractions: 0   # (Optional) set the initial ionization fractions depending on gas temperature assuming ionization equilibrium.
+  hydrogen_mass_fraction:   0.76                    # total hydrogen mass fraction
+  stellar_spectrum_type: 1                          # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_blackbody_temperature_K: 1.e5    # (Conditional) if stellar_spectrum_type=1, use this temperature (in K) for the blackbody spectrum. 
+  case_B_recombination: 0                           # (Optional) use case B recombination interaction rates.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.00990099  # z~100
+  a_end:          0.011      # z~90
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
+
diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/run.sh b/examples/RadiativeTransferTests/CosmoHeatingTest/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..7a0a1e545963188c2bd3bfeaa04b3177e1de39a5
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoHeatingTest/run.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -f ./heating_test.hdf5 ]; then
+    echo "creating ICs"
+    python3 makeIC.py
+fi
+
+# Run SWIFT with RT
+../../../swift \
+    --hydro \
+    --cosmology \
+    --threads=4 \
+    --verbose=0  \
+    --radiation \
+    --external-gravity \
+    --stars \
+    --feedback \
+    ./rt_heating_test.yml 2>&1 | tee output.log
+
+python3 plotSolution.py
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/README b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/README
new file mode 100644
index 0000000000000000000000000000000000000000..5ce885e6fe84b375e91521fa6899a91881e52bc7
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/README
@@ -0,0 +1,14 @@
+Strömgen Sphere example in 3D
+-----------------------------
+
+This directory contains the following example:
+    -   run a propagation test of photons emitted from a single 
+        central source in an otherwise uniform box.
+        To run this example, use the provided `runPropagationTest.sh` script.
+        This script will then make use of the `makePropagationTestIC.py` and
+        `plotPhotonPropagationCheck.py` script.
+             
+
+To use the GEAR RT model, compile with :
+    --with-rt=GEAR_1 --with-rt-riemann-solver=GLF --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
+
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/getGlass.sh b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/getGlass.sh
new file mode 100755
index 0000000000000000000000000000000000000000..50f27f4e9f9981da9449608bd70669c56d9d3985
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/getGlass.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/glassCube_64.hdf5
+#wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/glassCube_128.hdf5
\ No newline at end of file
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/makePropagationTestIC.py b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/makePropagationTestIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..49e24b65e7d74649fc151dab17169b030b629ad2
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/makePropagationTestIC.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2022 Tsang Keung Chan (chantsangkeung@gmail.com)
+#               2024 Stan Verhoeve (s06verhoeve@gmail.com)
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+# ---------------------------------------------------------------------
+# Add a single star in the center of a glass distribution
+# Intended for the photon propagation test.
+# ---------------------------------------------------------------------
+
+import h5py
+import numpy as np
+import unyt
+from swiftsimio import Writer
+from swiftsimio.units import cosmo_units
+
+glass = h5py.File("glassCube_64.hdf5", "r")
+parts = glass["PartType0"]
+xp = parts["Coordinates"][:]
+h = parts["SmoothingLength"][:]
+glass.close()
+
+# replace the particle closest to the center
+# by the star
+r = np.sqrt(np.sum((0.5 - xp) ** 2, axis=1))
+rmin = np.argmin(r)
+mininds = np.argsort(r)
+center_parts = xp[mininds[:4]]
+xs = center_parts.sum(axis=0) / center_parts.shape[0]
+
+# Double-check all particles for boundaries
+for i in range(3):
+    mask = xp[:, i] < 0.0
+    xp[mask, i] += 1.0
+    mask = xp[:, i] > 1.0
+    xp[mask, i] -= 1.0
+
+unitL = cosmo_units["length"]
+edgelen = (2 * 260 * unyt.Mpc).to(unitL)
+boxsize = unyt.unyt_array([edgelen.v, edgelen.v, edgelen.v], unitL)
+
+xs = unyt.unyt_array(
+    [np.array([xs[0] * edgelen, xs[1] * edgelen, xs[2] * edgelen])], unitL
+)
+xp *= edgelen
+h *= edgelen
+
+w = Writer(unit_system=cosmo_units, box_size=boxsize, dimension=3)
+
+w.gas.coordinates = xp
+w.stars.coordinates = xs
+w.gas.velocities = np.zeros(xp.shape) * (unyt.cm / unyt.s)
+w.stars.velocities = np.zeros(xs.shape) * (unyt.cm / unyt.s)
+w.gas.masses = np.ones(xp.shape[0], dtype=float) * 1e1 * unyt.Msun
+w.stars.masses = np.ones(xs.shape[0], dtype=float) * 100.0 * unyt.Msun
+w.gas.internal_energy = (
+    np.ones(xp.shape[0], dtype=float) * (300.0 * unyt.kb * unyt.K) / unyt.g
+)
+
+w.gas.smoothing_length = h
+w.stars.smoothing_length = w.gas.smoothing_length[:1]
+
+w.write("propagationTest-3D.hdf5")
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/plotPhotonPropagationCheck.py b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/plotPhotonPropagationCheck.py
new file mode 100755
index 0000000000000000000000000000000000000000..04c154314899b24a36f451775f413caec70c72e9
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/plotPhotonPropagationCheck.py
@@ -0,0 +1,598 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+#               2022 Tsang Keung Chan (chantsangkeung@gmail.com)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+# ----------------------------------------------------------------------
+# plots
+#   - radiation energies of particles as function of radius
+#   - magnitude of radiation fluxes of particles as function of radius
+#   - total energy in radial bins
+#   - total vectorial sum of fluxes in radial bins
+# and compare with expected propagation speed solution.
+# Usage:
+#   give snapshot number as cmdline arg to plot
+#   single snapshot, otherwise this script plots
+#   all snapshots available in the workdir.
+#   Make sure to select the photon group to plot that
+#   doesn't interact with gas to check the *propagation*
+#   correctly.
+# ----------------------------------------------------------------------
+
+import gc
+import os
+import sys
+
+import matplotlib as mpl
+import numpy as np
+import swiftsimio
+import unyt
+from matplotlib import pyplot as plt
+from scipy import stats
+from scipy.optimize import curve_fit
+
+import stromgren_plotting_tools as spt
+
+# Parameters users should/may tweak
+
+# snapshot basename
+snapshot_base = "propagation_test"
+
+time_first = 0
+
+# additional anisotropy estimate plot?
+plot_anisotropy_estimate = False
+
+# which photon group to use.
+# NOTE: array index, not group number (which starts at 1 for GEAR)
+group_index = 0
+
+scatterplot_kwargs = {
+    "alpha": 0.1,
+    "s": 1,
+    "marker": ".",
+    "linewidth": 0.0,
+    "facecolor": "blue",
+}
+
+lineplot_kwargs = {"linewidth": 2}
+
+# -----------------------------------------------------------------------
+
+# Read in cmdline arg: Are we plotting only one snapshot, or all?
+plot_all = False
+try:
+    snapnr = int(sys.argv[1])
+except IndexError:
+    plot_all = True
+
+mpl.rcParams["text.usetex"] = True
+
+
+def analytical_integrated_energy_solution(L, time, r, rmax):
+    """
+    Compute analytical solution for the sum of the energy
+    in bins for given injection rate <L> at time <time> 
+    at bin edges <r> and maximal radius <rmax>
+    """
+
+    r_center = 0.5 * (r[:-1] + r[1:])
+    r0 = r[0]
+    Etot = L * time
+
+    if rmax == 0:
+        return r_center, np.zeros(r.shape[0] - 1) * Etot.units
+
+    E = np.zeros(r.shape[0] - 1) * Etot.units
+    mask = r_center <= rmax
+    E[mask] = Etot / (rmax - r0) * (r[1:] - r[:-1])[mask]
+
+    return r_center, E
+
+
+def analytical_energy_solution(L, time, r, rmax):
+    """
+    Compute analytical solution for the energy distribution
+    for given injection rate <L> at time <time> at radii <r>
+    """
+
+    r_center = 0.5 * (r[:-1] + r[1:])
+    r0 = r[0]
+    Etot = L * time
+
+    if rmax == 0:
+        return r_center, np.zeros(r.shape[0] - 1) * Etot.units
+
+    E_fraction_bin = np.zeros(r.shape[0] - 1) * Etot.units
+    mask = r_center <= rmax
+    dr = r[1:] ** 2 - r[:-1] ** 2
+    E_fraction_bin[mask] = 1.0 / (rmax ** 2 - r0 ** 2) * dr[mask]
+    bin_surface = dr
+    total_weight = Etot / np.sum(E_fraction_bin / bin_surface)
+    E = E_fraction_bin / bin_surface * total_weight
+
+    return r_center, E
+
+
+def analytical_flux_magnitude_solution(L, time, r, rmax, scheme):
+    """
+    For radiation that doesn't interact with the gas, the
+    flux should correspond to the free streaming (optically
+    thin) limit. So compute and return that.
+    """
+    r, E = analytical_energy_solution(L, time, r, rmax)
+    if scheme.startswith("GEAR M1closure"):
+        F = unyt.c.to(r.units / time.units) * E / r.units ** 3
+    elif scheme.startswith("SPH M1closure"):
+        F = unyt.c.to(r.units / time.units) * E
+    else:
+        raise ValueError("Unknown scheme", scheme)
+
+    return r, F
+
+
+def x2(x, a, b):
+    return a * x ** 2 + b
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshot(s) that are to be plotted 
+    and return their names as list
+    """
+
+    snaplist = []
+
+    if plot_all:
+        dirlist = os.listdir()
+        for f in dirlist:
+            if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+                snaplist.append(f)
+
+        snaplist = sorted(snaplist)
+
+    else:
+        fname = snapshot_basename + "_" + str(snapnr).zfill(4) + ".hdf5"
+        if not os.path.exists(fname):
+            print("Didn't find file", fname)
+            quit(1)
+        snaplist.append(fname)
+
+    return snaplist
+
+
+def plot_photons(filename, emin, emax, fmin, fmax):
+    """
+    Create the actual plot.
+
+    filename: file to work with
+    emin: list of minimal nonzero energy of all snapshots
+    emax: list of maximal energy of all snapshots
+    fmin: list of minimal flux magnitude of all snapshots
+    fmax: list of maximal flux magnitude of all snapshots
+    """
+    global time_first
+    print("working on", filename)
+
+    # Read in data first
+    data = swiftsimio.load(filename)
+    meta = data.metadata
+    scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8"))
+    boxsize = meta.boxsize
+    edgelen = min(boxsize[0], boxsize[1])
+
+    xstar = data.stars.coordinates
+    xpart = data.gas.coordinates
+    dxp = xpart - xstar
+    r = np.sqrt(np.sum(dxp ** 2, axis=1))
+
+    time = meta.time.copy()
+    # Take care of simulation time not starting at 0
+    if time_first == 0:
+        time_first = time
+        time = 0 * unyt.Gyr
+    else:
+        time -= time_first
+
+    r_expect = time * meta.reduced_lightspeed
+
+    L = None
+
+    use_const_emission_rates = False
+    if scheme.startswith("GEAR M1closure"):
+        luminosity_model = meta.parameters["GEARRT:stellar_luminosity_model"]
+        use_const_emission_rates = luminosity_model.decode("utf-8") == "const"
+    elif scheme.startswith("SPH M1closure"):
+        use_const_emission_rates = bool(
+            meta.parameters["SPHM1RT:use_const_emission_rates"]
+        )
+    else:
+        print("Error: Unknown RT scheme " + scheme)
+        exit()
+
+    if use_const_emission_rates:
+        # read emission rate parameter as string
+        if scheme.startswith("GEAR M1closure"):
+            const_emission_rates = (
+                spt.trim_paramstr(
+                    meta.parameters["GEARRT:const_stellar_luminosities_LSol"].decode(
+                        "utf-8"
+                    )
+                )
+                * unyt.L_Sun
+            )
+            L = const_emission_rates[group_index]
+        elif scheme.startswith("SPH M1closure"):
+            units = data.units
+            unit_l_in_cgs = units.length.in_cgs()
+            unit_v_in_cgs = (units.length / units.time).in_cgs()
+            unit_m_in_cgs = units.mass.in_cgs()
+            const_emission_rates = (
+                spt.trim_paramstr(
+                    meta.parameters["SPHM1RT:star_emission_rates"].decode("utf-8")
+                )
+                * unit_m_in_cgs
+                * unit_v_in_cgs ** 3
+                / unit_l_in_cgs
+            )
+            L = const_emission_rates[group_index]
+        else:
+            print("Error: Unknown RT scheme " + scheme)
+            exit()
+
+    if plot_anisotropy_estimate:
+        ncols = 4
+    else:
+        ncols = 3
+    fig = plt.figure(figsize=(5 * ncols, 5.5), dpi=200)
+
+    nbins = 100
+    r_bin_edges = np.linspace(0.5 * edgelen * 1e-3, 0.507 * edgelen, nbins + 1)
+    r_bin_centres = 0.5 * (r_bin_edges[1:] + r_bin_edges[:-1])
+    r_analytical_bin_edges = np.linspace(
+        0.5 * edgelen * 1e-6, 0.507 * edgelen, nbins + 1
+    )
+
+    # --------------------------
+    # Read in and process data
+    # --------------------------
+
+    energies = getattr(data.gas.photon_energies, "group" + str(group_index + 1))
+    Fx = getattr(data.gas.photon_fluxes, "Group" + str(group_index + 1) + "X")
+    Fy = getattr(data.gas.photon_fluxes, "Group" + str(group_index + 1) + "Y")
+    Fz = getattr(data.gas.photon_fluxes, "Group" + str(group_index + 1) + "Z")
+
+    fmag = np.sqrt(Fx ** 2 + Fy ** 2 + Fz ** 2)
+    particle_count, _ = np.histogram(
+        r,
+        bins=r_analytical_bin_edges,
+        range=(r_analytical_bin_edges[0], r_analytical_bin_edges[-1]),
+    )
+    L = L.to(energies.units / time.units)
+
+    xlabel_units_str = boxsize.units.latex_representation()
+    energy_units_str = energies.units.latex_representation()
+    flux_units_str = Fx.units.latex_representation()
+
+    # ------------------------
+    # Plot photon energies
+    # ------------------------
+    ax1 = fig.add_subplot(1, ncols, 1)
+    ax1.set_title("Particle Radiation Energies")
+    ax1.set_ylabel("Photon Energy [$" + energy_units_str + "$]")
+
+    # don't expect more than float precision
+    emin_to_use = max(emin, 1e-5 * emax)
+
+    if use_const_emission_rates:
+        # plot entire expected solution
+        rA, EA = analytical_energy_solution(L, time, r_analytical_bin_edges, r_expect)
+
+        mask = particle_count > 0
+        if mask.any():
+            EA = EA[mask].to(energies.units)
+            rA = rA[mask]
+            pcount = particle_count[mask]
+
+            # the particle bin counts will introduce noise.
+            # So use a linear fit for the plot. I assume here
+            # that the particle number per bin increases
+            # proprtional to r^2, which should roughly be the
+            # case for the underlying glass particle distribution.
+            popt, _ = curve_fit(x2, rA, pcount)
+            ax1.plot(
+                rA,
+                EA / x2(rA.v, popt[0], popt[1]),
+                **lineplot_kwargs,
+                linestyle="--",
+                c="red",
+                label="Analytical Solution",
+            )
+
+    else:
+        # just plot where photon front should be
+        ax1.plot(
+            [r_expect, r_expect],
+            [emin_to_use, emax * 1.1],
+            label="expected photon front",
+            color="red",
+        )
+
+    ax1.scatter(r, energies, **scatterplot_kwargs)
+    energies_binned, _, _ = stats.binned_statistic(
+        r,
+        energies,
+        statistic="mean",
+        bins=r_bin_edges,
+        range=(r_bin_edges[0], r_bin_edges[-1]),
+    )
+    ax1.plot(
+        r_bin_centres, energies_binned, **lineplot_kwargs, label="Mean Radiation Energy"
+    )
+    ax1.set_ylim(emin_to_use, emax * 1.1)
+
+    # ------------------------------
+    # Plot binned photon energies
+    # ------------------------------
+    ax2 = fig.add_subplot(1, ncols, 2)
+    ax2.set_title("Total Radiation Energy in radial bins")
+    ax2.set_ylabel("Total Photon Energy [$" + energy_units_str + "$]")
+
+    energies_summed_bin, _, _ = stats.binned_statistic(
+        r,
+        energies,
+        statistic="sum",
+        bins=r_bin_edges,
+        range=(r_bin_edges[0], r_bin_edges[-1]),
+    )
+    ax2.plot(
+        r_bin_centres,
+        energies_summed_bin,
+        **lineplot_kwargs,
+        label="Total Energy in Bin",
+    )
+    current_ylims = ax2.get_ylim()
+    ax2.set_ylim(emin_to_use, current_ylims[1])
+
+    if use_const_emission_rates:
+        # plot entire expected solution
+        # Note: you need to use the same bins as for the actual results
+        rA, EA = analytical_integrated_energy_solution(L, time, r_bin_edges, r_expect)
+
+        ax2.plot(
+            rA,
+            EA.to(energies.units),
+            **lineplot_kwargs,
+            linestyle="--",
+            c="red",
+            label="Analytical Solution",
+        )
+    else:
+        # just plot where photon front should be
+        ax2.plot(
+            [r_expect, r_expect],
+            ax2.get_ylim(r),
+            label="Expected Photon Front",
+            color="red",
+        )
+
+    # ------------------------------
+    # Plot photon fluxes
+    # ------------------------------
+    ax3 = fig.add_subplot(1, ncols, 3)
+    ax3.set_title("Particle Radiation Flux Magnitudes")
+    ax3.set_ylabel("Photon Flux Magnitude [$" + flux_units_str + "$]")
+
+    fmin_to_use = max(fmin, 1e-5 * fmax)
+    ax3.set_ylim(fmin_to_use, fmax * 1.1)
+
+    ax3.scatter(r, fmag, **scatterplot_kwargs)
+
+    fmag_mean_bin, _, _ = stats.binned_statistic(
+        r,
+        fmag,
+        statistic="mean",
+        bins=r_bin_edges,
+        range=(r_bin_edges[0], r_bin_edges[-1]),
+    )
+    ax3.plot(
+        r_bin_centres,
+        fmag_mean_bin,
+        **lineplot_kwargs,
+        label="Mean Radiation Flux of particles",
+    )
+
+    if use_const_emission_rates:
+        # plot entire expected solution
+        rA, FA = analytical_flux_magnitude_solution(
+            L, time, r_analytical_bin_edges, r_expect, scheme
+        )
+
+        mask = particle_count > 0
+        if mask.any():
+            FA = FA[mask].to(Fx.units)
+            rA = rA[mask]
+            pcount = particle_count[mask]
+
+            # the particle bin counts will introduce noise.
+            # So use a linear fit for the plot. I assume here
+            # that the particle number per bin increases
+            # proprtional to r^2, which should roughly be the
+            # case for the underlying glass particle distribution.
+            popt, _ = curve_fit(x2, rA, pcount)
+
+            ax3.plot(
+                rA,
+                FA / x2(rA.v, popt[0], popt[1]),
+                **lineplot_kwargs,
+                linestyle="--",
+                c="red",
+                label="analytical solution",
+            )
+
+    else:
+        # just plot where photon front should be
+        ax1.plot(
+            [r_expect, r_expect],
+            [emin_to_use, emax * 1.1],
+            label="expected photon front",
+            color="red",
+        )
+
+    # ------------------------------
+    # Plot photon flux sum
+    # ------------------------------
+
+    if plot_anisotropy_estimate:
+        ax4 = fig.add_subplot(1, ncols, 4)
+        ax4.set_title("Vectorial Sum of Radiation Flux in radial bins")
+        ax4.set_ylabel("[1]")
+
+        fmag_sum_bin, _, _ = stats.binned_statistic(
+            r,
+            fmag,
+            statistic="sum",
+            bins=r_bin_edges,
+            range=(r_bin_edges[0], r_bin_edges[-1]),
+        )
+        mask_sum = fmag_sum_bin > 0
+        fmag_max_bin, _, _ = stats.binned_statistic(
+            r,
+            fmag,
+            statistic="max",
+            bins=r_bin_edges,
+            range=(r_bin_edges[0], r_bin_edges[-1]),
+        )
+        mask_max = fmag_max_bin > 0
+        Fx_sum_bin, _, _ = stats.binned_statistic(
+            r,
+            Fx,
+            statistic="sum",
+            bins=r_bin_edges,
+            range=(r_bin_edges[0], r_bin_edges[-1]),
+        )
+        Fy_sum_bin, _, _ = stats.binned_statistic(
+            r,
+            Fy,
+            statistic="sum",
+            bins=r_bin_edges,
+            range=(r_bin_edges[0], r_bin_edges[-1]),
+        )
+        F_sum_bin = np.sqrt(Fx_sum_bin ** 2 + Fy_sum_bin ** 2)
+
+        ax4.plot(
+            r_bin_centres[mask_sum],
+            F_sum_bin[mask_sum] / fmag_sum_bin[mask_sum],
+            **lineplot_kwargs,
+            label=r"$\left| \sum_{i \in \mathrm{particles \ in \ bin}} \mathbf{F}_i \\right| $ "
+            + r"/ $\sum_{i \in \mathrm{particles \ in \ bin}} \left| \mathbf{F}_{i} \\right| $",
+        )
+        ax4.plot(
+            r_bin_centres[mask_max],
+            F_sum_bin[mask_max] / fmag_max_bin[mask_max],
+            **lineplot_kwargs,
+            linestyle="--",
+            label=r"$\left| \sum_{i \in \mathrm{particles \ in \ bin}} \mathbf{F}_i \\right| $ "
+            + r" / $\max_{i \in \mathrm{particles \ in \ bin}} \left| \mathbf{F}_{i} \\right| $",
+        )
+
+    # -------------------------------------------
+    # Cosmetics that all axes have in common
+    # -------------------------------------------
+    for ax in fig.axes:
+        ax.set_xlabel("r [$" + xlabel_units_str + "$]")
+        ax.set_yscale("log")
+        ax.set_xlim(0.0, 0.501 * edgelen)
+        ax.legend(fontsize="x-small")
+
+    # Add title
+    title = filename.replace("_", r"\_")  # exception handle underscore for latex
+    if meta.cosmology is not None:
+        title += ", $z$ = {0:.2e}".format(meta.z)
+    title += ", $t$ = {0:.2e}".format(1 * meta.time)
+    fig.suptitle(title)
+
+    plt.tight_layout()
+    figname = filename[:-5]
+    figname += "-PhotonPropagation.png"
+    plt.savefig(figname)
+    plt.close()
+    gc.collect()
+
+    return
+
+
+def get_plot_boundaries(filenames):
+    """
+    Get minimal and maximal nonzero photon energy values
+    """
+
+    data = swiftsimio.load(filenames[0])
+    energies = (
+        1 * getattr(data.gas.photon_energies, "group" + str(group_index + 1))
+    ).value
+    emaxguess = energies.max()
+
+    emin = emaxguess
+    emax = 0.0
+    fmagmin = 1e30
+    fmagmax = -10.0
+
+    for f in filenames:
+        data = swiftsimio.load(f)
+
+        energies = (
+            1 * getattr(data.gas.photon_energies, "group" + str(group_index + 1))
+        ).value
+        mask = energies > 0.0
+
+        if mask.any():
+
+            nonzero_energies = energies[mask]
+            this_emin = nonzero_energies.min()
+            emin = min(this_emin, emin)
+
+            this_emax = energies.max()
+            emax = max(emax, this_emax)
+
+        fx = (
+            1 * getattr(data.gas.photon_fluxes, "Group" + str(group_index + 1) + "X")
+        ).value
+        fy = (
+            1 * getattr(data.gas.photon_fluxes, "Group" + str(group_index + 1) + "Y")
+        ).value
+        fmag = np.sqrt(fx ** 2 + fy ** 2)
+
+        fmagmin = min(fmagmin, fmag.min())
+        fmagmax = max(fmagmax, fmag.max())
+
+    return emin, emax, fmagmin, fmagmax
+
+
+if __name__ == "__main__":
+
+    print(
+        "REMINDER: Make sure you selected the correct photon group",
+        "to plot, which is hardcoded in this script.",
+    )
+    snaplist = get_snapshot_list(snapshot_base)
+    emin, emax, fmagmin, fmagmax = get_plot_boundaries(snaplist)
+
+    for f in snaplist:
+        plot_photons(f, emin, emax, fmagmin, fmagmax)
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/propagationTest-3D.yml b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/propagationTest-3D.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d64ea4c0a6f500084f5814281e8f2f7770505a1e
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/propagationTest-3D.yml
@@ -0,0 +1,67 @@
+MetaData:
+  run_name: StromgrenSpherePropagationTest3D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     2.e-02  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            propagation_test_low_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_low_redshift
+  scale_factor_first:  0.93
+  delta_time:          1.02
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.93
+  delta_time:          1.02 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./propagationTest-3D.hdf5     # The file to read
+  periodic:   1                             # peridioc ICs. Keep them periodic so we don't loose photon energy.
+
+GEARRT:
+  f_reduce_c: 1.                                    # reduce the speed of light for the RT solver by multiplying c with this factor
+  CFL_condition: 0.99
+  photon_groups_Hz: [0.]                            # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                   # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [1.]             # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                     # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 0                          # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_const_max_frequency_Hz: 1.e17    # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  set_initial_ionization_mass_fractions: 1          # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                            # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                             # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                            # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+  skip_thermochemistry: 1                           # skip thermochemistry.
+  stars_max_timestep: 1.562500e-05                  # (Optional) restrict the maximal timestep of stars to this value (in internal units). Set to negative to turn off.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.93
+  a_end:          1.
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
+
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/runPropagationTest.sh b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/runPropagationTest.sh
new file mode 100755
index 0000000000000000000000000000000000000000..02756b7971e32084304af7359d1b22f34085bc66
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/runPropagationTest.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -e glassCube_64.hdf5 ]
+then
+    echo "Fetching initial glass file for Strömgen Sphere 3D example ..."
+    ./getGlass.sh
+fi
+
+if [ ! -f 'propagationTest-3D.hdf5' ]; then
+    echo "Generating ICs"
+    python3 makePropagationTestIC.py
+fi
+
+# Run SWIFT with RT
+../../../swift \
+    --hydro  \
+    --cosmology \
+    --threads=4 \
+    --stars \
+    --external-gravity \
+    --feedback \
+    --radiation \
+    ./propagationTest-3D.yml 2>&1 | tee output.log
+
+# Plot the photon propagation checks.
+# Make sure you set the correct photon group to plot
+# inside the script
+python3 ./plotPhotonPropagationCheck.py
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/snapshot_times_low_redshift b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/snapshot_times_low_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e58febac09ba12e5677c6fc8abafe94cc31824ad
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/snapshot_times_low_redshift
@@ -0,0 +1,12 @@
+# Redshift
+0.0751932785953775
+0.06869649828543634
+0.06225945012746337
+0.055881161353223074
+0.04956068264358926
+0.04329708741463545
+0.03708947113706218
+0.030936950674366415
+0.02483866364616727
+0.018793767817153695
+0.01280144050017884
diff --git a/examples/RadiativeTransferTests/CosmoPropagationTest_3D/stromgren_plotting_tools.py b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/stromgren_plotting_tools.py
new file mode 120000
index 0000000000000000000000000000000000000000..2121a14d08c7cfcd516d98ec9338f0f46ee7358f
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoPropagationTest_3D/stromgren_plotting_tools.py
@@ -0,0 +1 @@
+../StromgrenSphere_3D/stromgren_plotting_tools.py
\ No newline at end of file
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/README b/examples/RadiativeTransferTests/CosmoUniformBox_3D/README
new file mode 100644
index 0000000000000000000000000000000000000000..0c2a147a53c08de58ef854f148953154920c8e4e
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/README
@@ -0,0 +1,6 @@
+A simple box with static particles on a grid, and uniform radiation energy density everywhere. 
+Test that total radiation energy and radiation energy density dilute with the correct scale-factor relation.
+
+The ICs are created to be compatible with GEAR_RT. Recommended configuration:
+    --with-rt=GEAR_11 --with-rt-riemann-solver=GLF --with-hydro-dimension=3 --with-hydro=gizmo-mfv \
+     --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/makeIC.py b/examples/RadiativeTransferTests/CosmoUniformBox_3D/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..aee6dc2f158488cae735690fddcc7bf2c5db07e3
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/makeIC.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+import h5py
+import numpy as np
+import unyt
+from swiftsimio import Writer
+from swiftsimio.units import cosmo_units
+
+# Unit system we're working with
+unitsystem = cosmo_units
+
+# Box is 260 Mpc in each direction
+boxsize = 260 * unyt.Mpc
+boxsize = boxsize.to(unitsystem["length"])
+
+reduced_speed_of_light_fraction = 1.0
+
+# Number of photon groups
+nPhotonGroups = 11
+
+# Number of particles in each dimension
+# Total number of particles is thus n_p^3
+n_p = 10
+
+# Filename of ICs to be generated
+outputfilename = "uniform_3D.hdf5"
+
+
+def initial_condition(unitsystem):
+    """
+    The initial conditions of the uniform box
+    
+    unitsystem: The unit system to use for IC
+
+    returns:
+    E: Photon energy density
+    F: Photon flux
+    """
+    # you can make the photon quantities unitless, the units will
+    # already have been written down in the writer.
+    # However, that means that you need to convert them manually.
+
+    unit_energy = (
+        unitsystem["mass"] * unitsystem["length"] ** 2 / unitsystem["time"] ** 2
+    )
+    unit_velocity = unitsystem["length"] / unitsystem["time"]
+    unit_flux = unit_energy * unit_velocity
+
+    c_internal = (unyt.c * reduced_speed_of_light_fraction).to(unit_velocity)
+
+    # Uniform energy
+    E = np.ones((n_p ** 3), dtype=np.float64) * unit_energy
+
+    # Assuming all photons flow in only one direction
+    # (optically thin regime, "free streaming limit"),
+    # we have that |F| = c * E
+    fluxes = np.zeros((3, n_p ** 3), dtype=np.float64)
+    fluxes[0] *= (E * c_internal / 1.73205).to(unit_flux)  # sqrt(3)
+    fluxes[1] *= (E * c_internal / 1.73205).to(unit_flux)  # sqrt(3)
+    fluxes[2] *= (E * c_internal / 1.73205).to(unit_flux)  # sqrt(3)
+
+    return E, fluxes.T
+
+
+if __name__ in ("__main__"):
+    # Coordinate array
+    coords = np.zeros((n_p ** 3, 3), dtype=np.float64)
+
+    # Calculate grid of evenly spaced coordinates
+    coords_per_dim = np.linspace(0.5, n_p - 0.5, n_p)
+    grid = np.meshgrid(coords_per_dim, coords_per_dim, coords_per_dim)
+
+    for i in range(3):
+        coords[:, i] = grid[i].flatten()
+
+    # Calculate and apply grid spacing
+    dx = boxsize / n_p
+    coords *= dx
+
+    w = Writer(unitsystem, boxsize, dimension=3)
+
+    w.gas.coordinates = coords
+    w.gas.velocities = np.zeros((n_p ** 3, 3)) * (unyt.cm / unyt.s)
+
+    mpart = 1e20 * unyt.M_sun
+    mpart = mpart.to(unitsystem["mass"])
+    w.gas.masses = np.ones(n_p ** 3, dtype=np.float64) * mpart
+    w.gas.internal_energy = (
+        np.ones(n_p ** 3, dtype=np.float64) * (300.0 * unyt.kb * unyt.K) / unyt.g
+    )
+
+    # Generate initial guess for smoothing lengths based on MIPS
+    w.gas.generate_smoothing_lengths(boxsize=boxsize, dimension=3)
+
+    # If IDs are not present, this automatically generates
+    w.write(outputfilename)
+
+    # Now open file back up again and add photon groups
+    # you can make them unitless, the units have already been
+    # written down in the writer, In this case, it's cosmo_units
+
+    F = h5py.File(outputfilename, "r+")
+    header = F["Header"]
+    nparts = header.attrs["NumPart_ThisFile"][0]
+    parts = F["/PartType0"]
+
+    for i in range(nPhotonGroups):
+        # Generate initial conditions
+        E, fluxes = initial_condition(unitsystem)
+
+        # Create photon energy data entry
+        dsetname = f"PhotonEnergiesGroup{i+1}"
+        energydata = np.zeros((nparts), dtype=np.float32)
+        parts.create_dataset(dsetname, data=E)
+
+        # Create photon fluxes data entry
+        dsetname = f"PhotonFluxesGroup{i+1}"
+        fluxdata = np.zeros((nparts, 3), dtype=np.float32)
+        parts.create_dataset(dsetname, data=fluxes)
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/plotSolution.py b/examples/RadiativeTransferTests/CosmoUniformBox_3D/plotSolution.py
new file mode 100755
index 0000000000000000000000000000000000000000..549c16d84313b822be98e2ec9e880163e5988fe6
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/plotSolution.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import argparse
+
+import matplotlib as mpl
+import numpy as np
+import swiftsimio
+from matplotlib import pyplot as plt
+from scipy.optimize import curve_fit as cf
+
+# Parameters users should/may tweak
+snapshot_base = "output"  # snapshot basename
+plot_physical_quantities = True
+
+nPhotonGroups = 11
+
+mpl.rcParams["text.usetex"] = True
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-z", "--redshift", help="Redshift domain to plot advection for", default="high"
+    )
+
+    args = parser.parse_args()
+    return args
+
+
+def get_snapshot_list(snapshot_basename="output"):
+    """
+    Find the snapshots that are to be plotted 
+    and return their names as list
+    """
+    snaplist = []
+
+    dirlist = os.listdir()
+    for f in dirlist:
+        if f.startswith(snapshot_basename) and f.endswith("hdf5"):
+            snaplist.append(f)
+
+    snaplist = sorted(snaplist)
+    if len(snaplist) == 0:
+        print(f"No snapshots with base {snapshot_basename} found!")
+        sys.exit(1)
+    return snaplist
+
+
+def plot_param_over_time(
+    snapshot_list, param="energy density", redshift_domain="high_redshift"
+):
+    print(f"Now plotting {param} over time")
+
+    # Arrays to keep track of plot_param and scale factor
+    plot_param = [[], []]
+    scale_factor = []
+    analytic_exponent = [0, 0]
+
+    # Functions to convert between scale factor and redshift
+    a2z = lambda a: 1 / a - 1
+    z2a = lambda z: 1 / (z + 1)
+
+    for file in snapshot_list:
+        data = swiftsimio.load(file)
+        meta = data.metadata
+
+        # Read comoving quantities
+        energy = data.gas.photon_energies.group1
+        for i in range(nPhotonGroups - 1):
+            energy += getattr(data.gas.photon_energies, f"group{i+2}")
+        mass = data.gas.masses
+        rho = data.gas.densities
+        vol = mass / rho
+        energy_density = energy / vol
+
+        if plot_physical_quantities:
+            physical_energy_density = energy_density.to_physical()
+            physical_mass = mass.to_physical()
+            physical_vol = vol.to_physical()
+            physical_energy = physical_energy_density * physical_vol
+            if param == "energy density":
+                plot_param[1].append(
+                    1
+                    * np.sum(physical_energy_density)
+                    / physical_energy_density.shape[0]
+                )
+                analytic_exponent[1] = -4.0
+            elif param == "total energy":
+                plot_param[1].append(1 * np.sum(physical_energy))
+                analytic_exponent[1] = -1.0
+
+        if param == "energy density":
+            plot_param[0].append(1 * np.sum(energy_density) / energy_density.shape[0])
+            analytic_exponent[0] = -1.0
+        elif param == "total energy":
+            plot_param[0].append(1 * np.sum(energy))
+            analytic_exponent[0] = -1.0
+
+        scale_factor.append(meta.scale_factor)
+
+    fig = plt.figure(figsize=(5.05 * (1 + plot_physical_quantities), 5.4), dpi=200)
+
+    x = np.linspace(min(scale_factor), max(scale_factor), 1000)
+
+    if param == "energy density":
+        titles = ["Comoving energy density", "Physical energy density"]
+        ylabel = "Average energy density"
+        figname = f"output_energy_density_over_time-{redshift_domain}.png"
+    elif param == "total energy":
+        titles = ["Comoving total energy", "Physical total energy"]
+        ylabel = "Total energy"
+        figname = f"output_total_energy_over_time-{redshift_domain}.png"
+
+    for i in range(1 + plot_physical_quantities):
+        ax = fig.add_subplot(1, (1 + plot_physical_quantities), (1 + i))
+        ax.scatter(scale_factor, plot_param[i], label="Simulation")
+
+        # Analytic scale-factor relation
+        analytic = x ** analytic_exponent[i]
+
+        # Scale solution to correct offset
+        analytic = analytic / analytic[0] * plot_param[i][0]
+        ax.plot(
+            x,
+            analytic,
+            c="r",
+            label=f"Analytic solution $\propto a^{{{analytic_exponent[i]}}}$",
+        )
+
+        ax.legend()
+        ax.set_title(titles[i] + " sum of all groups")
+
+        ax.set_xlabel("Scale factor")
+        secax = ax.secondary_xaxis("top", functions=(a2z, z2a))
+        secax.set_xlabel("Redshift")
+
+        ax.yaxis.get_offset_text().set_position((-0.05, 1))
+
+        if analytic_exponent[i] == 0.0:
+            ax.set_ylim(plot_param[i][0] * 0.95, plot_param[i][0] * 1.05)
+        if i == 0:
+            units = plot_param[i][0].units.latex_representation()
+            ax.set_ylabel(f"{ylabel} [${units}$]")
+
+    plt.tight_layout()
+    plt.savefig(figname)
+    plt.close()
+
+
+if __name__ in ("__main__"):
+    # Get command line args
+    args = parse_args()
+    domain = args.redshift.lower()
+    if domain in ("low", "l", "low_redshift", "low redshift", "low-redshift"):
+        redshift_domain = "low_redshift"
+    elif domain in (
+        "medium",
+        "m",
+        "medium_redshift",
+        "medium redshift",
+        "medium-redshift",
+    ):
+        redshift_domain = "medium_redshift"
+    elif domain in ("high", "h", "high_redshift", "high redshift", "high-redshift"):
+        redshift_domain = "high_redshift"
+    else:
+        print("Redshift domain not recognised!")
+        sys.exit(1)
+
+    snaplist = get_snapshot_list(snapshot_base + f"_{redshift_domain}")
+    if len(snaplist) < 1:
+        print("No snapshots found!")
+        exit(1)
+
+    for param in ["energy density", "total energy"]:
+        plot_param_over_time(snaplist, param, redshift_domain)
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_high_redshift.yml b/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_high_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a258a9d92d5dbc2e70a6b1897e74bbc848e75e59
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_high_redshift.yml
@@ -0,0 +1,68 @@
+MetaData:
+  run_name: cosmo_RT_uniform_box-3D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_high_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_high_redshift
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.00990099
+  delta_time:          1.06   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./uniform_3D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0                       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99                             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [0, 3.2880e+15, 6.5760e+15, 9.8640e+15, 1.3152e+16, 1.6440e+16, 1.9728e+16, 2.3016e+16, 2.6304e+16, 2.9592e+16, 3.2880e+16]  # Hz
+  stellar_luminosity_model: const                 # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0, 2.221e+04, 2.020e+04, 1.153e+04, 5.122e+03, 1.952e+03, 6.705e+02, 2.140e+02, 6.461e+01, 1.869e+01, 7.158e+00]
+  hydrogen_mass_fraction:  0.76                   # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 1                        # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_blackbody_temperature_K: 1.e6  # K
+  stellar_spectrum_const_max_frequency_Hz: 1.e17  # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. 
+  set_initial_ionization_mass_fractions: 1        # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+  skip_thermochemistry: 1                         # Skip thermochemsitry.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.00990099  # z~100
+  a_end:          0.01408451  # z~70
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_low_redshift.yml b/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_low_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fc07cfe7dd5da62b99f6e54afc116eb99b3cf8a2
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_low_redshift.yml
@@ -0,0 +1,67 @@
+MetaData:
+  run_name: cosmo_RT_uniform_box-3D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_low_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_low_redshift
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.93
+  delta_time:          1.005   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./uniform_3D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0                       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99                             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [0, 3.2880e+15, 6.5760e+15, 9.8640e+15, 1.3152e+16, 1.6440e+16, 1.9728e+16, 2.3016e+16, 2.6304e+16, 2.9592e+16, 3.2880e+16]  # Hz
+  stellar_luminosity_model: const                 # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0, 2.221e+04, 2.020e+04, 1.153e+04, 5.122e+03, 1.952e+03, 6.705e+02, 2.140e+02, 6.461e+01, 1.869e+01, 7.158e+00]
+  hydrogen_mass_fraction: 0.76                   # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 1                        # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_blackbody_temperature_K: 1.e6 # (Optinal) Blackbody temperature of the spectrum
+  set_initial_ionization_mass_fractions: 1        # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+  skip_thermochemistry: 1                         # Skip thermochemsitry.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.93 
+  a_end:          1.     
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_medium_redshift.yml b/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_medium_redshift.yml
new file mode 100644
index 0000000000000000000000000000000000000000..412be5b56e32109ec11def92dc349f1591d57581
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/rt_uniform3D_medium_redshift.yml
@@ -0,0 +1,67 @@
+MetaData:
+  run_name: cosmo_RT_uniform_box-3D
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # Mpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  dt_min:     1.e-17  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output_medium_redshift # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_medium_redshift
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  scale_factor_first:  0.0909
+  delta_time:          1.02   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./uniform_3D.hdf5  # The file to read
+  periodic:   1                     # periodic ICs
+
+Restarts:
+  delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
+
+GEARRT:
+  f_reduce_c: 1.                                  # reduce the speed of light for the RT solver by multiplying c with this factor
+  f_limit_cooling_time: 0.0                       # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  CFL_condition: 0.99                             # CFL condition for RT, independent of hydro
+  photon_groups_Hz: [0, 3.2880e+15, 6.5760e+15, 9.8640e+15, 1.3152e+16, 1.6440e+16, 1.9728e+16, 2.3016e+16, 2.6304e+16, 2.9592e+16, 3.2880e+16]  # Hz
+  stellar_luminosity_model: const                 # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0, 2.221e+04, 2.020e+04, 1.153e+04, 5.122e+03, 1.952e+03, 6.705e+02, 2.140e+02, 6.461e+01, 1.869e+01, 7.158e+00]
+  hydrogen_mass_fraction: 0.76                   # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  stellar_spectrum_type: 1                        # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum.
+  stellar_spectrum_blackbody_temperature_K: 1.e6 # (Optinal) Blackbody temperature of the spectrum
+  set_initial_ionization_mass_fractions: 1        # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  mass_fraction_HI: 0.76                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HI mass fractions to this value
+  mass_fraction_HII: 0.                           # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HII mass fractions to this value
+  mass_fraction_HeI: 0.24                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeI mass fractions to this value
+  mass_fraction_HeII: 0.                          # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeII mass fractions to this value
+  mass_fraction_HeIII: 0.                         # (Conditional) If overwrite_initial_ionization_fractions=1, needed to set initial HeIII mass fractions to this value
+  skip_thermochemistry: 1                         # Skip thermochemsitry.
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.0909    # z=10
+  a_end:          0.1428571 # z=6
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/run.sh b/examples/RadiativeTransferTests/CosmoUniformBox_3D/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..42c200af6c300fa2913a907cab764b22c0fadb4a
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/run.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+# exit if anything fails
+set -e
+set -o pipefail
+
+# Generate the initial conditions if they are not present.
+if [ ! -e uniform_3D.hdf5 ]
+then
+    echo "Generating initial conditions for the 3D uniform box example..."
+    python3 makeIC.py
+fi
+
+# Default run
+ymlfile=rt_uniform3D_high_redshift.yml
+zdomain="h"
+
+# Do we have a cmdline argument provided?
+if [ $# -gt 0 ]; then
+    case "$1" in
+    -l | -low | --l | --low | l | ./rt_uniform3D_low_redshift.yml | rt_uniform3D_low_redshift | rt_uniform3D_low_redshift.yml )
+        ymlfile=rt_uniform3D_low_redshift.yml
+	zdomain="l"
+        ;;
+    -m | -mid | --m | --mid | m | ./rt_uniform3D_medium_redshift.yml | rt_uniform3D_medium_redshift | rt_uniform3D_medium_redshift.yml )
+        ymlfile=rt_uniform3D_medium_redshift.yml
+	zdomain="m"
+        ;;
+    -h | -high | -hi | --h | --hi | --high | h | ./rt_uniform3D_high_redshift.yml | rt_uniform3D_high_redshift | rt_uniform3D_high_redshift.yml )
+        ymlfile=rt_uniform3D_high_redshift.yml
+	zdomain="h"
+        ;;
+    *)
+        echo unknown cmdline param, running default $ymlfile
+        ;;
+    esac
+fi
+
+
+# Run SWIFT with RT and cosmology
+../../../swift \
+    --hydro \
+    --cosmology \
+    --threads=4 \
+    --verbose=0  \
+    --radiation \
+    --stars \
+    --feedback \
+    --external-gravity \
+    $ymlfile 2>&1 | tee output.log
+
+python3 ./plotSolution.py -z $zdomain
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_high_redshift b/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_high_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e504014177837a7b2c1d134d1c4a0d5a785d0044
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_high_redshift
@@ -0,0 +1,12 @@
+# Redshift
+99.98460572382712
+96.47031888378449
+93.23367859034735
+90.25299848922855
+87.49735694548497
+84.9407463291136
+82.56112036003019
+80.33965676782498
+78.26018022924342
+76.30870609169932
+74.47307621234665
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_low_redshift b/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_low_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e58febac09ba12e5677c6fc8abafe94cc31824ad
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_low_redshift
@@ -0,0 +1,12 @@
+# Redshift
+0.0751932785953775
+0.06869649828543634
+0.06225945012746337
+0.055881161353223074
+0.04956068264358926
+0.04329708741463545
+0.03708947113706218
+0.030936950674366415
+0.02483866364616727
+0.018793767817153695
+0.01280144050017884
diff --git a/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_medium_redshift b/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_medium_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..958165894af540df676443cfdf3fbf20ce46dd1c
--- /dev/null
+++ b/examples/RadiativeTransferTests/CosmoUniformBox_3D/snapshot_times_medium_redshift
@@ -0,0 +1,12 @@
+# Redshift
+9.983090485639307
+9.499283920730043
+8.92297287774699
+8.419654113369706
+7.975570397565402
+7.580294600085116
+7.225767198868688
+6.905651913777566
+6.614892571630509
+6.349401127100822
+6.1058334302500645
diff --git a/examples/RadiativeTransferTests/HeatingTest/plotSolution.py b/examples/RadiativeTransferTests/HeatingTest/plotSolution.py
index 2c6cee439cde01eed6161f026582e74d95f99373..95e3338e37b16ee0fe6f75965fe0928cf2533ee2 100755
--- a/examples/RadiativeTransferTests/HeatingTest/plotSolution.py
+++ b/examples/RadiativeTransferTests/HeatingTest/plotSolution.py
@@ -49,7 +49,7 @@ mass_units = unyt.Msun
 
 def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp):
     """
-    Determines the mean molecular weight for given 
+    Determines the mean molecular weight for given
     mass fractions of
         hydrogen:   XH0
         H+:         XHp
@@ -76,7 +76,7 @@ def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp):
 
 def gas_temperature(u, mu, gamma):
     """
-    Compute the gas temperature given the specific internal 
+    Compute the gas temperature given the specific internal
     energy u and the mean molecular weight mu
     """
 
@@ -90,7 +90,7 @@ def gas_temperature(u, mu, gamma):
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -155,14 +155,14 @@ def get_snapshot_data(snaplist):
     Returns:
         numpy arrays of:
             time
-            temperatures 
+            temperatures
             mean molecular weights
             mass fractions
     """
 
     nsnaps = len(snaplist)
     firstdata = swiftsimio.load(snaplist[0])
-    ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"][0])
 
     times = np.zeros(nsnaps) * unyt.Myr
     temperatures = np.zeros(nsnaps) * unyt.K
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml
index 1bf391f4a5da004dbe6cdf967fa7b0db9fb729ad..89271db00a87eddb8f76f496b7e0e2486c9acd28 100644
--- a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml
@@ -13,7 +13,7 @@ InternalUnitSystem:
 TimeIntegration:
   max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
-  time_end:   0.01   # end time: radiation reaches edge of box
+  time_end:   1   # end time: radiation reaches edge of box
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
   dt_max:     1.e-03  # The maximal time-step size of the simulation (in internal units).
 
@@ -22,7 +22,7 @@ TimeIntegration:
 Snapshots:
   basename:            output # Common part of the name of output files
   time_first:          0.     # Time of the first output (in internal units)
-  delta_time:          0.001 # Time between snapshots
+  delta_time:          0.1 # Time between snapshots
 
 # Parameters governing the conserved quantities statistics
 Statistics:
@@ -47,7 +47,7 @@ GEARRT:
   photon_groups_Hz: [3.288e15, 5.945e15, 13.157e15] # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
   stellar_luminosity_model: const                   # Which model to use to determine the stellar luminosities.
   const_stellar_luminosities_LSol: [0., 0., 0.]     # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
-  hydrogen_mass_fraction:  0.50                     # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  hydrogen_mass_fraction:  0.76                     # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
   set_equilibrium_initial_ionization_mass_fractions: 0   # (Optional) set the initial ionization fractions depending on gas temperature assuming ionization equilibrium.
   set_initial_ionization_mass_fractions: 0          # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
   stellar_spectrum_type: 1                          # Which radiation spectrum to use. 0: constant over all frequencies. 1: blackbody spectrum.
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions_cosmo.yml b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions_cosmo.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e4debac1573ff0678b604b2670b5e007b45fc562
--- /dev/null
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions_cosmo.yml
@@ -0,0 +1,63 @@
+MetaData:
+  run_name: advect_ions
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.98841586e+33 # 1 M_Sol
+  UnitLength_in_cgs:   3.08567758e21 # kpc in cm
+  UnitVelocity_in_cgs: 1.e5 # km/s
+  UnitCurrent_in_cgs:  1.
+  UnitTemp_in_cgs:     1. # K
+
+# Parameters governing the time integration
+TimeIntegration:
+  max_nr_rt_subcycles: 1
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   0.01   # end time: radiation reaches edge of box
+  dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1.e-03  # The maximal time-step size of the simulation (in internal units).
+
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            cosmo_output # Common part of the name of output files
+  output_list_on:      1
+  output_list:         snapshot_times_high_redshift
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  time_first:          0.00990099
+  delta_time:          1.06 # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.9      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   10.      # Kelvin
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advect_ions.hdf5     # The file to read
+  periodic:   1                      # periodic ICs. Keep them periodic so we don't loose photon energy.
+
+GEARRT:
+  f_reduce_c: 1.e-5                                 # reduce the speed of light for the RT solver by multiplying c with this factor
+  CFL_condition: 0.9
+  f_limit_cooling_time: 0.0                         # (Optional) multiply the cooling time by this factor when estimating maximal next time step. Set to 0.0 to turn computation of cooling time off.
+  photon_groups_Hz: [3.288e15, 5.945e15, 13.157e15] # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N
+  stellar_luminosity_model: const                   # Which model to use to determine the stellar luminosities.
+  const_stellar_luminosities_LSol: [0., 0., 0.]     # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity.
+  hydrogen_mass_fraction:  0.76                     # total hydrogen (H + H+) mass fraction in the metal-free portion of the gas
+  set_equilibrium_initial_ionization_mass_fractions: 0   # (Optional) set the initial ionization fractions depending on gas temperature assuming ionization equilibrium.
+  set_initial_ionization_mass_fractions: 0          # (Optional) manually overwrite initial mass fraction of each species (using the values you set below)
+  stellar_spectrum_type: 1                          # Which radiation spectrum to use. 0: constant over all frequencies. 1: blackbody spectrum.
+  stellar_spectrum_blackbody_temperature_K: 1.e5    # (Conditional) if stellar_spectrum_type=1, use this temperature (in K) for the blackbody spectrum.
+  skip_thermochemistry: 1
+
+Cosmology:        # Planck13 (EAGLE flavour)
+  a_begin:        0.00990099  # z~100
+  a_end:          0.01408451  # z~70
+  h:              0.6777
+  Omega_cdm:      0.2587481
+  Omega_lambda:   0.693
+  Omega_b:        0.0482519
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/makeIC.py b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/makeIC.py
index 0f5621e826d5aa99ada36f38046e7fd70137d2ca..8c59ad795d0b360648fb9b35d47e285aa92f5457 100755
--- a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/makeIC.py
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/makeIC.py
@@ -43,7 +43,7 @@ if __name__ == "__main__":
 
     # Set up metadata
     unitL = unyt.Mpc
-    edgelen = 2 * 15 * 1e-3 * unitL  # 30 kpc
+    edgelen = 1 * unyt.Mpc
     edgelen = edgelen.to(unitL)
     boxsize = np.array([1.0, 1.0, 0.0]) * edgelen
 
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/plotIonization.py b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/plotIonization.py
index 57f79fc99b2e333184ba342c531444fcf690574b..e76b9d6cbf1ebb85543ac09e7584a87380e4dec6 100755
--- a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/plotIonization.py
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/plotIonization.py
@@ -31,13 +31,12 @@ import swiftsimio
 from matplotlib import pyplot as plt
 from matplotlib.colors import SymLogNorm
 from mpl_toolkits.axes_grid1 import make_axes_locatable
+import argparse
 
 # Parameters users should/may tweak
 
 # plot all groups and all photon quantities
 plot_all_data = True
-# snapshot basename
-snapshot_base = "output"
 # fancy up the plots a bit?
 fancy = True
 
@@ -49,15 +48,20 @@ projection_kwargs = {"resolution": 512, "parallel": True}
 
 # -----------------------------------------------------------------------
 
-
+mpl.rcParams["text.usetex"] = True
 # Read in cmdline arg: Are we plotting only one snapshot, or all?
 plot_all = False
-try:
-    snapnr = int(sys.argv[1])
-except IndexError:
-    plot_all = True
 
-mpl.rcParams["text.usetex"] = True
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-n", "--snapshot-number", help="Number of snapshot to plot", type=int
+    )
+    parser.add_argument("-b", "--basename", help="Snapshot basename", default="output")
+
+    args = parser.parse_args()
+    return args
 
 
 def get_snapshot_list(snapshot_basename="output"):
@@ -75,6 +79,9 @@ def get_snapshot_list(snapshot_basename="output"):
                 snaplist.append(f)
 
         snaplist = sorted(snaplist)
+        if len(snaplist) == 0:
+            print(f"No snapshots with base '{snapshot_basename}' found")
+            sys.exit(1)
 
     else:
         fname = snapshot_basename + "_" + str(snapnr).zfill(4) + ".hdf5"
@@ -224,7 +231,16 @@ def plot_ionization(filename):
 
 
 if __name__ == "__main__":
+    # Get command line arguments
+    args = parse_args()
+
+    if args.snapshot_number:
+        plot_all = False
+        snapnr = int(args.snapshot_numbeR)
+    else:
+        plot_all = True
 
+    snapshot_base = args.basename
     snaplist = get_snapshot_list(snapshot_base)
 
     for f in snaplist:
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/runCosmo.sh b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/runCosmo.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b3e9edd3c15c01d01336fb05e912201de32a1678
--- /dev/null
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/runCosmo.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -e glassPlane_64.hdf5 ]
+then
+    echo "Fetching initial glass file ..."
+    ./getGlass.sh
+fi
+
+if [ ! -f 'advect_ions.hdf5' ]; then
+    echo "Generating ICs"
+    python3 makeIC.py
+fi
+
+# Run SWIFT with RT
+../../../swift \
+    --hydro \
+    --cosmology \
+    --threads=4 \
+    --stars \
+    --external-gravity \
+    --feedback \
+    --radiation \
+    advect_ions_cosmo.yml 2>&1 | tee output.log
+
+python3 plotIonization.py -b cosmo
+python3 testIonization.py -b cosmo
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/snapshot_times_high_redshift b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/snapshot_times_high_redshift
new file mode 100644
index 0000000000000000000000000000000000000000..e504014177837a7b2c1d134d1c4a0d5a785d0044
--- /dev/null
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/snapshot_times_high_redshift
@@ -0,0 +1,12 @@
+# Redshift
+99.98460572382712
+96.47031888378449
+93.23367859034735
+90.25299848922855
+87.49735694548497
+84.9407463291136
+82.56112036003019
+80.33965676782498
+78.26018022924342
+76.30870609169932
+74.47307621234665
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/testIonization.py b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/testIonization.py
index 3c737462e2ea28f61897c34c6b0d7642626aa463..da002bea6ecbb36e819af6a71e12cc452a0dfa48 100755
--- a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/testIonization.py
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/testIonization.py
@@ -25,11 +25,19 @@
 # ----------------------------------------------------
 
 import os
+import sys
 
 import swiftsimio
 from matplotlib import pyplot as plt
+import argparse
 
-snapshot_base = "output"
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-b", "--basename", help="Snapshot basename", default="output")
+
+    args = parser.parse_args()
+    return args
 
 
 def get_snapshot_list(snapshot_basename="output"):
@@ -46,6 +54,9 @@ def get_snapshot_list(snapshot_basename="output"):
             snaplist.append(f)
 
     snaplist = sorted(snaplist)
+    if len(snaplist) == 0:
+        print(f"No snapshots with base '{snapshot_basename}' found")
+        sys.exit(1)
 
     return snaplist
 
@@ -61,6 +72,11 @@ def compare_data(snaplist):
     HeII = []
     HeIII = []
 
+    if "cosmo" in snaplist[0]:
+        savename = "total_abundancies_cosmo.png"
+    else:
+        savename = "total_abundancies.png"
+
     for filename in snaplist:
         data = swiftsimio.load(filename)
 
@@ -86,12 +102,15 @@ def compare_data(snaplist):
 
     #  plt.show()
     plt.tight_layout()
-    plt.savefig("total_abundancies.png", dpi=200)
+    plt.savefig(savename, dpi=200)
 
     return
 
 
 if __name__ == "__main__":
+    # Get command line arguments
+    args = parse_args()
 
+    snapshot_base = args.basename
     snaplist = get_snapshot_list(snapshot_base)
     compare_data(snaplist)
diff --git a/examples/RadiativeTransferTests/RandomizedBox_3D/plotRadiationProjection.py b/examples/RadiativeTransferTests/RandomizedBox_3D/plotRadiationProjection.py
index 6837842e09302839460955eaa855d8c16ecc50fb..aa190f93d516a32081bf55d88cec971a700d2f10 100755
--- a/examples/RadiativeTransferTests/RandomizedBox_3D/plotRadiationProjection.py
+++ b/examples/RadiativeTransferTests/RandomizedBox_3D/plotRadiationProjection.py
@@ -76,7 +76,7 @@ mpl.rcParams["text.usetex"] = True
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -116,9 +116,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -128,7 +128,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     data = swiftsimio.load(filename)
     meta = data.metadata
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     xlabel_units_str = meta.boxsize.units.latex_representation()
 
     global imshow_kwargs
@@ -324,7 +324,7 @@ def get_minmax_vals(snaplist):
         data = swiftsimio.load(filename)
         meta = data.metadata
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml b/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml
index 340aac434b3d9fe08fc45c577abd379e41b87bef..2027cd976588b9dc8d2ed3c9f7b46a7422eafce1 100644
--- a/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml
+++ b/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml
@@ -33,6 +33,7 @@ SPH:
   resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
   CFL_condition:         0.6      # Courant-Friedrich-Levy condition for time integration.
   minimal_temperature:   10.      # Kelvin
+  h_max:                 45.
 
 # Parameters related to the initial conditions
 InitialConditions:
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_2D/README b/examples/RadiativeTransferTests/StromgrenSphere_2D/README
index d291adc607e54c1ab20e561f725fe54a535e0416..603fb463f555828f6ad11406c849e806f7d80fbc 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_2D/README
+++ b/examples/RadiativeTransferTests/StromgrenSphere_2D/README
@@ -21,9 +21,13 @@ Additional scripts:
         'snapshot_base' variable at the top of the script depending on which
         solutions you want to plot.
 
-To use the GEAR RT model, compile with :
+To use the GEAR RT (with gizmo-mfv solver) model, compile with :
     --with-rt=GEAR_1 --with-rt-riemann-solver=GLF --with-hydro-dimension=2 --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
 
+To use the GEAR RT (with sphenix SPH solver) model, compile with :
+    --with-rt=GEAR_1 --with-rt-riemann-solver=GLF --with-hydro-dimension=2 --with-hydro=sphenix  --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT
+
+
 To use the SPHM1 RT model, compile with :
     --with-rt=SPHM1RT_4 --with-hydro-dimension=2  --with-stars=basic --with-feedback=none   --with-sundials=$SUNDIALS_ROOT
     
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_2D/plotRadiationProjection.py b/examples/RadiativeTransferTests/StromgrenSphere_2D/plotRadiationProjection.py
index 06a6e95e32900d3fcde3748fefe74d8825093075..5feac7486df09a301036db8a40685b74506861ae 100755
--- a/examples/RadiativeTransferTests/StromgrenSphere_2D/plotRadiationProjection.py
+++ b/examples/RadiativeTransferTests/StromgrenSphere_2D/plotRadiationProjection.py
@@ -108,7 +108,7 @@ def get_units(scheme, unit_system="cgs_units"):
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -152,9 +152,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -173,7 +173,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
             scheme, unit_system="cgs_units"
         )
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     xlabel_units_str = meta.boxsize.units.latex_representation()
 
     global imshow_kwargs
@@ -352,7 +352,7 @@ def get_minmax_vals(snaplist):
                 scheme, unit_system="cgs_units"
             )
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_3D/plotRadiationProjection.py b/examples/RadiativeTransferTests/StromgrenSphere_3D/plotRadiationProjection.py
index 09e78f504acf75ef86e923e7d04c7eb11d6b1268..3ce7646691687f18c6ec55ecc38d0b123242ae15 100755
--- a/examples/RadiativeTransferTests/StromgrenSphere_3D/plotRadiationProjection.py
+++ b/examples/RadiativeTransferTests/StromgrenSphere_3D/plotRadiationProjection.py
@@ -108,7 +108,7 @@ def get_units(scheme, unit_system="cgs_units"):
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -152,9 +152,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -174,7 +174,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
             scheme, unit_system="cgs_units"
         )
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     xlabel_units_str = meta.boxsize.units.latex_representation()
 
     global imshow_kwargs
@@ -378,7 +378,7 @@ def get_minmax_vals(snaplist):
                 scheme, unit_system="cgs_units"
             )
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/plotRadiationProjection.py b/examples/RadiativeTransferTests/UniformBox_3D/plotRadiationProjection.py
index 746c1e10ff297e7986382f1e1c5bcbf97a053326..34ad10a7f8be4cc67b9f9741bc849d43cc08c578 100755
--- a/examples/RadiativeTransferTests/UniformBox_3D/plotRadiationProjection.py
+++ b/examples/RadiativeTransferTests/UniformBox_3D/plotRadiationProjection.py
@@ -74,7 +74,7 @@ mpl.rcParams["text.usetex"] = True
 
 def get_snapshot_list(snapshot_basename="output"):
     """
-    Find the snapshot(s) that are to be plotted 
+    Find the snapshot(s) that are to be plotted
     and return their names as list
     """
 
@@ -114,9 +114,9 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     Create the actual plot.
 
     filename: file to work with
-    energy_boundaries:  list of [E_min, E_max] for each photon group. 
+    energy_boundaries:  list of [E_min, E_max] for each photon group.
                         If none, limits are set automatically.
-    flux_boundaries:    list of [F_min, F_max] for each photon group. 
+    flux_boundaries:    list of [F_min, F_max] for each photon group.
                         If none, limits are set automatically.
     """
 
@@ -126,7 +126,7 @@ def plot_photons(filename, energy_boundaries=None, flux_boundaries=None):
     data = swiftsimio.load(filename)
     meta = data.metadata
 
-    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
     xlabel_units_str = meta.boxsize.units.latex_representation()
 
     global imshow_kwargs
@@ -320,7 +320,7 @@ def get_minmax_vals(snaplist):
         data = swiftsimio.load(filename)
         meta = data.metadata
 
-        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"])
+        ngroups = int(meta.subgrid_scheme["PhotonGroupNumber"][0])
         emin_group = []
         emax_group = []
         fluxmin_group = []
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py b/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py
index 6fe1efdd00fec032d93a892d81d98619a26cfa58..cf4b8474ec23ded81414bf211b14d18cf7350a85 100644
--- a/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py
+++ b/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py
@@ -171,7 +171,7 @@ def get_snap_data(prefix="output", skip_snap_zero=False, skip_last_snap=False):
             "Compile swift --with-rt=GEAR_N",
         )
 
-    ngroups = int(firstfile.metadata.subgrid_scheme["PhotonGroupNumber"])
+    ngroups = int(firstfile.metadata.subgrid_scheme["PhotonGroupNumber"][0])
     rundata.ngroups = ngroups
 
     luminosity_model = firstfile.metadata.parameters["GEARRT:stellar_luminosity_model"]
diff --git a/examples/SantaBarbara/SantaBarbara-256/getIC.sh b/examples/SantaBarbara/SantaBarbara-256/getIC.sh
index a3073631ceedea47c8ac218a5e62529efee6fc56..d27f21a07293503107021d0344eeb50a0c06d93b 100755
--- a/examples/SantaBarbara/SantaBarbara-256/getIC.sh
+++ b/examples/SantaBarbara/SantaBarbara-256/getIC.sh
@@ -1,2 +1,2 @@
 #!/bin/bash
-wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/SantaBarbara.hdf5
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/SantaBarbara/SantaBarbara_256.hdf5
diff --git a/examples/SantaBarbara/SantaBarbara-256/santa_barbara.yml b/examples/SantaBarbara/SantaBarbara-256/santa_barbara.yml
index bc549fd97c3c3d2accde2a47639b9b618b4ebc3e..b0f8a2e8a57ed5d695182ff5899c4c586d0a00d6 100644
--- a/examples/SantaBarbara/SantaBarbara-256/santa_barbara.yml
+++ b/examples/SantaBarbara/SantaBarbara-256/santa_barbara.yml
@@ -42,7 +42,7 @@ Statistics:
 # Parameters for the self-gravity scheme
 Gravity:
   eta:                      0.025
-  MAC:                      adpative
+  MAC:                      adaptive
   theta_cr:                 0.7
   epsilon_fmm:              0.001
   use_tree_below_softening: 1
diff --git a/examples/SantaBarbara/SantaBarbara-256/velociraptor_cfg.cfg b/examples/SantaBarbara/SantaBarbara-256/velociraptor_cfg.cfg
index b904d2a419ad6010e12073209bd77e8f59eef7c4..09bd6e86aadf93eebdf25b609c1bf202cd0e9c41 100644
--- a/examples/SantaBarbara/SantaBarbara-256/velociraptor_cfg.cfg
+++ b/examples/SantaBarbara/SantaBarbara-256/velociraptor_cfg.cfg
@@ -41,6 +41,9 @@ Significance_level=1.0 #how significant a substructure is relative to Poisson no
 Length_unit_to_kpc=1000. #conversion of output length units to kpc
 Velocity_to_kms=1.0      #conversion of output velocity units to km/s
 Mass_to_solarmass=1e+10  #conversion of output mass units to solar masses
+Star_formation_rate_to_solarmassperyear=1.0
+Metallicity_to_solarmetallicity=1.0
+Stellar_age_to_yr=1.0
 
 # units conversion from input to desired internal unit.
 # These should be set to 1 unless a conversion is expected.
diff --git a/examples/SinkParticles/HomogeneousBox/README b/examples/SinkParticles/HomogeneousBox/README
new file mode 100644
index 0000000000000000000000000000000000000000..bf8b8b52f1bd7053663fd758604a946abb9313b3
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/README
@@ -0,0 +1,40 @@
+# Intro
+This example is a non cosmological homogeneous box with gas only. The cooling and small perturbations fragment the box into small compact regions that create sink particles. As the sinks accrete gas, they star spawning stars. As the first stars explode in supernovae, the first sink particles cannot accrete gas anymore. The simulations runs 282 Myr until the first stars explode and remove the gas.
+
+The default level is 5 (N_particle = 2^(3*level)). It is quick. If you want to try the example with higher levels, we recommend using HPC facilities.
+
+This example tests the impact of different parameters on the sink formation (e.g. cut_off_radius, density_threshold, ...), accretion and star spawning. It also test the effects of feedback (e.g. supernovae_efficiency) and cooling (equilibrium cooling with grackle_0 mode versu non equilibrium cooling with grackle_X, X = 1, 2 or 3) on the sink particles and star formation.
+
+This example allows you to play with the sink parameters to see how they impact the simulation. You can also change parameters in the feedback, the cooling, etc.
+
+# Configure
+To run this example with GEAR model,
+
+'''
+./configure --disable-mpi --with-chemistry=GEAR_10 --with-cooling=grackle_0 --with-stars=GEAR --with-star-formation=GEAR --with-feedback=GEAR --with-sink=GEAR --with-kernel=wendland-C2 --with-adaptive-softening --with-grackle=path/to/grackle
+
+and then
+
+make -j
+
+You can remove the adaptive softening. In this case, you may need to change the default `max_physical_baryon_softening` value.
+
+Other sink models can be probed by changing swift configuration and adding the relevant parameters in params.yml.
+
+# ICs
+The run.sh script calls `makeIC.py' script with default values. You can experiment by changing the ICs. Run `python3 makeIC.py --help` to get the list of parameters.
+
+# Run
+Type run.sh, and let's go!
+
+# Changing the cooling
+You can change grackle mode to 1,2, or 3 if you want. You may also want to change the simulation end time.
+
+For grackle_0 (equilibrium mode), the first sink form roughly at 0.260 (internal units). The end time is 0.282 Myr. The simulation runs in ~ 2 minutes.
+
+Notice that, for grackle_X with X from 1 to 3, the level 5 simulation does not have enough particles to run for long times. At 0.5 Gyr, swift ends because there are not enough particles per top-level cell. However, note how different the collapse from grackle_0 is!
+
+# For sink debugging
+This example was used to debug the sink's tasks and other sink-related bugs. The level (N_part = 2^(3*level)) was 8 during this debugging phase.
+
+Starting from the default ICs generated by `makeIC.py` can take some time. You can use `debug=1 ./run.sh` to run a simulation configured for debugging purposes. Debugging mode retrieves ICs for a level 8 HomogeneousBox in a state close to sink formation. 
\ No newline at end of file
diff --git a/examples/SinkParticles/HomogeneousBox/getChemistryTable.sh b/examples/SinkParticles/HomogeneousBox/getChemistryTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b10fd23964158ee7a38d352dbd0ddd9beb7bdd77
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/getChemistryTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/FeedbackTables/POPIIsw.h5
diff --git a/examples/SinkParticles/HomogeneousBox/getDebuggingICs.sh b/examples/SinkParticles/HomogeneousBox/getDebuggingICs.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9cc9a61e6f3332064b09a04062e6f4d1db7673a9
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/getDebuggingICs.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/SinkParticles/snapshot_0003restart.hdf5
diff --git a/examples/SinkParticles/HomogeneousBox/getGrackleCoolingTable.sh b/examples/SinkParticles/HomogeneousBox/getGrackleCoolingTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ffdce192a7739b569840a07e721198e81b75dfe8
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/getGrackleCoolingTable.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012.h5
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012_high_density.h5
diff --git a/examples/SinkParticles/HomogeneousBox/makeIC.py b/examples/SinkParticles/HomogeneousBox/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..00da1b6f980859f9e604a0b0488026bcb09c0927
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/makeIC.py
@@ -0,0 +1,192 @@
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Yves Revaz (yves.revaz@epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+
+import h5py
+import numpy as np
+from optparse import OptionParser
+from astropy import units
+from astropy import constants
+
+
+def parse_options():
+
+    usage = "usage: %prog [options] file"
+    parser = OptionParser(usage=usage)
+
+    parser.add_option(
+        "--lJ",
+        action="store",
+        dest="lJ",
+        type="float",
+        default=0.250,
+        help="Jeans wavelength in box size unit",
+    )
+
+    parser.add_option(
+        "--rho",
+        action="store",
+        dest="rho",
+        type="float",
+        default=0.1,
+        help="Mean gas density in atom/cm3",
+    )
+
+    parser.add_option(
+        "--mass",
+        action="store",
+        dest="mass",
+        type="float",
+        default=50,
+        help="Gas particle mass in solar mass",
+    )
+
+    parser.add_option(
+        "--level",
+        action="store",
+        dest="level",
+        type="int",
+        default=5,
+        help="Resolution level: N = (2**l)**3",
+    )
+
+    parser.add_option(
+        "-o",
+        action="store",
+        dest="outputfilename",
+        type="string",
+        default="box.dat",
+        help="output filename",
+    )
+
+    (options, args) = parser.parse_args()
+
+    files = args
+
+    return files, options
+
+
+########################################
+# main
+########################################
+
+files, opt = parse_options()
+
+# define standard units
+UnitMass_in_cgs = 1.988409870698051e43  # 10^10 M_sun in grams
+UnitLength_in_cgs = 3.0856775814913673e21  # kpc in centimeters
+UnitVelocity_in_cgs = 1e5  # km/s in centimeters per second
+UnitCurrent_in_cgs = 1  # Amperes
+UnitTemp_in_cgs = 1  # Kelvin
+UnitTime_in_cgs = UnitLength_in_cgs / UnitVelocity_in_cgs
+
+UnitMass = UnitMass_in_cgs * units.g
+UnitLength = UnitLength_in_cgs * units.cm
+UnitTime = UnitTime_in_cgs * units.s
+UnitVelocity = UnitVelocity_in_cgs * units.cm / units.s
+
+np.random.seed(1)
+
+# Number of particles
+N = (2 ** opt.level) ** 3  # number of particles
+
+# Mean density
+rho = opt.rho  # atom/cc
+rho = rho * constants.m_p / units.cm ** 3
+
+# Gas particle mass
+m = opt.mass  # in solar mass
+m = m * units.Msun
+
+# Gas mass in the box
+M = N * m
+
+# Size of the box
+L = (M / rho) ** (1 / 3.0)
+
+# Jeans wavelength in box size unit
+lJ = opt.lJ
+lJ = lJ * L
+
+# Gravitational constant
+G = constants.G
+
+# Jeans wave number
+kJ = 2 * np.pi / lJ
+# Velocity dispersion
+sigma = np.sqrt(4 * np.pi * G * rho) / kJ
+
+
+print("Number of particles                   : {}".format(N))
+print("Equivalent velocity dispertion        : {}".format(sigma.to(units.m / units.s)))
+
+# Convert to code units
+m = m.to(UnitMass).value
+L = L.to(UnitLength).value
+rho = rho.to(UnitMass / UnitLength ** 3).value
+sigma = sigma.to(UnitVelocity).value
+
+# Generate the particles
+pos = np.random.random([N, 3]) * np.array([L, L, L])
+vel = np.zeros([N, 3])
+mass = np.ones(N) * m
+u = np.ones(N) * sigma ** 2
+ids = np.arange(N)
+h = np.ones(N) * 3 * L / N ** (1 / 3.0)
+rho = np.ones(N) * rho
+
+print("Inter-particle distance (code unit)   : {}".format(L / N ** (1 / 3.0)))
+
+
+# File
+fileOutput = h5py.File(opt.outputfilename, "w")
+print("{} saved.".format(opt.outputfilename))
+
+# Header
+grp = fileOutput.create_group("/Header")
+grp.attrs["BoxSize"] = [L, L, L]
+grp.attrs["NumPart_Total"] = [N, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_ThisFile"] = [N, 0, 0, 0, 0, 0]
+grp.attrs["Time"] = 0.0
+grp.attrs["NumFileOutputsPerSnapshot"] = 1
+grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["Dimension"] = 3
+
+
+# Units
+grp = fileOutput.create_group("/Units")
+grp.attrs["Unit length in cgs (U_L)"] = UnitLength_in_cgs
+grp.attrs["Unit mass in cgs (U_M)"] = UnitMass_in_cgs
+grp.attrs["Unit time in cgs (U_t)"] = UnitTime_in_cgs
+grp.attrs["Unit current in cgs (U_I)"] = UnitCurrent_in_cgs
+grp.attrs["Unit temperature in cgs (U_T)"] = UnitTemp_in_cgs
+
+
+# Particle group
+grp = fileOutput.create_group("/PartType0")
+grp.create_dataset("Coordinates", data=pos, dtype="d")
+grp.create_dataset("Velocities", data=vel, dtype="f")
+grp.create_dataset("Masses", data=mass, dtype="f")
+grp.create_dataset("SmoothingLength", data=h, dtype="f")
+grp.create_dataset("InternalEnergy", data=u, dtype="f")
+grp.create_dataset("ParticleIDs", data=ids, dtype="L")
+grp.create_dataset("Densities", data=rho, dtype="f")
+
+fileOutput.close()
diff --git a/examples/SinkParticles/HomogeneousBox/params.yml b/examples/SinkParticles/HomogeneousBox/params.yml
new file mode 100755
index 0000000000000000000000000000000000000000..ac851c5e3ed4236887fc95741e9985cdc3817ce7
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/params.yml
@@ -0,0 +1,118 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+43   # 10^10 solar masses 
+  UnitLength_in_cgs:   3.0856775814913673e+21  # 1 kpc 
+  UnitVelocity_in_cgs: 1e5   # km/s
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:          0.025               # Constant dimensionless multiplier for time integration.
+  theta:        0.7                 # Opening angle (Multipole acceptance criterion).
+  max_physical_baryon_softening: 0.005  # Physical softening length (in internal units)
+  mesh_side_length:        32
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:        0.    # The starting time of the simulation (in internal units).
+  time_end:          0.5 #0.282   #500 Myr # The end time of the simulation (in internal units).
+  dt_min:            1e-10  # The minimal time-step size of the simulation (in internal units).
+  dt_max:            1e-1  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir: snap
+  basename:   snapshot      # Common part of the name of output files
+  time_first: 0. #230 Myr # (Optional) Time of the first output if non-cosmological time-integration (in internal units)
+  delta_time: 1e-3        # Time difference between consecutive outputs (in internal units)
+
+Scheduler:
+  cell_extra_gparts: 10000      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sinks: 10000       # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sparts: 10000      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  max_top_level_cells: 3        #
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:           1e-3     # Time between statistics output
+  time_first:             0.     # (Optional) Time of the first stats output if non-cosmological time-integration (in internal units)
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          ICs_homogeneous_box.hdf5
+  periodic:                    1    # Are we running with periodic ICs?
+  shift:              [10.0,10.0,10.0]
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 57Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  h_max:                 5
+  h_min_ratio:           0.1
+  minimal_temperature:   1
+
+
+# Cooling with Grackle 3.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5 # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 0 # Enable or not the UV background
+  redshift: -1 # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 1 # Enable or not the metal cooling
+  provide_volumetric_heating_rates: 0 # User provide volumetric heating rates
+  provide_specific_heating_rates: 0 # User provide specific heating rates
+  self_shielding_method: -1 # Grackle (<= 3) or Gear self shielding method
+  self_shielding_threshold_atom_per_cm3: 0.007  # Required only with GEAR's self shielding. Density threshold of the self shielding
+  max_steps: 1000
+  convergence_limit: 1e-2
+  thermal_time_myr: 5
+  maximal_density_Hpcm3: -1    # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+GEARChemistry:
+  initial_metallicity: 1e-6
+
+GEARFeedback:
+  supernovae_energy_erg: 1e51           # supernovae energy, used only for SNIa
+  supernovae_efficiency: 0.1           # supernovae energy efficiency, used for both SNIa and SNII
+  yields_table: POPIIsw.h5
+  yields_table_first_stars: POPIIsw.h5
+  discrete_yields: 1
+  imf_transition_metallicity: -5         # Maximal metallicity ([Fe/H]) for a first star (0 to deactivate).
+  elements: [Fe, Mg, O, C, Al, Ca, Ba, Zn, Eu]              # Elements to read in the yields table. The number of element should be one less than the number of elements (N) requested during the configuration (--with-chemistry=GEAR_N).
+
+
+GEARSink:
+  use_fixed_cut_off_radius: 1                 # Are we using a fixed cutoff radius? If we are, in GEAR the cutoff radius is fixed at the value specified below, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+  cut_off_radius: 5e-3                        # Cut off radius of all the sinks in internal units. Ignored if use_fixed_cut_off_radius is 0. 
+  f_acc: 1e-2
+  temperature_threshold_K: 1000               # Max temperature (in K) for forming a sink when density_threshold_Hpcm3 <= density <= maximal_density_threshold_Hpcm3.
+  density_threshold_Hpcm3: 1e1                # Minimum gas density (in g/cm3) required to form a sink particle.
+  maximal_density_threshold_Hpcm3: 1e5        # If the gas density exceeds this value (in g/cm3), a sink forms regardless of temperature if all other criteria are passed.
+  stellar_particle_mass_Msun: 50              # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_Msun: 8               # Minimal mass of stars represented by discrete particles, in solar mass
+  stellar_particle_mass_first_stars_Msun: 50  # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_first_stars_Msun: 8   # Minimal mass of stars represented by discrete particles, in solar mass
+  star_spawning_sigma_factor: 0.5             # Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+  sink_formation_contracting_gas_criterion: 1     # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+  sink_formation_smoothing_length_criterion: 1    # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+  sink_formation_jeans_instability_criterion: 1   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+  sink_formation_bound_state_criterion: 1         # (Optional) Activate the bound state check for sink formation. (Default: 1)
+  sink_formation_overlapping_sink_criterion: 1    # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+  disable_sink_formation: 0                       # (Optional) Disable sink formation. (Default: 0)
+
+  # Timesteps parameters
+  CFL_condition:                        0.5       # Courant-Friedrich-Levy condition for time integration.
+  timestep_age_threshold_unlimited_Myr: 100.      # (Optional) Age above which sinks have no time-step restriction any more (in Mega-years). Defaults to 0.
+  timestep_age_threshold_Myr:           25.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_young_Myr:               0.5       # (Optional) Maximal time-step length of young sinks (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_old_Myr:                  1.       # (Optional) Maximal time-step length of old sinks (in Mega-years). Defaults to FLT_MAX.
+  n_IMF:                                 2.       # (Optional) Number of times the IMF mass can be swallowed in a single timestep. (Default: FLTM_MAX)
+
+Stars:
+  timestep_age_threshold_unlimited_Myr: 30.      # (Optional) Age above which sinks have no time-step restriction any more (in Mega-years). Defaults to 0.
+  timestep_age_threshold_Myr:           10.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_young_Myr:               1       # (Optional) Maximal time-step length of young sinks (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_old_Myr:                 5         # (Optional) Maximal time-step length of old sinks (in Mega-years). Defaults to FLT_MAX.
diff --git a/examples/SinkParticles/HomogeneousBox/params_debug.yml b/examples/SinkParticles/HomogeneousBox/params_debug.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cf636e992d6a93824975432a7f2fd99a54123951
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/params_debug.yml
@@ -0,0 +1,134 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+43   # 10^10 solar masses 
+  UnitLength_in_cgs:   3.0856775814913673e+21  # 1 kpc 
+  UnitVelocity_in_cgs: 1e5   # km/s
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:              adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:      0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:         0.7       # Opening angle for the purely gemoetric criterion.
+  eta:              0.025     # Constant dimensionless multiplier for time integration.
+  theta:            0.7       # Opening angle (Multipole acceptance criterion).
+  max_physical_baryon_softening: 0.01  # Physical softening length (in internal units)
+  mesh_side_length:        32
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:        0.     # The starting time of the simulation (in internal units).
+  time_end:          1.0    # The end time of the simulation (in internal units).
+  dt_min:            1e-12  # The minimal time-step size of the simulation (in internal units).
+  dt_max:            1e-2   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir:     snap
+  basename:   snapshot    # Common part of the name of output files
+  time_first: 0.          # (Optional) Time of the first output if non-cosmological time-integration (in internal units)
+  delta_time: 0.01        # Time difference between consecutive outputs (in internal units)
+
+Restarts:
+  enable:      1
+  delta_hours: 1 # Write one restart file per hour
+
+Scheduler:
+  cell_extra_gparts: 10000       # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sinks:  10000       # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sparts: 10000       # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  max_top_level_cells: 3
+  dependency_graph_frequency: 1  # (Optional) Dumping frequency of the dependency graph. By default, writes only at the first step.
+#  dependency_graph_cell:       3866632 # Once the code crashes, you can get the problematic cells
+  links_per_tasks: 30
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:           1e-2     # Time between statistics output
+  time_first:             0.     # (Optional) Time of the first stats output if non-cosmological time-integration (in internal units)
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:         snapshot_0003restart.hdf5 # ICs_homogeneous_box.hdf5 # The file to read
+  periodic:          1                         # Are we running with periodic ICs?
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  minimal_temperature:   1
+
+# Cooling with Grackle 3.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012_high_density.h5 # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 0                  # Enable or not the UV background
+  redshift:           -1                 # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 1                  # Enable or not the metal cooling
+  provide_volumetric_heating_rates: 0    # User provide volumetric heating rates
+  provide_specific_heating_rates:   0    # User provide specific heating rates
+  self_shielding_method:            -1   # Grackle (<= 3) or Gear self shielding method
+  self_shielding_threshold_atom_per_cm3: 0.007  # Required only with GEAR's self shielding. Density threshold of the self shielding
+  max_steps:          1000
+  convergence_limit:  1e-2
+  thermal_time_myr:   5
+  maximal_density_Hpcm3: 1e10            # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+GEARChemistry:
+  initial_metallicity: -1     # If < 0, use the metallicity sotred in the ICs
+
+GEARFeedback:
+  supernovae_energy_erg:    1e51                # Supernovae energy, used only for SNIa
+  supernovae_efficiency:    0.1                 # Supernovae energy efficiency, used for both SNIa and SNII
+  yields_table:             POPIIsw.h5
+  yields_table_first_stars: POPIIsw.h5
+  discrete_yields:          1
+  imf_transition_metallicity: -5                # Maximal metallicity ([Fe/H]) for a first star (0 to deactivate).ppp
+  elements: [Fe, Mg, O, C, Al, Ca, Ba, Zn, Eu]  # Elements to read in the yields table. The number of element should be one less than the number of elements (N) requested during the configuration (--with-chemistry=GEAR_N).
+
+# These parameters are set to trigger sink formation more quickly than in the params.yml file
+GEARSink:
+  use_fixed_cut_off_radius: 1                 # Are we using a fixed cutoff radius? If we are, in GEAR the cutoff radius is fixed at the value specified below, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+  cut_off_radius: 1e-2                        # Cut off radius of all the sinks in internal units.
+  f_acc: 0.1
+  temperature_threshold_K: 3e4                # Max temperature (in K) for forming a sink when density_threshold_Hpcm3 <= density <= maximal_density_threshold_Hpcm3.
+  density_threshold_Hpcm3: 1e1                # Minimum gas density (in g/cm3) required to form a sink particle
+  maximal_density_threshold_Hpcm3: 1e2        # If the gas density exceeds this value (in g/cm3), a sink forms regardless of temperature if all other criteria are passed
+  stellar_particle_mass_Msun: 60              # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_Msun: 8               # Minimal mass of stars represented by discrete particles, in solar mass
+  stellar_particle_mass_first_stars_Msun: 60  # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_first_stars_Msun: 8   # Minimal mass of stars represented by discrete particles, in solar mass
+  star_spawning_sigma_factor: 0.5             # Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+  sink_formation_contracting_gas_criterion: 0     # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+  sink_formation_smoothing_length_criterion: 0    # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+  sink_formation_jeans_instability_criterion: 0   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+  sink_formation_bound_state_criterion: 0         # (Optional) Activate the bound state check for sink formation. (Default: 1)
+  sink_formation_overlapping_sink_criterion: 0    # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+  disable_sink_formation: 0                       # (Optional) Disable sink formation. (Default: 0)
+
+  # Timesteps parameters
+  CFL_condition:                        0.8       # Courant-Friedrich-Levy condition for time integration.
+  timestep_age_threshold_unlimited_Myr: 100.      # (Optional) Age above which sinks have no time-step restriction any more (in Mega-years). Defaults to 0.
+  timestep_age_threshold_Myr:           25.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_young_Myr:               2.       # (Optional) Maximal time-step length of young sinks (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_old_Myr:                 5.       # (Optional) Maximal time-step length of old sinks (in Mega-years). Defaults to FLT_MAX.
+#  n_IMF:                               2.       # (Optional) Number of times the IMF mass can be swallowed in a single timestep. (Default: FLTM_MAX)
+
+# Use GEAR SF if needed
+GEARStarFormation:
+  star_formation_efficiency: 0.01     # star formation efficiency (c_*)
+  maximal_temperature_K:     3e4      # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   10      # Density threshold in Hydrogen atoms/cm3
+  n_stars_per_particle: 4
+  min_mass_frac: 0.5
+
+# GEAR SF requires this to be set
+GEARPressureFloor:
+  jeans_factor: 10
+
+Stars:
+  timestep_age_threshold_unlimited_Myr: 30.      # (Optional) Age above which sinks have no time-step restriction any more (in Mega-years). Defaults to 0.
+  timestep_age_threshold_Myr:           10.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_young_Myr:               1       # (Optional) Maximal time-step length of young sinks (in Mega-years). Defaults to FLT_MAX.
+  max_timestep_old_Myr:                 5         # (Optional) Maximal time-step length of old sinks (in Mega-years). Defaults to FLT_MAX.
+
diff --git a/examples/SinkParticles/HomogeneousBox/plot_gas_density.py b/examples/SinkParticles/HomogeneousBox/plot_gas_density.py
new file mode 100644
index 0000000000000000000000000000000000000000..37febc8e7d03f229ac07cf3a0d62ba75381a64ea
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/plot_gas_density.py
@@ -0,0 +1,433 @@
+"""
+Makes a gas density projection plot. Uses the swiftsimio library.
+"""
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+import swiftsimio as sw
+from swiftsimio.visualisation.projection import project_gas
+
+import unyt
+from unyt import mh, cm
+from tqdm import tqdm
+from matplotlib.colors import LogNorm
+from matplotlib.animation import FuncAnimation
+
+# %%
+# Constants; these could be put in the parameter file but are rarely changed.
+image_resolution = 1024
+
+# Plotting controls
+cmap = "inferno"
+
+# %%
+
+
+def get_gas_mu(data: sw.SWIFTDataset) -> np.array:
+    """
+    Return the mean molecular weight of the gas.
+    """
+    # Get the cooling model
+    cooling = data.metadata.subgrid_scheme["Cooling Model"]
+
+    # Use a variable for better readability
+    gas = data.gas
+
+    # Get rho
+    rho = gas.densities.to(unyt.g / unyt.cm ** 3)  # self.Rho(units='g/cm3')
+
+    # hydrogen mass in gram
+    mh.convert_to_cgs()
+    mH_in_g = mh
+
+    if cooling == b"Grackle3":  # grackle3
+        nHI = gas.hi * rho / (mH_in_g)
+        nHII = gas.hii * rho / (mH_in_g)
+        nHeI = gas.he_i * rho / (4 * mH_in_g)
+        nHeII = gas.he_ii * rho / (4 * mH_in_g)
+        nHeIII = gas.he_iii * rho / (4 * mH_in_g)
+        nH2I = gas.h2_i * rho / (2 * mH_in_g)
+        nH2II = gas.h2_ii * rho / (2 * mH_in_g)
+        nHDI = gas.hdi * rho / (3 * mH_in_g)
+
+        nel = nHII + nHeII + 2 * nHeIII + nH2II
+        mu = (
+            (nHI + nHII) + (nHeI + nHeII + nHeIII) * 4 + (nH2I + nH2II) * 2 + nHDI * 3
+        ) / (nHI + nHII + nHeI + nHeII + nHeIII + nH2I + nH2II + nHDI + nel)
+        return mu
+
+    elif cooling == b"Grackle2":  # grackle2
+        nHI = gas.hi * rho / (mH_in_g)
+        nHII = gas.hii * rho / (mH_in_g)
+        nHeI = gas.he_i * rho / (4 * mH_in_g)
+        nHeII = gas.he_ii * rho / (4 * mH_in_g)
+        nHeIII = gas.he_iii * rho / (4 * mH_in_g)
+        nH2I = gas.h2_i * rho / (2 * mH_in_g)
+        nH2II = gas.h2_ii * rho / (2 * mH_in_g)
+
+        nel = nHII + nHeII + 2 * nHeIII + nH2II
+        mu = ((nHI + nHII) + (nHeI + nHeII + nHeIII) * 4 + (nH2I + nH2II) * 2) / (
+            nHI + nHII + nHeI + nHeII + nHeIII + nH2I + nH2II + nel
+        )
+        return mu
+
+    elif cooling == b"Grackle1":  # grackle1
+        nHI = gas.hi * rho / (mH_in_g)
+        nHII = gas.hii * rho / (mH_in_g)
+        nHeI = gas.he_i * rho / (4 * mH_in_g)
+        nHeII = gas.he_ii * rho / (4 * mH_in_g)
+        nHeIII = gas.he_iii * rho / (4 * mH_in_g)
+        nel = nHII + nHeII + 2 * nHeIII
+        mu = ((nHI + nHII) + (nHeI + nHeII + nHeIII) * 4) / (
+            nHI + nHII + nHeI + nHeII + nHeIII + nel
+        )
+        return mu
+
+    else:  # Grackle0
+        # print("... found info for grackle mode=0")
+
+        from unyt.physical_constants import kboltz_cgs as k_B_cgs
+
+        gamma = data.metadata.gas_gamma[0]
+
+        # Get internal energy
+        u = data.gas.internal_energies
+        u = u.to_physical()
+        u = u.to(unyt.erg / unyt.g)
+
+        # Get hydrigen fraction
+        H_frac = float(
+            data.metadata.parameters["GrackleCooling:HydrogenFractionByMass"]
+        )
+
+        # Compute T/mu
+        T_over_mu = (gamma - 1.0) * u.value * mH_in_g.value / k_B_cgs.value
+        T_trans = 1.1e4
+        mu_trans = 4.0 / (8.0 - 5.0 * (1.0 - H_frac))
+
+        # Determine if we are ionized or not
+        mu = np.ones(np.size(u))
+        mask_ionized = T_over_mu > (T_trans + 1) / mu_trans
+        mask_neutral = T_over_mu < (T_trans + 1) / mu_trans
+
+        # Give the right mu
+        mu[mask_ionized] = 4.0 / (8.0 - 5.0 * (1.0 - H_frac))
+        mu[mask_neutral] = 4.0 / (1.0 + 3.0 * H_frac)
+
+        return mu
+
+
+def get_gas_temperatures(data: sw.SWIFTDataset) -> np.array:
+    """
+        Compute the temperature of the gas.
+    """
+    from unyt.physical_constants import kboltz_cgs as k_B
+
+    # from unyt.physical_constants import mh
+
+    # Convert to cgs
+    mh.convert_to_cgs()
+
+    # Get the cooling model
+    cooling = data.metadata.subgrid_scheme["Cooling Model"]
+
+    # Get internal energy and convert to physical units in cgs
+    u = data.gas.internal_energies
+    u = u.to_physical()
+    u = u.to(unyt.erg / unyt.g)
+
+    # Get gamm and compute mu
+    gamma = data.metadata.gas_gamma[0]
+    mu = get_gas_mu(data)
+
+    # FInally compute the the temperature
+    if cooling == b"Grackle3" or cooling == b"Grackle2" or cooling == b"Grackle1":
+        T = mu * (gamma - 1.0) * u * mh / k_B
+    else:
+        a = (gamma - 1.0) * (mu * mh) / k_B * u
+        T = np.where((u.value > 0), a.value, 0) * unyt.kelvin
+    return T
+
+
+def get_sink_and_stars_positions(filename):
+    data = sw.load(filename)
+
+    sink_pos = data.sinks.coordinates
+    star_pos = data.stars.coordinates
+
+    return sink_pos, star_pos
+
+
+def make_projection(filename, image_resolution):
+    """
+    Compute a mass projection with swiftsimio.
+    """
+    data = sw.load(filename)
+
+    # Compute projected density
+    projected_mass = project_gas(
+        data,
+        resolution=image_resolution,
+        project="masses",
+        parallel=True,
+        periodic=True,
+    )
+
+    boxsize = data.metadata.boxsize
+    x_edges = np.linspace(0 * unyt.kpc, boxsize[0], image_resolution)
+    y_edges = np.linspace(0 * unyt.kpc, boxsize[1], image_resolution)
+
+    # Convert to 1/cm**2
+    projected_mass = projected_mass.to(mh / (cm ** 2))
+
+    return projected_mass.T, x_edges, y_edges
+
+
+def setup_axes():
+    """
+    Creates the figure and axis object.
+    """
+    fig, ax = plt.subplots(1, figsize=(6, 5), dpi=300)
+
+    ax.set_xlabel("x [kpc]")
+    ax.set_ylabel("y [kpc]")
+
+    return fig, ax
+
+
+def make_single_image(filename, image_resolution):
+    """
+    Makes a single image and saves it to mass_projection_{snapshot_number}.png.
+
+    Filename should be given _without_ hdf5 extension.
+    """
+    file = "{:s}.hdf5".format(filename)
+    fig, ax = setup_axes()
+    projected_mass, x_edges, y_edges = make_projection(file, image_resolution)
+
+    mappable = ax.pcolormesh(
+        x_edges, y_edges, projected_mass, cmap=cmap, norm=LogNorm()
+    )
+    fig.colorbar(mappable, label="Surface density [cm$^{-2}]$", pad=0)
+
+    sink_pos, star_pos = get_sink_and_stars_positions(file)
+
+    if star_pos.size != 0:
+        ax.scatter(star_pos[:, 0], star_pos[:, 1], c="limegreen", zorder=1, marker="*")
+
+    if sink_pos.size != 0:
+        ax.scatter(sink_pos[:, 0], sink_pos[:, 1], c="blue", zorder=2, marker=".")
+
+    ax.text(
+        0.7,
+        0.95,
+        "$N_{\mathrm{star}}" + " = {}$".format(len(star_pos)),
+        transform=ax.transAxes,
+        fontsize=7,
+        bbox=dict(facecolor="white", alpha=0.8),
+    )
+    ax.text(
+        0.1,
+        0.95,
+        "$N_{\mathrm{sink}}" + " = {}$".format(len(sink_pos)),
+        transform=ax.transAxes,
+        fontsize=7,
+        bbox=dict(facecolor="white", alpha=0.8),
+    )
+
+    fig.tight_layout()
+
+    image = "mass_projection_{:s}.png".format(filename[-4:])
+    fig.savefig(image)
+
+    return
+
+
+def make_movie(args, image_resolution):
+    """
+    Makes a movie and saves it to mass_projection_movie.mp4.
+    """
+
+    fig, ax = setup_axes()
+
+    def grab_metadata(n):
+        filename = "{:s}_{:04d}.hdf5".format(args["stub"], n)
+        data = sw.load(filename)
+
+        return data.metadata
+
+    def grab_data(n):
+        filename = "{:s}_{:04d}.hdf5".format(args["stub"], n)
+
+        H, _, _ = make_projection(filename, image_resolution)
+
+        # Need to ravel because pcolormesh's set_array takes a 1D array. Might
+        # as well do it here, because 1d arrays are easier to max() than 2d.
+        return H.ravel()
+
+    def grab_sink_star_pos(n):
+        filename = "{:s}_{:04d}.hdf5".format(args["stub"], n)
+
+        sink_pos, star_pos = get_sink_and_stars_positions(filename)
+
+        return sink_pos, star_pos
+
+    histograms = [
+        grab_data(n)
+        for n in tqdm(
+            range(args["initial"], args["final"] + 1),
+            desc="Computing gas mass projection",
+        )
+    ]
+
+    sink_star = [
+        grab_sink_star_pos(n)
+        for n in tqdm(
+            range(args["initial"], args["final"] + 1),
+            desc="Getting sink and stars positions",
+        )
+    ]
+
+    sink_pos = [s[0] for s in sink_star]
+    star_pos = [s[1] for s in sink_star]
+
+    metadata = [
+        grab_metadata(n)
+        for n in tqdm(
+            range(args["initial"], args["final"] + 1), desc="Grabbing metadata"
+        )
+    ]
+
+    # Need to get a reasonable norm so that we don't overshoot.
+    max_particles = max([x.max() for x in histograms])
+    min_particles = max([x.min() for x in histograms])
+    norm = LogNorm(vmin=min_particles, vmax=max_particles)
+
+    # First, let's make the initial frame (we need this for our d, T values that we
+    # got rid of in grab_data.
+    hist, x_edges, y_edges = make_projection(
+        "{:s}_{:04d}.hdf5".format(args["stub"], args["initial"]), image_resolution
+    )
+
+    mappable = ax.pcolormesh(x_edges, y_edges, hist, cmap=cmap, norm=norm)
+    fig.colorbar(mappable, label="Surface density [cm$^{-2}]$", pad=0)
+
+    fig.tight_layout()
+
+    # Once we've rearranged the figure with tight_layout(), we can start laying
+    # Down the metadata text.
+
+    def format_metadata(metadata: sw.SWIFTMetadata):
+        t = metadata.t
+        t.convert_to_units(unyt.Myr)
+
+        x = "$a$: {:2.2f}\n$z$: {:2.2f}\n$t$: {:2.2f}".format(metadata.a, metadata.z, t)
+
+        return x
+
+    text = ax.text(
+        0.025,
+        0.975,
+        format_metadata(metadata[0]),
+        ha="left",
+        va="top",
+        transform=ax.transAxes,
+        color="white",
+    )
+
+    ax.text(
+        0.975,
+        0.975,
+        metadata[0].code["Git Revision"].decode("utf-8"),
+        ha="right",
+        va="top",
+        transform=ax.transAxes,
+        color="white",
+    )
+
+    def animate(data):
+        mappable.set_array(histograms[data])
+        text.set_text(format_metadata(metadata[data]))
+
+        if star_pos[data].size != 0:
+            ax.scatter(
+                star_pos[data][:, 0],
+                star_pos[data][:, 1],
+                c="limegreen",
+                zorder=1,
+                marker="*",
+            )
+
+        if sink_pos[data].size != 0:
+            ax.scatter(
+                sink_pos[data][:, 0],
+                sink_pos[data][:, 1],
+                c="blue",
+                zorder=2,
+                marker=".",
+            )
+
+        return mappable
+
+    animation = FuncAnimation(
+        fig, animate, range(len(histograms)), fargs=[], interval=1000 / 10
+    )
+
+    animation.save("mass_projection_movie.mp4")
+
+    return
+
+
+# %%
+if __name__ == "__main__":
+    import argparse as ap
+
+    parser = ap.ArgumentParser(
+        description="""
+             Plotting script for making a mass projection plot.
+             Takes the filename handle, start, and (optionally) stop
+             snapshots. If stop is not given, png plot is produced for
+             that snapshot. If given, a movie is made.
+             """
+    )
+
+    parser.add_argument(
+        "-i",
+        "--initial",
+        help="""Initial snapshot number. Default: 0.""",
+        default=0,
+        required=False,
+        type=int,
+    )
+
+    parser.add_argument(
+        "-f",
+        "--final",
+        help="""Final snapshot number. Default: 0.""",
+        default=0,
+        required=False,
+        type=int,
+    )
+
+    parser.add_argument(
+        "-s",
+        "--stub",
+        help="""Root of the snapshots filenames (e.g. snapshot). This is
+                the first part of the filename for the snapshots,
+                not including the final underscore. Required.""",
+        type=str,
+        required=True,
+    )
+
+    args = vars(parser.parse_args())
+
+    if args["final"] <= args["initial"]:
+        # Run in single image mode.
+        filename = "{:s}_{:04d}".format(args["stub"], args["initial"])
+
+        make_single_image(filename, image_resolution=image_resolution)
+
+    else:
+        # Movie mode!
+        make_movie(args, image_resolution=image_resolution)
diff --git a/examples/SinkParticles/HomogeneousBox/rhoTPlot.py b/examples/SinkParticles/HomogeneousBox/rhoTPlot.py
new file mode 100644
index 0000000000000000000000000000000000000000..05f5371da0464e5c2c3daac1d436e402dddc3f89
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/rhoTPlot.py
@@ -0,0 +1,388 @@
+"""
+Makes a rho-T plot. Uses the swiftsimio library.
+"""
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+import swiftsimio as sw
+
+import unyt
+from unyt import mh, cm
+from tqdm import tqdm
+from matplotlib.colors import LogNorm
+from matplotlib.animation import FuncAnimation
+
+# %%
+# Constants; these could be put in the parameter file but are rarely changed.
+density_bounds = [1e-6, 1e6]  # in nh/cm^3
+temperature_bounds = [1e0, 1e8]  # in K
+bins = 128
+
+# Plotting controls
+cmap = "viridis"
+
+# %%
+
+
+def get_gas_mu(data: sw.SWIFTDataset) -> np.array:
+    """
+    Return the mean molecular weight of the gas.
+    """
+    from unyt.physical_constants import mh
+
+    # Get the cooling model
+    cooling = data.metadata.subgrid_scheme["Cooling Model"]
+
+    # Use a variable for better readability
+    gas = data.gas
+
+    # Get rho
+    rho = gas.densities.to(unyt.g / unyt.cm ** 3)  # self.Rho(units='g/cm3')
+
+    # hydrogen mass in gram
+    mh.convert_to_cgs()
+    mH_in_g = mh
+
+    if cooling == b"Grackle3":  # grackle3
+        nHI = gas.hi * rho / (mH_in_g)
+        nHII = gas.hii * rho / (mH_in_g)
+        nHeI = gas.he_i * rho / (4 * mH_in_g)
+        nHeII = gas.he_ii * rho / (4 * mH_in_g)
+        nHeIII = gas.he_iii * rho / (4 * mH_in_g)
+        nH2I = gas.h2_i * rho / (2 * mH_in_g)
+        nH2II = gas.h2_ii * rho / (2 * mH_in_g)
+        nHDI = gas.hdi * rho / (3 * mH_in_g)
+
+        nel = nHII + nHeII + 2 * nHeIII + nH2II
+        mu = (
+            (nHI + nHII) + (nHeI + nHeII + nHeIII) * 4 + (nH2I + nH2II) * 2 + nHDI * 3
+        ) / (nHI + nHII + nHeI + nHeII + nHeIII + nH2I + nH2II + nHDI + nel)
+        return mu
+
+    elif cooling == b"Grackle2":  # grackle2
+        nHI = gas.hi * rho / (mH_in_g)
+        nHII = gas.hii * rho / (mH_in_g)
+        nHeI = gas.he_i * rho / (4 * mH_in_g)
+        nHeII = gas.he_ii * rho / (4 * mH_in_g)
+        nHeIII = gas.he_iii * rho / (4 * mH_in_g)
+        nH2I = gas.h2_i * rho / (2 * mH_in_g)
+        nH2II = gas.h2_ii * rho / (2 * mH_in_g)
+
+        nel = nHII + nHeII + 2 * nHeIII + nH2II
+        mu = ((nHI + nHII) + (nHeI + nHeII + nHeIII) * 4 + (nH2I + nH2II) * 2) / (
+            nHI + nHII + nHeI + nHeII + nHeIII + nH2I + nH2II + nel
+        )
+        return mu
+
+    elif cooling == b"Grackle1":  # grackle1
+        nHI = gas.hi * rho / (mH_in_g)
+        nHII = gas.hii * rho / (mH_in_g)
+        nHeI = gas.he_i * rho / (4 * mH_in_g)
+        nHeII = gas.he_ii * rho / (4 * mH_in_g)
+        nHeIII = gas.he_iii * rho / (4 * mH_in_g)
+        nel = nHII + nHeII + 2 * nHeIII
+        mu = ((nHI + nHII) + (nHeI + nHeII + nHeIII) * 4) / (
+            nHI + nHII + nHeI + nHeII + nHeIII + nel
+        )
+        return mu
+
+    else:  # Grackle0
+        from unyt.physical_constants import kboltz_cgs as k_B_cgs
+
+        gamma = data.metadata.gas_gamma[0]
+
+        # Get internal energy
+        u = data.gas.internal_energies
+        u = u.to_physical()
+        u = u.to(unyt.erg / unyt.g)
+
+        # Get hydrigen fraction
+        H_frac = float(
+            data.metadata.parameters["GrackleCooling:HydrogenFractionByMass"]
+        )
+
+        # Compute T/mu
+        T_over_mu = (gamma - 1.0) * u.value * mH_in_g.value / k_B_cgs.value
+        T_trans = 1.1e4
+        mu_trans = 4.0 / (8.0 - 5.0 * (1.0 - H_frac))
+
+        # Determine if we are ionized or not
+        mu = np.ones(np.size(u))
+        mask_ionized = T_over_mu > (T_trans + 1) / mu_trans
+        mask_neutral = T_over_mu < (T_trans + 1) / mu_trans
+
+        # Give the right mu
+        mu[mask_ionized] = 4.0 / (8.0 - 5.0 * (1.0 - H_frac))
+        mu[mask_neutral] = 4.0 / (1.0 + 3.0 * H_frac)
+
+        return mu
+
+
+def get_gas_temperatures(data: sw.SWIFTDataset) -> np.array:
+    """
+        Compute the temperature of the gas.
+    """
+    from unyt.physical_constants import kboltz_cgs as k_B
+    from unyt.physical_constants import mh
+
+    # Convert to cgs
+    mh.convert_to_cgs()
+
+    # Get the cooling model
+    cooling = data.metadata.subgrid_scheme["Cooling Model"]
+
+    # Get internal energy and convert to physical units in cgs
+    u = data.gas.internal_energies
+    u = u.to_physical()
+    u = u.to(unyt.erg / unyt.g)
+
+    # Get gamm and compute mu
+    gamma = data.metadata.gas_gamma[0]
+    mu = get_gas_mu(data)
+
+    # FInally compute the the temperature
+    if cooling == b"Grackle3" or cooling == b"Grackle2" or cooling == b"Grackle1":
+        T = mu * (gamma - 1.0) * u * mh / k_B
+    else:
+        a = (gamma - 1.0) * (mu * mh) / k_B * u
+        T = np.where((u.value > 0), a.value, 0) * unyt.kelvin
+    return T
+
+
+def get_data(filename):
+    """
+    Grabs the data (T in Kelvin and density in mh / cm^3).
+
+    Note: Converts data.gas.densities to mh/cm^3.
+    """
+    data = sw.SWIFTDataset(filename)
+
+    data.gas.densities = data.gas.densities.to(mh / (cm ** 3))
+    data.gas.temperatures = get_gas_temperatures(data)
+    data.gas.temperatures.convert_to_cgs()
+
+    return data.gas.densities, data.gas.temperatures
+
+
+def make_hist(filename, density_bounds, temperature_bounds, bins):
+    """
+    Makes the histogram for filename with bounds as lower, higher
+    for the bins and "bins" the number of bins along each dimension.
+
+    Also returns the edges for pcolormesh to use.
+    """
+
+    density_bins = np.logspace(
+        np.log10(density_bounds[0]), np.log10(density_bounds[1]), bins
+    )
+    temperature_bins = np.logspace(
+        np.log10(temperature_bounds[0]), np.log10(temperature_bounds[1]), bins
+    )
+
+    # print(density_bins, temperature_bins)
+
+    H, density_edges, temperature_edges = np.histogram2d(
+        *get_data(filename), bins=[density_bins, temperature_bins]
+    )
+
+    return H.T, density_edges, temperature_edges
+
+
+def setup_axes():
+    """
+    Creates the figure and axis object.
+    """
+    fig, ax = plt.subplots(1, figsize=(6, 5), dpi=300)
+
+    ax.set_xlabel("Density [$n_H$ cm$^{-3}$]")
+    ax.set_ylabel("Temperature [K]")
+
+    ax.loglog()
+
+    return fig, ax
+
+
+def make_single_image(filename, density_bounds, temperature_bounds, bins):
+    """
+    Makes a single image and saves it to rhoTPlot_{filename}.png.
+
+    Filename should be given _without_ hdf5 extension.
+    """
+
+    fig, ax = setup_axes()
+    hist, rho, T = make_hist(
+        "{:s}.hdf5".format(filename), density_bounds, temperature_bounds, bins
+    )
+
+    mappable = ax.pcolormesh(rho, T, hist, cmap=cmap, norm=LogNorm())
+    fig.colorbar(mappable, label="Number of particles", pad=0)
+
+    fig.tight_layout()
+
+    fig.savefig("rhoTPlot_{:s}.png".format(filename[-4:]))
+
+    return
+
+
+def make_movie(args, density_bounds, temperature_bounds, bins):
+    """
+    Makes a movie and saves it to rhoTPlot_{stub}.mp4.
+    """
+
+    fig, ax = setup_axes()
+
+    def grab_metadata(n):
+        filename = "{:s}_{:04d}.hdf5".format(args["stub"], n)
+        data = sw.load(filename)
+
+        return data.metadata
+
+    def grab_data(n):
+        filename = "{:s}_{:04d}.hdf5".format(args["stub"], n)
+
+        H, _, _ = make_hist(filename, density_bounds, temperature_bounds, bins)
+
+        # Need to ravel because pcolormesh's set_array takes a 1D array. Might
+        # as well do it here, beacuse 1d arrays are easier to max() than 2d.
+        return H.ravel()
+
+    histograms = [
+        grab_data(n)
+        for n in tqdm(
+            range(args["initial"], args["final"] + 1), desc="Histogramming data"
+        )
+    ]
+
+    metadata = [
+        grab_metadata(n)
+        for n in tqdm(
+            range(args["initial"], args["final"] + 1), desc="Grabbing metadata"
+        )
+    ]
+
+    # Need to get a reasonable norm so that we don't overshoot.
+    max_particles = max([x.max() for x in histograms])
+
+    norm = LogNorm(vmin=1, vmax=max_particles)
+
+    # First, let's make the initial frame (we need this for our d, T values that we
+    # got rid of in grab_data.
+    hist, d, T = make_hist(
+        "{:s}_{:04d}.hdf5".format(args["stub"], args["initial"]),
+        density_bounds,
+        temperature_bounds,
+        bins,
+    )
+
+    mappable = ax.pcolormesh(d, T, hist, cmap=cmap, norm=norm)
+    fig.colorbar(mappable, label="Number of particles", pad=0)
+
+    fig.tight_layout()
+
+    # Once we've rearranged the figure with tight_layout(), we can start laying
+    # Down the metadata text.
+
+    def format_metadata(metadata: sw.SWIFTMetadata):
+        t = metadata.t
+        t.convert_to_units(unyt.Myr)
+
+        x = "$a$: {:2.2f}\n$z$: {:2.2f}\n$t$: {:2.2f}".format(metadata.a, metadata.z, t)
+
+        return x
+
+    text = ax.text(
+        0.025,
+        0.975,
+        format_metadata(metadata[0]),
+        ha="left",
+        va="top",
+        transform=ax.transAxes,
+    )
+
+    ax.text(
+        0.975,
+        0.975,
+        metadata[0].code["Git Revision"].decode("utf-8"),
+        ha="right",
+        va="top",
+        transform=ax.transAxes,
+    )
+
+    def animate(data):
+        mappable.set_array(histograms[data])
+        text.set_text(format_metadata(metadata[data]))
+
+        return mappable
+
+    animation = FuncAnimation(
+        fig, animate, range(len(histograms)), fargs=[], interval=1000 / 25
+    )
+
+    animation.save("rhoTPlot.mp4")
+
+    return
+
+
+# %%
+if __name__ == "__main__":
+    import argparse as ap
+
+    parser = ap.ArgumentParser(
+        description="""
+             Plotting script for making a rho-T plot.
+             Takes the filename handle, start, and (optionally) stop
+             snapshots. If stop is not given, png plot is produced for
+             that snapshot. If given, a movie is made.
+             """
+    )
+
+    parser.add_argument(
+        "-i",
+        "--initial",
+        help="""Initial snapshot number. Default: 0.""",
+        default=0,
+        required=False,
+        type=int,
+    )
+
+    parser.add_argument(
+        "-f",
+        "--final",
+        help="""Final snapshot number. Default: 0.""",
+        default=0,
+        required=False,
+        type=int,
+    )
+
+    parser.add_argument(
+        "-s",
+        "--stub",
+        help="""Root of the snapshots filenames (e.g. snapshot). This is
+                the first part of the filename for the snapshots,
+                not including the final underscore. Required.""",
+        type=str,
+        required=True,
+    )
+
+    args = vars(parser.parse_args())
+
+    if args["final"] <= args["initial"]:
+        # Run in single image mode.
+        filename = "{:s}_{:04d}".format(args["stub"], args["initial"])
+
+        make_single_image(
+            filename,
+            density_bounds=density_bounds,
+            temperature_bounds=temperature_bounds,
+            bins=bins,
+        )
+
+    else:
+        # Movie mode!
+        make_movie(
+            args,
+            density_bounds=density_bounds,
+            temperature_bounds=temperature_bounds,
+            bins=bins,
+        )
diff --git a/examples/SinkParticles/HomogeneousBox/run.sh b/examples/SinkParticles/HomogeneousBox/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..1f273b1291342277c4bc4b067b06eb71b1e50086
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBox/run.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+n_threads=${n_threads:=8}  #Number of threads to use
+level=${level:=5}  #Number of particles = 2^(3*level)
+jeans_length=${jeans_length:=0.250} #Jeans wavelength in unit of the boxsize
+debug=${debug:=0}
+
+
+#Create the ICs if they do not exist
+if [ ! -e ICs_homogeneous_box.hdf5 ]
+then
+    echo "Generating initial conditions to run the example..."
+    python3 makeIC.py --level $level -o ICs_homogeneous_box.hdf5 --lJ $jeans_length
+fi
+
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ./getGrackleCoolingTable.sh
+fi
+
+
+if [ ! -e POPIIsw.h5 ]
+then
+    echo "Fetching the chemistry tables..."
+    ./getChemistryTable.sh
+fi
+
+
+# Create output directory
+DIR="snap"
+if [ -d "$DIR" ]; then
+    echo "$DIR directory exists. Its content will be removed."
+    rm -r "$DIR"
+fi
+mkdir "$DIR"
+
+if [[ -z "$debug" || "$debug" -eq 0 ]]; then
+    
+    printf "Running simulation..."
+    ../../../swift --hydro --sinks --stars --self-gravity --feedback \
+		   --cooling --sync --limiter --threads=$n_threads \
+		   params.yml 2>&1 | tee output.log
+
+    #Do some data analysis to show what's in this box
+    python3 plot_gas_density.py -i 282 -s 'snap/snapshot'
+    python3 rhoTPlot.py -i 282 -s 'snap/snapshot'
+    python3 rhoTPlot.py -i 0 -f 282 -s 'snap/snapshot'
+    python3 plot_gas_density.py -i 0 -f 282 -s 'snap/snapshot'
+else
+    # Get the debugging ICs
+    if [ ! -e snapshot_0003restart.hdf5 ]
+    then
+	echo "Fetching the debugging ICs..."
+	./getDebuggingICs.sh
+    fi
+
+    # Get the Grackle cooling table
+    if [ ! -e CloudyData_UVB=HM2012_high_density.h5 ]
+    then
+	echo "Fetching the Cloudy tables required by Grackle..."
+	./getGrackleCoolingTable.sh
+    fi
+
+    echo "Running simulation in debug mode..."
+    ../../../swift --hydro --sinks --stars --self-gravity --feedback \
+		   --cooling --sync --limiter --threads=$n_threads \
+		   params_debug.yml 2>&1 | tee output.log
+fi
diff --git a/examples/SinkParticles/HomogeneousBoxSinkParticles/README b/examples/SinkParticles/HomogeneousBoxSinkParticles/README
new file mode 100644
index 0000000000000000000000000000000000000000..8669089494c529ed3e2fbe4c9ebc8c084574eeea
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/README
@@ -0,0 +1,24 @@
+# Intro
+This example is a modified version of the HomogeneousBox in SWIFT that allows to add sink particles to the box. By default, there is no gravity.
+
+The default level is 5 (N_particle = 2^(3*level)). It is quick. If you want to try the example with higher levels, we recommend using HPC facilities.
+
+*This example is mainly used for sink MPI debugging.*
+
+# Configure
+
+To run this example with GEAR model,
+
+./configure --with-chemistry=GEAR_10 --with-cooling=grackle_0 --with-stars=GEAR --with-star-formation=GEAR --with-feedback=GEAR --with-sink=GEAR --with-kernel=wendland-C2 --with-grackle=path/to/grackle --enable-debug --enable-debugging-checks
+
+and then
+
+make -j
+
+# ICs
+The run.sh script calls `makeIC.py' script with default values. You can experiment by changing the ICs. Run `python3 makeIC.py --help` to get the list of parameters.
+
+# Run
+Type `run.sh` (or `n_ranks=4 mpi_run.sh`), and let's go!
+
+You can provide parameters to the running scripts, have a look inside them.
diff --git a/examples/SinkParticles/HomogeneousBoxSinkParticles/getChemistryTable.sh b/examples/SinkParticles/HomogeneousBoxSinkParticles/getChemistryTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b10fd23964158ee7a38d352dbd0ddd9beb7bdd77
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/getChemistryTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/FeedbackTables/POPIIsw.h5
diff --git a/examples/SinkParticles/HomogeneousBoxSinkParticles/getGrackleCoolingTable.sh b/examples/SinkParticles/HomogeneousBoxSinkParticles/getGrackleCoolingTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e3eb106240709c80151a48625567d2cd78e5f568
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/getGrackleCoolingTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012.h5
diff --git a/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py b/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..6b710d7675e709ff98f5c4de166266bfa7ea95b2
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py
@@ -0,0 +1,268 @@
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Yves Revaz (yves.revaz@epfl.ch)
+#                         2024 Darwin Roduit (darwin.roduit@epfl.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+
+import h5py
+import numpy as np
+import argparse
+from astropy import units
+from astropy import constants
+
+
+class store_as_array(argparse._StoreAction):
+    """Provides numpy array as argparse arguments."""
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        values = np.array(values)
+        return super().__call__(parser, namespace, values, option_string)
+
+
+def parse_options():
+
+    usage = "usage: %prog [options] file"
+    parser = argparse.ArgumentParser(description=usage)
+
+    parser.add_argument(
+        "--lJ",
+        action="store",
+        dest="lJ",
+        type=float,
+        default=0.250,
+        help="Jeans wavelength in box size unit",
+    )
+
+    parser.add_argument(
+        "--rho",
+        action="store",
+        dest="rho",
+        type=float,
+        default=0.1,
+        help="Mean gas density in atom/cm3",
+    )
+
+    parser.add_argument(
+        "--mass",
+        action="store",
+        dest="mass",
+        type=float,
+        default=50,
+        help="Gas particle mass in solar mass",
+    )
+
+    parser.add_argument(
+        "--level",
+        action="store",
+        dest="level",
+        type=int,
+        default=5,
+        help="Resolution level: N = (2**l)**3",
+    )
+
+    parser.add_argument(
+        "--sink_mass",
+        action="store",
+        type=float,
+        default=50,
+        help="Sink particles mass in solar mass",
+    )
+
+    parser.add_argument(
+        "--sinks_vel",
+        action=store_as_array,
+        nargs=3,
+        type=float,
+        default=np.array([10, 0, 0]),
+        help="Sink particle velocity. All sinks get the same velocity",
+    )
+
+    parser.add_argument(
+        "--sink_pos",
+        action=store_as_array,
+        nargs=3,
+        type=float,
+        default=np.array([0, 0, 0]),
+        help="Sink particle position. Only use it to place one sink particle.",
+    )
+
+    parser.add_argument(
+        "--n_sink",
+        action="store",
+        type=int,
+        default=10,
+        help="Number of sink particles in the box",
+    )
+
+    parser.add_argument(
+        "-o",
+        action="store",
+        dest="outputfilename",
+        type=str,
+        default="box.hdf5",
+        help="output filename",
+    )
+
+    options = parser.parse_args()
+    return options
+
+
+########################################
+# main
+########################################
+
+opt = parse_options()
+
+# define standard units
+UnitMass_in_cgs = 1.988409870698051e43  # 10^10 M_sun in grams
+UnitLength_in_cgs = 3.0856775814913673e21  # kpc in centimeters
+UnitVelocity_in_cgs = 1e5  # km/s in centimeters per second
+UnitCurrent_in_cgs = 1  # Amperes
+UnitTemp_in_cgs = 1  # Kelvin
+UnitTime_in_cgs = UnitLength_in_cgs / UnitVelocity_in_cgs
+
+UnitMass = UnitMass_in_cgs * units.g
+UnitLength = UnitLength_in_cgs * units.cm
+UnitTime = UnitTime_in_cgs * units.s
+UnitVelocity = UnitVelocity_in_cgs * units.cm / units.s
+
+np.random.seed(1)
+
+# Number of particles
+N = (2 ** opt.level) ** 3  # number of particles
+
+# Mean density
+rho = opt.rho  # atom/cc
+rho = rho * constants.m_p / units.cm ** 3
+
+# Gas particle mass
+m = opt.mass  # in solar mass
+m = m * units.Msun
+
+# Gas mass in the box
+M = N * m
+
+# Size of the box
+L = (M / rho) ** (1 / 3.0)
+
+# Jeans wavelength in box size unit
+lJ = opt.lJ
+lJ = lJ * L
+
+# Gravitational constant
+G = constants.G
+
+# Jeans wave number
+kJ = 2 * np.pi / lJ
+# Velocity dispersion
+sigma = np.sqrt(4 * np.pi * G * rho) / kJ
+
+
+print("Boxsize                               : {}".format(L.to(units.kpc)))
+print("Number of particles                   : {}".format(N))
+print("Equivalent velocity dispertion        : {}".format(sigma.to(units.m / units.s)))
+
+# Convert to code units
+m = m.to(UnitMass).value
+L = L.to(UnitLength).value
+rho = rho.to(UnitMass / UnitLength ** 3).value
+sigma = sigma.to(UnitVelocity).value
+
+# Generate the particles
+pos = np.random.random([N, 3]) * np.array([L, L, L])
+vel = np.zeros([N, 3])
+mass = np.ones(N) * m
+u = np.ones(N) * sigma ** 2
+ids = np.arange(N)
+h = np.ones(N) * 3 * L / N ** (1 / 3.0)
+rho = np.ones(N) * rho
+
+print("Inter-particle distance (code unit)   : {}".format(L / N ** (1 / 3.0)))
+
+
+#####################
+# Now, take care of the sink
+#####################
+N_sink = opt.n_sink
+m_sink = opt.sink_mass * units.M_sun
+m_sink = m_sink.to(UnitMass).value  # Convert the sink mass to internal units
+
+if N_sink == 1:
+    pos_sink = np.reshape(opt.sinks_vel, (N_sink, 3))
+else:
+    pos_sink = np.random.random([N_sink, 3]) * np.array([L, L, L])
+
+if opt.sinks_vel is not None:
+    vel_sink = np.tile(
+        opt.sinks_vel, (N_sink, 1)
+    )  # Duplicate the velocity for all sinks
+else:
+    np.zeros([N_sink, 3])
+
+mass_sink = np.ones(N_sink) * m_sink
+h_sink = np.ones(N_sink) * 3 * L / (N + N_sink) ** (1 / 3.0)
+ids_sink = np.arange(N, N + N_sink)
+
+#####################
+# Finally write the ICs in the file
+#####################
+
+# File
+fileOutput = h5py.File(opt.outputfilename, "w")
+print("{} saved.".format(opt.outputfilename))
+
+# Header
+grp = fileOutput.create_group("/Header")
+grp.attrs["BoxSize"] = [L, L, L]
+grp.attrs["NumPart_Total"] = [N, 0, 0, N_sink, 0, 0]
+grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_ThisFile"] = [N, 0, 0, N_sink, 0, 0]
+grp.attrs["Time"] = 0.0
+grp.attrs["NumFileOutputsPerSnapshot"] = 1
+grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["Dimension"] = 3
+
+
+# Units
+grp = fileOutput.create_group("/Units")
+grp.attrs["Unit length in cgs (U_L)"] = UnitLength_in_cgs
+grp.attrs["Unit mass in cgs (U_M)"] = UnitMass_in_cgs
+grp.attrs["Unit time in cgs (U_t)"] = UnitTime_in_cgs
+grp.attrs["Unit current in cgs (U_I)"] = UnitCurrent_in_cgs
+grp.attrs["Unit temperature in cgs (U_T)"] = UnitTemp_in_cgs
+
+
+# Write Gas particle group
+grp = fileOutput.create_group("/PartType0")
+grp.create_dataset("Coordinates", data=pos, dtype="d")
+grp.create_dataset("Velocities", data=vel, dtype="f")
+grp.create_dataset("Masses", data=mass, dtype="f")
+grp.create_dataset("SmoothingLength", data=h, dtype="f")
+grp.create_dataset("InternalEnergy", data=u, dtype="f")
+grp.create_dataset("ParticleIDs", data=ids, dtype="L")
+grp.create_dataset("Densities", data=rho, dtype="f")
+
+
+# Write sink particle group
+grp = fileOutput.create_group("/PartType3")
+grp.create_dataset("Coordinates", data=pos_sink, dtype="d")
+grp.create_dataset("Velocities", data=vel_sink, dtype="f")
+grp.create_dataset("Masses", data=mass_sink, dtype="f")
+grp.create_dataset("SmoothingLength", data=h_sink, dtype="f")
+grp.create_dataset("ParticleIDs", data=ids_sink, dtype="L")
+fileOutput.close()
diff --git a/examples/SinkParticles/HomogeneousBoxSinkParticles/mpi_run.sh b/examples/SinkParticles/HomogeneousBoxSinkParticles/mpi_run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..8b884691d68c92902a68b62826c4a7dcfaa9dc91
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/mpi_run.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+n_ranks=${n_ranks:=2}  #Number of MPI ranks
+n_threads=${n_threads:=1}  #Number of threads to use
+level=${level:=5}  #Number of particles = 2^(3*level)
+jeans_length=${jeans_length:=0.250}  #Jeans wavelenght in unit of the boxsize
+n_sinks=${n_sinks:=10}  #Number of sinks
+
+# Remove the ICs
+if [ -e ICs_homogeneous_box.hdf5 ]
+then
+    rm ICs_homogeneous_box.hdf5
+fi
+
+#Create the ICs if they do not exist
+if [ ! -e ICs_homogeneous_box.hdf5 ]
+then
+    echo "Generating initial conditions to run the example..."
+    python3 makeIC.py --level $level -o ICs_homogeneous_box.hdf5 --lJ $jeans_length --n_sink $n_sinks --sink_pos 0 0 0 --sinks_vel 10 10 0
+fi
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ./getGrackleCoolingTable.sh
+fi
+
+if [ ! -e POPIIsw.h5 ]
+then
+    echo "Fetching the chemistry tables..."
+    ./getChemistryTable.sh
+fi
+
+# Create output directory
+DIR=snap #First test of units conversion
+if [ -d "$DIR" ];
+then
+    echo "$DIR directory exists. Its content will be removed."
+    rm -r $DIR
+else
+    echo "$DIR directory does not exists. It will be created."
+    mkdir $DIR
+fi
+
+printf "Running simulation..."
+
+mpirun -n $n_ranks ../../../swift_mpi --pin --hydro --sinks --stars --external-gravity --feedback --threads=$n_threads --verbose 1 params.yml 2>&1 | tee output.log
diff --git a/examples/SinkParticles/HomogeneousBoxSinkParticles/params.yml b/examples/SinkParticles/HomogeneousBoxSinkParticles/params.yml
new file mode 100755
index 0000000000000000000000000000000000000000..3e7dfb075980d7acf74d8f782d26718887301c2f
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/params.yml
@@ -0,0 +1,105 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+43   # 10^10 solar masses 
+  UnitLength_in_cgs:   3.0856775814913673e+21  # 1 kpc 
+  UnitVelocity_in_cgs: 1e5   # km/s
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:          0.025               # Constant dimensionless multiplier for time integration.
+  theta:        0.7                 # Opening angle (Multipole acceptance criterion).
+  max_physical_baryon_softening: 0.005  # Physical softening length (in internal units)
+  mesh_side_length:        32
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:        0.    # The starting time of the simulation (in internal units).
+  time_end:          0.282 # The end time of the simulation (in internal units).
+  dt_min:            1e-10 # The minimal time-step size of the simulation (in internal units).
+  dt_max:            2e-2  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir: snap
+  basename:   snapshot      # Common part of the name of output files
+  time_first: 0. #230 Myr # (Optional) Time of the first output if non-cosmological time-integration (in internal units)
+  delta_time: 10e-3        # Time difference between consecutive outputs (in internal units)
+
+Scheduler:
+  cell_extra_gparts: 100      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sinks: 100       # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sparts: 100      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  max_top_level_cells: 3        #
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:           5e-3     # Time between statistics output
+  time_first:             0.     # (Optional) Time of the first stats output if non-cosmological time-integration (in internal units)
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          ICs_homogeneous_box.hdf5
+  periodic:                    1    # Are we running with periodic ICs?
+  shift:              [0.0,0.0,0.0]
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 57Ngbs with the Wendland C2 kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  h_max:                 5
+  minimal_temperature:   1
+
+
+# Cooling with Grackle 3.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5        # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 0                         # Enable or not the UV background
+  redshift: -1                                  # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 1                         # Enable or not the metal cooling
+  provide_volumetric_heating_rates: 0           # User provide volumetric heating rates
+  provide_specific_heating_rates: 0             # User provide specific heating rates
+  self_shielding_method: -1                     # Grackle (<= 3) or Gear self shielding method
+  self_shielding_threshold_atom_per_cm3: 0.007  # Required only with GEAR's self shielding. Density threshold of the self shielding
+  max_steps: 1000
+  convergence_limit: 1e-2
+  thermal_time_myr: 5
+  maximal_density_Hpcm3: -1                     # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+GEARChemistry:
+  initial_metallicity: 0
+
+GEARFeedback:
+  supernovae_energy_erg: 1e51                     # supernovae energy, used only for SNIa
+  supernovae_efficiency: 0.1                      # supernovae energy efficiency, used for both SNIa and SNII
+  yields_table: POPIIsw.h5
+  yields_table_first_stars: POPIIsw.h5
+  discrete_yields: 1
+  imf_transition_metallicity: -5                  # Maximal metallicity ([Fe/H]) for a first star (0 to deactivate).
+  elements: [Fe, Mg, O, C, Al, Ca, Ba, Zn, Eu]    # Elements to read in the yields table. The number of element should be one less than the number of elements (N) requested during the configuration (--with-chemistry=GEAR_N).
+
+GEARSink:
+  use_fixed_cut_off_radius: 1                 # Are we using a fixed cutoff radius? If we are, in GEAR the cutoff radius is fixed at the value specified below, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+  cut_off_radius: 5e-3                        # Cut off radius of all the sinks in internal units. Ignored if use_fixed_cut_off_radius is 0. 
+  f_acc: 0.1
+  temperature_threshold_K:       100              # Max temperature (in K) for forming a sink when density_threshold_g_per_cm3 <= density <= maximal_density_threshold_g_per_cm3.
+  density_threshold_Hpcm3: 1e1           # Minimum gas density (in g/cm3) required to form a sink particle.
+  maximal_density_threshold_Hpcm3: 1e5   # If the gas density exceeds this value (in g/cm3), a sink forms regardless of temperature if all other criteria are passed.
+  stellar_particle_mass_Msun: 50                  # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_Msun: 8                   # Minimal mass of stars represented by discrete particles, in solar mass
+  stellar_particle_mass_first_stars_Msun: 50      # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_first_stars_Msun: 8       # Minimal mass of stars represented by discrete particles, in solar mass
+  star_spawning_sigma_factor: 0.5                 # Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+  sink_formation_contracting_gas_criterion:   1   # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+  sink_formation_smoothing_length_criterion:  1   # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+  sink_formation_jeans_instability_criterion: 1   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+  sink_formation_bound_state_criterion:       1   # (Optional) Activate the bound state check for sink formation. (Default: 1)
+  sink_formation_overlapping_sink_criterion:  1   # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+  disable_sink_formation:                     0   # (Optional) Disable sink formation. (Default: 0)
+
+  # Timesteps parameters
+  CFL_condition:                        0.5       # Courant-Friedrich-Levy condition for time integration.
diff --git a/examples/SinkParticles/HomogeneousBoxSinkParticles/run.sh b/examples/SinkParticles/HomogeneousBoxSinkParticles/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9db866d75f792434d5fe2d93f9e6668299737dd2
--- /dev/null
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/run.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+n_threads=${n_threads:=8}  #Number of threads to use
+level=${level:=5}  #Number of particles = 2^(3*level)
+jeans_length=${jeans_length:=0.250}  #Jeans wavelenght in unit of the boxsize
+gas_density=${gas_density:=0.1} #Gas density in atom/cm^3
+gas_particle_mass=${gas_particle_mass:=50} #Mass of the gas particles
+n_sinks=${n_sinks:=10}  #Number of sinks
+
+# Remove the ICs
+if [ -e ICs_homogeneous_box.hdf5 ]
+then
+    rm ICs_homogeneous_box.hdf5
+fi
+
+#Create the ICs if they do not exist
+if [ ! -e ICs_homogeneous_box.hdf5 ]
+then
+    echo "Generating initial conditions to run the example..."
+    python3 makeIC.py --level $level -o ICs_homogeneous_box.hdf5 --lJ $jeans_length --n_sink $n_sinks --sink_pos 0 0 0 --sinks_vel 10 10 0
+fi
+
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ./getGrackleCoolingTable.sh
+fi
+
+
+if [ ! -e POPIIsw.h5 ]
+then
+    echo "Fetching the chemistry tables..."
+    ./getChemistryTable.sh
+fi
+
+
+# Create output directory
+DIR=snap #First test of units conversion
+if [ -d "$DIR" ];
+then
+    echo "$DIR directory exists. Its content will be removed."
+    rm -r $DIR
+else
+    echo "$DIR directory does not exists. It will be created."
+    mkdir $DIR
+fi
+
+printf "Running simulation..."
+
+../../../swift --hydro --sinks --stars --external-gravity --feedback --threads=$n_threads params.yml 2>&1 | tee output.log
diff --git a/examples/SinkParticles/IsolatedGalaxy_multi_component_GEAR b/examples/SinkParticles/IsolatedGalaxy_multi_component_GEAR
new file mode 120000
index 0000000000000000000000000000000000000000..8abfe91f453616995753e2edf39a265452bfaf84
--- /dev/null
+++ b/examples/SinkParticles/IsolatedGalaxy_multi_component_GEAR
@@ -0,0 +1 @@
+../IsolatedGalaxy/IsolatedGalaxy_multi_component/GEAR
\ No newline at end of file
diff --git a/examples/SinkParticles/IsolatedGalaxy_sink b/examples/SinkParticles/IsolatedGalaxy_sink
new file mode 120000
index 0000000000000000000000000000000000000000..54a7bc826e4a5b54260407bae741801304135171
--- /dev/null
+++ b/examples/SinkParticles/IsolatedGalaxy_sink
@@ -0,0 +1 @@
+../IsolatedGalaxy/IsolatedGalaxy_sink
\ No newline at end of file
diff --git a/examples/SinkParticles/PlummerSphere/README b/examples/SinkParticles/PlummerSphere/README
new file mode 100644
index 0000000000000000000000000000000000000000..89f97bf37e141d7ff53bdeebb01d3def50cb59c4
--- /dev/null
+++ b/examples/SinkParticles/PlummerSphere/README
@@ -0,0 +1,7 @@
+# Intro
+This example is a Plummer sphere collapsing onto itself. It produces sink particles and test the main algorithms.
+
+# Configure
+To run this example with GEAR model,
+
+./configure --disable-mpi --with-chemistry=GEAR_10 --with-cooling=grackle_0 --with-stars=GEAR --with-star-formation=GEAR --with-feedback=GEAR --with-sink=GEAR --with-kernel=wendland-C2 --with-grackle=path/to/grackle
diff --git a/examples/SinkParticles/PlummerSphere/getChemistryTable.sh b/examples/SinkParticles/PlummerSphere/getChemistryTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b10fd23964158ee7a38d352dbd0ddd9beb7bdd77
--- /dev/null
+++ b/examples/SinkParticles/PlummerSphere/getChemistryTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/FeedbackTables/POPIIsw.h5
diff --git a/examples/SinkParticles/PlummerSphere/getGrackleCoolingTable.sh b/examples/SinkParticles/PlummerSphere/getGrackleCoolingTable.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e3eb106240709c80151a48625567d2cd78e5f568
--- /dev/null
+++ b/examples/SinkParticles/PlummerSphere/getGrackleCoolingTable.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/CoolingTables/CloudyData_UVB=HM2012.h5
diff --git a/examples/SinkParticles/PlummerSphere/params.yml b/examples/SinkParticles/PlummerSphere/params.yml
new file mode 100755
index 0000000000000000000000000000000000000000..262c7dba9742e9d92e35b1e3525aeacfbd5cec65
--- /dev/null
+++ b/examples/SinkParticles/PlummerSphere/params.yml
@@ -0,0 +1,104 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+43   # 10^10 solar masses
+  UnitLength_in_cgs:   3.0856775814913673e+21  # 1 kpc
+  UnitVelocity_in_cgs: 1e5   # km/s
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:          0.025               # Constant dimensionless multiplier for time integration.
+  theta:        0.7                 # Opening angle (Multipole acceptance criterion).
+  max_physical_baryon_softening: 0.0005  # Physical softening length (in internal units)
+  mesh_side_length:        32
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:        0.     # The starting time of the simulation (in internal units).
+  time_end:          12e-3 # The end time of the simulation (in internal units).
+  dt_min:            1e-10  # The minimal time-step size of the simulation (in internal units).
+  dt_max:            1e-3   # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir: snap
+  basename:   snapshot      # Common part of the name of output files
+  time_first: 0.            # (Optional) Time of the first output if non-cosmological time-integration (in internal units)
+  delta_time: 1e-3          # Time difference between consecutive outputs (in internal units)
+
+Scheduler:
+  cell_extra_gparts: 10000      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sinks: 10000       # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sparts: 10000      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  max_top_level_cells: 3
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:           1e-2     # Time between statistics output
+  time_first:             0.     # (Optional) Time of the first stats output if non-cosmological time-integration (in internal units)
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          test_sink.hdf5
+  periodic:                    1    # Are we running with periodic ICs?
+  shift:              [10.0,10.0,10.0]
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+  h_max:                 5
+  minimal_temperature:   1
+
+
+# Cooling with Grackle 3.0
+GrackleCooling:
+  cloudy_table: CloudyData_UVB=HM2012.h5 # Name of the Cloudy Table (available on the grackle bitbucket repository)
+  with_UV_background: 0 # Enable or not the UV background
+  redshift: -1 # Redshift to use (-1 means time based redshift)
+  with_metal_cooling: 0 # Enable or not the metal cooling
+  provide_volumetric_heating_rates: 0 # User provide volumetric heating rates
+  provide_specific_heating_rates: 0 # User provide specific heating rates
+  self_shielding_method: -1 # Grackle (<= 3) or Gear self shielding method
+  self_shielding_threshold_atom_per_cm3: 0.007  # Required only with GEAR's self shielding. Density threshold of the self shielding
+  max_steps: 1000
+  convergence_limit: 1e-2
+  thermal_time_myr: 5
+  maximal_density_Hpcm3: -1    # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+
+GEARChemistry:
+  initial_metallicity: -5
+
+GEARFeedback:
+  supernovae_energy_erg: 1e51           # supernovae energy, used only for SNIa
+  supernovae_efficiency: 0.1           # supernovae energy efficiency, used for both SNIa and SNII
+  yields_table: POPIIsw.h5
+  yields_table_first_stars: POPIIsw.h5
+  discrete_yields: 1
+  imf_transition_metallicity: -5         # Maximal metallicity ([Fe/H]) for a first star (0 to deactivate).
+  #metallicity_max_first_stars: 1.766037e-8                 # Maximal metallicity (in mass fraction) for a first star (-1 to deactivate). (10**-5)*0.0017660372  ==  XFe_sol* 10**[Fe/H]
+  elements: [Fe, Mg, O, C, Al, Ca, Ba, Zn, Eu]              # Elements to read in the yields table. The number of element should be one less than the number of elements (N) requested during the configuration (--with-chemistry=GEAR_N).
+
+GEARSink:
+  use_fixed_cut_off_radius: 1                 # Are we using a fixed cutoff radius? If we are, in GEAR the cutoff radius is fixed at the value specified below, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+  cut_off_radius: 1e-2                        # Cut off radius of all the sinks in internal units. Ignored if use_fixed_cut_off_radius is 0. 
+  f_acc: 0.4
+  temperature_threshold_K: 3e4               # Max temperature (in K) for forming a sink when density_threshold_Hpcm3 <= density <= maximal_density_threshold_Hpcm3.
+  density_threshold_Hpcm3: 1e0               # Minimum gas density (in Hydrogen atoms/cm3) required to form a sink particle.
+  maximal_density_threshold_Hpcm3: 1e10      # If the gas density exceeds this value (in Hydrogen atoms/cm3), a sink forms regardless of temperature if all other criteria are passed.
+  stellar_particle_mass_Msun: 21             # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_Msun: 8              # Minimal mass of stars represented by discrete particles, in solar mass
+  stellar_particle_mass_first_stars_Msun: 20 # Mass of the stellar particle representing the low mass stars, in solar mass
+  minimal_discrete_mass_first_stars_Msun: 8  # Minimal mass of stars represented by discrete particles, in solar mass
+  star_spawning_sigma_factor: 0.1               #  Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+  sink_formation_contracting_gas_criterion: 0     # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+  sink_formation_smoothing_length_criterion: 0    # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+  sink_formation_jeans_instability_criterion: 0   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+  sink_formation_bound_state_criterion: 0         # (Optional) Activate the bound state check for sink formation. (Default: 1)
+  sink_formation_overlapping_sink_criterion: 0    # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+  disable_sink_formation: 0                       # (Optional) Disable sink formation. (Default: 0)
+  CFL_condition:                        0.5       # Courant-Friedrich-Levy condition for time integration.
diff --git a/examples/SinkParticles/PlummerSphere/run.sh b/examples/SinkParticles/PlummerSphere/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0e3c5d71c788f45700a57c47a4ef50d8065b714d
--- /dev/null
+++ b/examples/SinkParticles/PlummerSphere/run.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+
+if [ ! -e test_sink.hdf5 ]
+then
+    echo "Fetching initial conditions to run the example..."
+    wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/test_sink.hdf5
+fi
+
+
+# Get the Grackle cooling table
+if [ ! -e CloudyData_UVB=HM2012.h5 ]
+then
+    echo "Fetching the Cloudy tables required by Grackle..."
+    ./getGrackleCoolingTable.sh
+fi
+
+
+if [ ! -e POPIIsw.h5 ]
+then
+    echo "Fetching the chemistry tables..."
+    ./getChemistryTable.sh
+fi
+
+printf "Running simulation..."
+
+../../../swift --hydro --sinks --stars --self-gravity --feedback --cooling --threads=1 params.yml 2>&1 | tee output.log 
diff --git a/examples/SinkParticles/SingleSink/README b/examples/SinkParticles/SingleSink/README
new file mode 100644
index 0000000000000000000000000000000000000000..1498298fce434d0729a5ba32ed72d037cf0f570c
--- /dev/null
+++ b/examples/SinkParticles/SingleSink/README
@@ -0,0 +1,31 @@
+# Intro
+This example is a non cosmological homogeneous box containing gas and a single sink in the centre. It's designed for testing that your sink model accretes the amount of gas that you're expecting. It's mainly designed to test the Basic model, which requires no extra subgrid physics to function.
+
+# ICs
+The included python script `make_sink_ic.py` creates the initial conditions. You can set the particle masses of the gas and sink, the gas density and velocity dispersion (which sets the internal energy), and set an overall "level", which determines how many particles to simulate (N_particle = 2^(3*level)). Run `python make_sink_ic.py --help` to get the list of parameters.
+
+Running the script with no options produces a "level 6" box at "m5" resolution, with a sink mass 50x that of the gas mass, and a density and velocity dispersion of 0.1 atoms/cc and 10 km/s respectively, representative of the ISM. The ICs have the default name `ics.hdf5`.
+
+# Configure
+To run this example with GEAR model,
+
+./configure --with-sink=Basic --with-kernel=wendland-C2
+
+and then
+
+make -j
+
+You can also add `--enable-sink-density-checks=<CHECK_FREQUENCY>` to run the brute-force density checks for the sink, to make sure things are being calculated properly.
+
+# Run
+We can now run with 
+
+swift --hydro --self-gravity --sinks --threads=<NUM_THREADS> params.yml
+
+By default the simulation will run for 500 Myr, and output a snapshot every 10 Myr. You can change these options in your paramfile.
+
+# Testing the output
+
+Included in this directory is a very simple script for making some test plots. You can run it with
+
+python make_test_plots.py <snapshot-directory-name>
\ No newline at end of file
diff --git a/examples/SinkParticles/SingleSink/make_sink_ic.py b/examples/SinkParticles/SingleSink/make_sink_ic.py
new file mode 100755
index 0000000000000000000000000000000000000000..12179ede3259e0c65089d22844b07611548289cc
--- /dev/null
+++ b/examples/SinkParticles/SingleSink/make_sink_ic.py
@@ -0,0 +1,236 @@
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2022 Yves Revaz (yves.revaz@epfl.ch)
+# Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+
+import h5py
+import numpy as np
+from optparse import OptionParser
+from astropy import units
+from astropy import constants
+
+
+def parse_options():
+
+    usage = "usage: %prog [options] file"
+    parser = OptionParser(usage=usage)
+
+    parser.add_option(
+        "--rho",
+        action="store",
+        dest="rho",
+        type="float",
+        default=0.1,
+        help="Mean gas density in atom/cm3",
+    )
+
+    parser.add_option(
+        "--sigma",
+        action="store",
+        dest="sigma",
+        type="float",
+        default=10.0,
+        help="Velocity dispersion of the gas in km/s (sets internal energy).",
+    )
+
+    parser.add_option(
+        "--gas-mass",
+        action="store",
+        dest="gas_mass",
+        type="float",
+        default=1e5,
+        help="Gas particle mass in solar masses",
+    )
+
+    parser.add_option(
+        "--sink-mass",
+        action="store",
+        dest="sink_mass",
+        type="float",
+        default=5e6,
+        help="Sink particle mass in solar masses",
+    )
+
+    parser.add_option(
+        "--sink-velocity",
+        action="store",
+        dest="sink_mach",
+        type="float",
+        default=0.0,
+        help="Sink velocity as a multiple of the sound speed (i.e. Mach number)",
+    )
+
+    parser.add_option(
+        "--level",
+        action="store",
+        dest="level",
+        type="int",
+        default=6,
+        help="Resolution level: N = (2**l)**3",
+    )
+
+    parser.add_option(
+        "-o",
+        action="store",
+        dest="outputfilename",
+        type="string",
+        default="ics.hdf5",
+        help="output filename",
+    )
+
+    (options, args) = parser.parse_args()
+
+    files = args
+
+    return files, options
+
+
+########################################
+# main
+########################################
+
+files, opt = parse_options()
+
+# define standard units
+UnitMass_in_cgs = 1.988409870698051e43  # 10^10 M_sun in grams
+UnitLength_in_cgs = 3.0856775814913673e21  # kpc in centimeters
+UnitVelocity_in_cgs = 1e5  # km/s in centimeters per second
+UnitCurrent_in_cgs = 1  # Amperes
+UnitTemp_in_cgs = 1  # Kelvin
+UnitTime_in_cgs = UnitLength_in_cgs / UnitVelocity_in_cgs
+
+UnitMass = UnitMass_in_cgs * units.g
+UnitLength = UnitLength_in_cgs * units.cm
+UnitTime = UnitTime_in_cgs * units.s
+UnitVelocity = UnitVelocity_in_cgs * units.cm / units.s
+
+np.random.seed(1)
+
+# Number of particles
+N = (2 ** opt.level) ** 3  # number of particles
+
+# Mean density
+rho = opt.rho  # atom/cc
+rho = rho * constants.m_p / units.cm ** 3
+
+# Gas particle mass
+m = opt.gas_mass * units.Msun  # in solar mass
+
+# Sink particle mass
+sm = opt.sink_mass * units.Msun  # in solar mass
+
+# Gas mass in the box
+M = N * m
+
+# Size of the box
+L = (M / rho) ** (1 / 3.0)
+
+# Gravitational constant
+G = constants.G
+
+print("Number of particles                   : {}".format(N))
+
+# Convert to code units
+m = m.to(UnitMass).value
+sm = sm.to(UnitMass).value
+L = L.to(UnitLength).value
+rho = rho.to(UnitMass / UnitLength ** 3).value
+sigma = (opt.sigma * units.km / units.s).to(UnitVelocity).value
+
+# Generate the particles
+pos = np.random.random([N, 3]) * np.array([L, L, L])
+vel = np.zeros([N, 3])
+mass = np.ones(N) * m
+u = np.ones(N) * sigma ** 2
+ids = np.arange(N)
+h = np.ones(N) * 3 * L / N ** (1 / 3.0)
+rho = np.ones(N) * rho
+
+print("Inter-particle distance (code unit)   : {}".format(L / N ** (1 / 3.0)))
+
+# Generate the sink
+# Always put it 10% of the way through the box to give it room to move (if it's going to)
+
+NSINK = 1
+
+sink_pos = np.ones([NSINK, 3])
+sink_pos[:, 0] = L / 10.0
+sink_pos[:, 1] = L / 2.0
+sink_pos[:, 2] = L / 2.0
+
+sink_mass = np.array([sm])
+sink_ids = np.array([2 * ids[-1]])
+sink_h = np.array([3 * L / N ** (1 / 3.0)])
+
+gas_cs = np.sqrt(sigma ** 2 * 5.0 / 3.0 * ((5.0 / 3.0) - 1))
+
+sink_vel = np.zeros([NSINK, 3])
+sink_vel[:, 0] += gas_cs * opt.sink_mach
+
+print(f"Sink velocity: {gas_cs * opt.sink_mach}")
+
+if gas_cs * opt.sink_mach > 0:
+    sink_time_in_box = L * 0.9 / (gas_cs * opt.sink_mach)
+    print(
+        f"Sink will leave box (neglecting dynamical friction) after time: {sink_time_in_box}"
+    )
+
+
+# File
+fileOutput = h5py.File(opt.outputfilename, "w")
+
+# Header
+grp = fileOutput.create_group("/Header")
+grp.attrs["BoxSize"] = [L, L, L]
+grp.attrs["NumPart_Total"] = [N, 0, 0, NSINK, 0, 0]
+grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["NumPart_ThisFile"] = [N, 0, 0, NSINK, 0, 0]
+grp.attrs["Time"] = 0.0
+grp.attrs["NumFileOutputsPerSnapshot"] = 1
+grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+grp.attrs["Flag_Entropy_ICs"] = [0, 0, 0, 0, 0, 0]
+grp.attrs["Dimension"] = 3
+
+# Units
+grp = fileOutput.create_group("/Units")
+grp.attrs["Unit length in cgs (U_L)"] = UnitLength_in_cgs
+grp.attrs["Unit mass in cgs (U_M)"] = UnitMass_in_cgs
+grp.attrs["Unit time in cgs (U_t)"] = UnitTime_in_cgs
+grp.attrs["Unit current in cgs (U_I)"] = UnitCurrent_in_cgs
+grp.attrs["Unit temperature in cgs (U_T)"] = UnitTemp_in_cgs
+
+# Particle groups
+grp = fileOutput.create_group("/PartType0")
+grp.create_dataset("Coordinates", data=pos, dtype="d")
+grp.create_dataset("Velocities", data=vel, dtype="f")
+grp.create_dataset("Masses", data=mass, dtype="f")
+grp.create_dataset("SmoothingLength", data=h, dtype="f")
+grp.create_dataset("InternalEnergy", data=u, dtype="f")
+grp.create_dataset("ParticleIDs", data=ids, dtype="L")
+grp.create_dataset("Densities", data=rho, dtype="f")
+
+grp = fileOutput.create_group("/PartType3")
+grp.create_dataset("Coordinates", data=sink_pos, dtype="d")
+grp.create_dataset("Velocities", data=sink_vel, dtype="f")
+grp.create_dataset("Masses", data=sink_mass, dtype="f")
+grp.create_dataset("SmoothingLength", data=sink_h, dtype="f")
+grp.create_dataset("ParticleIDs", data=sink_ids, dtype="L")
+
+fileOutput.close()
+
+print(f"{opt.outputfilename} saved.")
diff --git a/examples/SinkParticles/SingleSink/make_test_plots.py b/examples/SinkParticles/SingleSink/make_test_plots.py
new file mode 100644
index 0000000000000000000000000000000000000000..7bb69048cbcaed90d974ddb29cb2db4cc618c4d7
--- /dev/null
+++ b/examples/SinkParticles/SingleSink/make_test_plots.py
@@ -0,0 +1,135 @@
+################################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+################################################################################
+
+"""
+Make some very simple plots showing the evolution of the single sink.
+
+Run with python make_test_plots.py <snapshot-directory-name>
+
+"""
+
+
+import numpy as np
+from sys import argv
+from glob import glob
+import matplotlib.pyplot as plt
+import unyt
+import h5py
+
+params = {
+    "text.usetex": True,
+    "axes.labelsize": 16,
+    "xtick.labelsize": 13,
+    "ytick.labelsize": 13,
+    "lines.linewidth": 2,
+    "axes.titlesize": 16,
+    "font.family": "serif",
+}
+plt.rcParams.update(params)
+
+# Units
+G_cgs = 6.6743e-8 * unyt.cm ** 3 * unyt.g ** -1 * unyt.s ** -2
+unit_mass_cgs = 1.988409870698051e43 * unyt.g
+unit_density_cgs = 6.767905323247329e-22 * unyt.Unit("g/cm**3")
+unit_velocity_cgs = (1.0 * unyt.Unit("km/s")).to("cm/s")
+
+
+# Basic Bondi-Hoyle prediction from the sink's starting mass in a constant medium
+def simple_bondi_hoyle(t, m, rho, v, cs):
+
+    m_sink = np.zeros(len(t), dtype=np.float64) * unyt.g
+
+    m_sink[0] = m[0] * unit_mass_cgs
+
+    rho_0 = rho[0] * unit_density_cgs
+    v_0 = v[0] * unit_velocity_cgs
+    cs_0 = cs[0] * unit_velocity_cgs
+
+    timestep = ((t[1] - t[0]) * unyt.Gyr).to("s")
+
+    for i in range(len(times) - 1):
+
+        numerator = 4.0 * np.pi * G_cgs ** 2 * m_sink[i] ** 2 * rho_0
+        denominator = np.power(v_0 ** 2 + cs_0 ** 2, 3.0 / 2.0)
+
+        m_sink[i + 1] = m_sink[i] + (numerator / denominator) * timestep
+
+    return m_sink.to("Msun")
+
+
+snapshots = glob(f"./{argv[1]}/*.hdf5")
+
+times = np.empty(len(snapshots), dtype=np.float32)
+mass_evo = np.empty(len(snapshots), dtype=np.float32)
+subgrid_mass_evo = np.empty(len(snapshots), dtype=np.float32)
+rho_evo = np.empty(len(snapshots), dtype=np.float32)
+v_evo = np.empty(len(snapshots), dtype=np.float32)
+cs_evo = np.empty(len(snapshots), dtype=np.float32)
+hsml_evo = np.empty(len(snapshots), dtype=np.float32)
+
+
+for s, snap in enumerate(snapshots):
+
+    with h5py.File(snap) as f:
+        times[s] = f["Header"].attrs["Time"][0]
+        mass_evo[s] = f["PartType3/Masses"][0]
+        subgrid_mass_evo[s] = f["PartType3/SubgridMasses"][0]
+        rho_evo[s] = f["PartType3/GasDensities"][0]
+        cs_evo[s] = f["PartType3/GasSoundSpeeds"][0]
+        hsml_evo[s] = f["PartType3/SmoothingLengths"][0]
+
+        v = f["PartType3/GasVelocities"][0] - f["PartType3/Velocities"][0]
+        v_evo[s] = np.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2)
+
+
+# Run the Bondi-Hoyle prediction
+m_sink_bondi_prediction = simple_bondi_hoyle(times, mass_evo, rho_evo, v_evo, cs_evo)
+
+# Normalise time to go up to 1
+t = times / times[-1]
+
+# Time evolution of the sink mass, target mass, and Bondi-Hoyle prediction
+fig, ax = plt.subplots(1, figsize=(8, 6))
+ax.plot(
+    t,
+    np.log10(m_sink_bondi_prediction),
+    label=r"Simple Bondi-Hoyle, constant $\rho$, $v$, $c_{\rm s}$",
+)
+ax.plot(t, np.log10(subgrid_mass_evo * 1e10), label=r"Subgrid mass")
+ax.plot(t, np.log10(mass_evo * 1e10), label=r"Dynamical mass")
+
+ax.set_xlabel(r"$t/t_{\rm final}$")
+ax.set_ylabel(r"$\log_{10}(M_{\rm sink})$")
+ax.legend(fontsize=14)
+ax.set_ylim(6.68, 7.0)
+fig.savefig("mass_evolution.png", bbox_inches="tight")
+
+# Time evolution of the total mass accreted relative to the target mass
+fig, ax = plt.subplots(1, figsize=(8, 6))
+ax.plot(t, mass_evo / subgrid_mass_evo)
+ax.set_xlabel(r"$t/t_{\rm final}$")
+ax.set_ylabel(r"$m_{\rm accr}^{\rm gas}/m_{\rm target}^{\rm gas}$")
+fig.savefig("target_mass_ratio.png", bbox_inches="tight")
+
+# Time evolution of the smoothing length
+fig, ax = plt.subplots(1, figsize=(8, 6))
+ax.plot(t, hsml_evo)
+ax.set_xlabel(r"$t/t_{\rm final}$")
+ax.set_ylabel(r"$h_{\rm sink}$ [kpc]")
+fig.savefig("smoothing_length.png", bbox_inches="tight")
diff --git a/examples/SinkParticles/SingleSink/params.yml b/examples/SinkParticles/SingleSink/params.yml
new file mode 100755
index 0000000000000000000000000000000000000000..eafa709d3687fd55ff2f2c73a5f8176c58c94b38
--- /dev/null
+++ b/examples/SinkParticles/SingleSink/params.yml
@@ -0,0 +1,61 @@
+# Define the system of units to use internally.
+InternalUnitSystem:
+  UnitMass_in_cgs:     1.988409870698051e+43   # 10^10 solar masses 
+  UnitLength_in_cgs:   3.0856775814913673e+21  # 1 kpc 
+  UnitVelocity_in_cgs: 1e5   # km/s
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+
+# Parameters for the self-gravity scheme
+Gravity:
+  MAC:                           adaptive  # Choice of mulitpole acceptance criterion: 'adaptive' OR 'geometric'.
+  epsilon_fmm:                   0.001     # Tolerance parameter for the adaptive multipole acceptance criterion.
+  theta_cr:                      0.7       # Opening angle for the purely gemoetric criterion.
+  eta:          0.025               # Constant dimensionless multiplier for time integration.
+  theta:        0.7                 # Opening angle (Multipole acceptance criterion).
+  max_physical_baryon_softening: 0.35  # Physical softening length (in internal units)
+  mesh_side_length:        32
+
+# Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
+TimeIntegration:
+  time_begin:        0.    # The starting time of the simulation (in internal units).
+  time_end:          0.5   # 500 Myr # The end time of the simulation (in internal units).
+  dt_min:            1e-10  # The minimal time-step size of the simulation (in internal units).
+  dt_max:            1e-2  # The maximal time-step size of the simulation (in internal units).
+
+# Parameters governing the snapshots
+Snapshots:
+  subdir: snap
+  basename:   snapshot      # Common part of the name of output files
+  time_first: 0. # (Optional) Time of the first output if non-cosmological time-integration (in internal units)
+  delta_time: 1e-2        # Time difference between consecutive outputs (in internal units)
+
+Scheduler:
+  cell_extra_gparts: 10000      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sinks: 10000       # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  cell_extra_sparts: 10000      # (Optional) Number of spare sparts per top-level allocated at rebuild time for on-the-fly creation.
+  max_top_level_cells: 3        #
+  dependency_graph_frequency: 0  # (Optional) Dumping frequency of the dependency graph. By default, writes only at the first step.
+
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  delta_time:           1e-3     # Time between statistics output
+  time_first:             0.     # (Optional) Time of the first stats output if non-cosmological time-integration (in internal units)
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:          ics.hdf5
+  periodic:                    1    # Are we running with periodic ICs?
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 57Ngbs with the Wendland C2 kernel).
+  # resolution_eta:        1.55   # Double the number of neighbours  
+  CFL_condition:         0.2      # Courant-Friedrich-Levy condition for time integration.
+  h_max:                 250.
+  minimal_temperature:   1
+
+BasicSink:
+  use_nibbling: 1
+  min_gas_mass_for_nibbling_Msun: 5e4 # half the gas particle mass in msun
diff --git a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/README b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/README
index 14a289cf4a1d638c18f421f23ca8bcf0ced68d1b..c1bbf64e21599eba5c499b3d0798a080998897b7 100644
--- a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/README
+++ b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/README
@@ -7,3 +7,12 @@ on the options to cancel the h-factors and a-factors at reading time.
 
 MD5 checksum of the ICs:
 08736c3101fd738e22f5159f78e6022b  small_cosmo_volume.hdf5
+
+# How to run with CSDS
+
+To enable the CSDS, add --enable-csds --enable-python when
+configuring swift.
+
+In `run_csds.sh`, update `velociraptor_path` to the location
+of velociraptor on your system. Then, use `./run_csds.sh` and
+let's go!
diff --git a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/csds_analysis.py b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/csds_analysis.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ae897fc46a0b23ea2e3d8ff41ec8dcaf6e862fb
--- /dev/null
+++ b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/csds_analysis.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Plots the evolution of the DM particles with the CSDS.
+"""
+
+import sys
+import os
+import argparse
+import numpy as np
+import matplotlib.pyplot as plt
+from velociraptor import load
+from velociraptor.particles import load_groups
+
+# Include the CSDS library to the path before importing the CSDS python module
+sys.path.append("../../../csds/src/.libs/")
+
+# Now we can load the csds
+import libcsds as csds
+
+#%%
+def get_redshift(a):
+    return 1 / a - 1
+
+
+def get_scale_factor(z):
+    return 1 / (z + 1)
+
+
+#%% Argparse options
+def parse_option():
+    description = """"Plots the evolution of the DM particles with the CSDS. """
+    epilog = """
+Examples:
+--------
+
+python3 csds_analysis.py csds_index_0000.dump
+python3 csds_analysis.py csds_index_0000.dump --halo 2
+python3 csds_analysis.py csds_index_0000.dump --halo 2 -n 250
+"""
+    parser = argparse.ArgumentParser(description=description, epilog=epilog)
+
+    parser.add_argument("csds_file", type=str, help="CSDS .dump file name.")
+
+    parser.add_argument(
+        "--halo",
+        action="store",
+        type=int,
+        dest="halo_index",
+        default=1,
+        help="Halo id of which we want trace the evolution. The halo id are given by velociraptor.",
+    )
+
+    parser.add_argument(
+        "-n",
+        action="store",
+        type=int,
+        default=500,
+        help="Number of timestamps to fetch the CSDS.",
+    )
+
+    parser.add_argument(
+        "--stf_file",
+        action="store",
+        type=str,
+        dest="stf_file",
+        default="stf_output/snap_0031",
+        help="stf file basename.",
+    )
+
+    args = parser.parse_args()
+
+    return args
+
+
+#%% Parse the arguments
+args = parse_option()
+
+stf_file = args.stf_file
+halo_index = args.halo_index
+filename = args.csds_file
+n_files = 1  # Number of csds files
+n = args.n  # Number of timestamps to fetch the CSDS
+dt = 1e-3  # small timestep for csds start (starts at t0+dt, see below)
+
+# These are the number of timestamps we want for z > 2 (n_high_z) and z <=2 (n_low_z). This allows to provide more images outputs for 0 < z < 0 for the movie.
+n_high_z = int(n / 10)
+n_low_z = int(9 / 10 * n)
+
+print("\n-------------------------------------")
+print("Welcome to csds_anlysis.py !\n")
+
+#%% Extract particles from halo
+# Load Velociraptor data
+print("Extracting halo data...")
+
+catalogue = load(stf_file + ".properties.0")
+groups = load_groups(stf_file + ".catalog_groups.0", catalogue=catalogue)
+
+# Get the particles in halo_index
+particles, unbound_particles = groups.extract_halo(halo_index=halo_index)
+
+# Get the IDs
+IDs = particles.particle_ids
+N_particles = len(IDs)
+
+#%% Now, we can open the CSDS, get the evolution of the particles though time with their IDs
+cm_positions = np.zeros((n_files, n, 3))
+positions = np.empty((n, N_particles, 3))
+velocities = np.empty((n, N_particles, 3))
+n_particles = np.zeros((n_files, n), dtype=int)
+m_tot = np.zeros((n_files, n))
+
+file_index = 0
+
+if filename.endswith(".dump"):
+    filename = filename[:-5]
+
+print("Openening the CSDS...")
+
+#%%
+with csds.Reader(
+    filename, verbose=0, number_index=10, restart_init=False, use_cache=True
+) as reader:
+
+    print("The CSDS is opened.")
+
+    # Check the time limits (scale-factors in this example)
+    t0, t1 = reader.get_time_limits()
+
+    print("Scale factor limits: [{:e}, {:e}]".format(t0, t1))
+    print(np.log10(t0), np.log10(t1))
+    print("Redshift limits: [{:e}, {:e}]".format(get_redshift(t0), get_redshift(t1)))
+
+    # Ensure that the fields are present
+    fields = ["Coordinates", "Masses", "ParticleIDs", "Velocities"]
+    missing = set(fields).difference(
+        set(reader.get_list_fields(part_type=csds.particle_types.gas))
+    )
+
+    if missing:
+        raise Exception("Fields %s not found in the logfile." % missing)
+
+    # Create the list of IDs for *all* particle types
+    IDs_list = [None] * csds.particle_types.count
+    IDs_list[csds.particle_types.dark_matter] = IDs
+
+    # Read the particles by IDs
+    out = reader.get_data(fields=fields, time=t0, filter_by_ids=IDs_list)
+
+    # Print the missing ids
+    dm_ids, ids_found = set(IDs), set(out["ParticleIDs"])
+    diff_ids = list(dm_ids.difference(ids_found))
+    diff_found = list(ids_found.difference(dm_ids))
+    print("The following ids were not found: ", np.array(diff_ids))
+    print("The following ids are wrongly missing: ", np.array(diff_found))
+
+    # Reverse the time: it is better to start from the end and rewind to the beginning
+    high_z = np.linspace(t0 + dt, get_scale_factor(2), num=n_high_z, endpoint=False)
+    low_z = np.linspace(get_scale_factor(2), t1, num=n_low_z)
+    times = np.concatenate((high_z, low_z))[::-1]
+
+    print("Starting to move through time")
+    for i, t in enumerate(times):
+        out = reader.get_data(fields=fields, time=t, filter_by_ids=IDs_list)
+
+        # Get positions and velocities
+        positions[i] = out["Coordinates"]
+        velocities[i] = out["Velocities"]
+
+        # Compute CM of halo
+        n_particles[file_index, i] = len(out["ParticleIDs"])
+        m_tot[file_index, i] = np.sum(out["Masses"])
+        pos_cm = np.sum(out["Masses"][:, np.newaxis] * out["Coordinates"], axis=0)
+        pos_cm /= m_tot[file_index, i]
+        cm_positions[file_index, i, :] = pos_cm
+
+print("The CSDS is now closed.")
+#%% Do a movie showing the evolution of the halo (positions)
+from matplotlib.animation import FuncAnimation
+
+fig, ax = plt.subplots(num=1, figsize=(5, 5), dpi=300)
+fig.tight_layout()
+
+
+def animate(index):
+    # Read the positions in the right order
+    i = -index - 1
+    x = positions[i, :, 0]
+    y = positions[i, :, 1]
+    scale_factor = times[i]
+    redshift = 1 / scale_factor - 1
+
+    # Clear the previous data
+    ax.cla()
+    ax.text(
+        0.975,
+        0.975,
+        "z = {:.2f}".format(redshift),
+        ha="right",
+        va="top",
+        transform=ax.transAxes,
+        color="k",
+    )
+    ax.scatter(x, y, c="k", zorder=1, marker=".")
+    ax.set_aspect("equal", "box")
+    return ax
+
+
+print("Making movie...")
+animation = FuncAnimation(
+    fig, animate, range(positions.shape[0]), fargs=[], interval=n / 24
+)
+animation.save("halo_evolution.mp4")
+print("End :)")
diff --git a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/run_csds.sh b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/run_csds.sh
new file mode 100755
index 0000000000000000000000000000000000000000..30b414eab1be7eabde93d14283e180ea9dac06f6
--- /dev/null
+++ b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/run_csds.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+#Path to velociraptor executable
+velociraptor_path=/path/to/velociraptor
+
+ # Generate the initial conditions if they are not present.
+if [ ! -e small_cosmo_volume.hdf5 ]
+then
+    echo "Fetching initial conditions for the small cosmological volume example..."
+    ./getIC.sh
+fi
+
+# Run SWIFT
+../../../swift --cosmology --self-gravity --csds --threads=12 small_cosmo_volume_dm.yml 2>&1 | tee output.log
+
+if [ ! -d "stf_output" ]
+then
+    mkdir stf_output
+fi
+
+# Run velociraptor
+$velociraptor_path/stf -I 2 -C vrconfig_3dfof_subhalos_SO_hydro.cfg -i snap_0031 -o stf_output/snap_0031
+
+# Make a movie of the evolution of the DM particles
+python3 csds_analysis.py csds_index_0000.dump --halo 1
diff --git a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/small_cosmo_volume_dm.yml b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/small_cosmo_volume_dm.yml
index f469b972e5d6e93f71c51a277e8549e9b2ab9d47..135500246d868e37bd2048ed51a57530a2e097bb 100644
--- a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/small_cosmo_volume_dm.yml
+++ b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/small_cosmo_volume_dm.yml
@@ -67,3 +67,11 @@ PowerSpectrum:
   fold_factor: 2
   window_order: 2
   requested_spectra: ["matter-matter"]
+
+# Parameters governing the CSDS snapshot system
+CSDS:
+  delta_step:           10       # Update the particle log every this many updates
+  basename:             csds_index  # Common part of the filenames
+  initial_buffer_size:  0.3      # (Optional) Buffer size in GB
+  buffer_scale:	        2       # (Optional) When buffer size is too small, update it with required memory times buffer_scale
+
diff --git a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/vrconfig_3dfof_subhalos_SO_hydro.cfg b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/vrconfig_3dfof_subhalos_SO_hydro.cfg
index 8590cbf5bc77e8d7a956d210339cced4bbdc692c..8ecc372e3fac2cf377d97955aa2139b4f74ca541 100644
--- a/examples/SmallCosmoVolume/SmallCosmoVolume_DM/vrconfig_3dfof_subhalos_SO_hydro.cfg
+++ b/examples/SmallCosmoVolume/SmallCosmoVolume_DM/vrconfig_3dfof_subhalos_SO_hydro.cfg
@@ -9,9 +9,9 @@
 ################################
 HDF_name_convention=6 #HDF SWIFT naming convention
 Input_includes_dm_particle=1 #include dark matter particles in hydro input
-Input_includes_gas_particle=1 #include gas particles in hydro input
-Input_includes_star_particle=1 #include star particles in hydro input
-Input_includes_bh_particle=1 #include bh particles in hydro input
+Input_includes_gas_particle=0 #include gas particles in hydro input
+Input_includes_star_particle=0 #include star particles in hydro input
+Input_includes_bh_particle=0 #include bh particles in hydro input
 Input_includes_wind_particle=0 #include wind particles in hydro input (used by Illustris and moves particle type 0 to particle type 3 when decoupled from hydro forces). Here shown as example
 Input_includes_tracer_particle=0 #include tracer particles in hydro input (used by Illustris). Here shown as example
 Input_includes_extradm_particle=0 #include extra dm particles stored in particle type 2 and type 3, useful for zooms
diff --git a/examples/SmallCosmoVolume/SmallCosmoVolume_cooling/small_cosmo_volume.yml b/examples/SmallCosmoVolume/SmallCosmoVolume_cooling/small_cosmo_volume.yml
index b1839de14bdba8ef6ea6306eb78ec69b2f1441b4..3aa29fc285f37938949069537d4ce15d166c9624 100644
--- a/examples/SmallCosmoVolume/SmallCosmoVolume_cooling/small_cosmo_volume.yml
+++ b/examples/SmallCosmoVolume/SmallCosmoVolume_cooling/small_cosmo_volume.yml
@@ -129,6 +129,7 @@ GrackleCooling:
   thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
   self_shielding_method: -1                    # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
   self_shielding_threshold_atom_per_cm3: 0.007 # Required only with GEAR's self shielding. Density threshold of the self shielding
+  maximal_density_Hpcm3: -1                 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
 
 
 # GEAR chemistry model (Revaz and Jablonka 2018)
@@ -139,7 +140,8 @@ GEARChemistry:
   # GEAR star formation model (Revaz and Jablonka 2018)
 GEARStarFormation:
   star_formation_efficiency: 0.01   # star formation efficiency (c_*)
-  maximal_temperature:  3e4         # Upper limit to the temperature of a star forming particle
+  maximal_temperature_K:  3e4       # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3: 5        # Density threshold in Hydrogen atoms/cm3
   n_stars_per_particle: 4
   min_mass_frac: 0.5
 
diff --git a/examples/SmallCosmoVolume/SmallCosmoVolume_lightcone/small_cosmo_volume.yml b/examples/SmallCosmoVolume/SmallCosmoVolume_lightcone/small_cosmo_volume.yml
index 5bd006dc9739f2d21c580fa7182dc93929b2fb8d..31e662882e219c8b37bd2906995bde3eed91c3f2 100644
--- a/examples/SmallCosmoVolume/SmallCosmoVolume_lightcone/small_cosmo_volume.yml
+++ b/examples/SmallCosmoVolume/SmallCosmoVolume_lightcone/small_cosmo_volume.yml
@@ -69,6 +69,8 @@ FOF:
   black_hole_seed_halo_mass_Msun:  1.0e10      # Minimal halo mass in which to seed a black hole (in solar masses).
   scale_factor_first:              0.05        # Scale-factor of first FoF black hole seeding calls.
   delta_time:                      1.00751     # Scale-factor ratio between consecutive FoF black hole seeding calls.
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 Scheduler:
   max_top_level_cells: 8
diff --git a/examples/SubgridTests/ParticleSplitting/run.sh b/examples/SubgridTests/ParticleSplitting/run.sh
old mode 100644
new mode 100755
index 9ce32ccf6e42cbc10cb295a32947739f076aebac..3e2410dce5b107e2c334c5683dc0ec3ab1d86892
--- a/examples/SubgridTests/ParticleSplitting/run.sh
+++ b/examples/SubgridTests/ParticleSplitting/run.sh
@@ -1,4 +1,4 @@
-#!bash
+#!/bin/bash
 
 if [ ! -e particleSplitting.hdf5 ]
 then
diff --git a/examples/SubgridTests/StellarEvolution/stellar_evolution.yml b/examples/SubgridTests/StellarEvolution/stellar_evolution.yml
index 8fa7c81a13a4badbe322e3bd22d64529ea11c2ec..b39b886de8d9b01e2b97ab14723a5690363864ab 100644
--- a/examples/SubgridTests/StellarEvolution/stellar_evolution.yml
+++ b/examples/SubgridTests/StellarEvolution/stellar_evolution.yml
@@ -19,6 +19,8 @@ Snapshots:
   time_first:          0.                # Time of the first output (in internal units)
   delta_time:          3.e-5             # Time difference between consecutive outputs (internal units)
   compression:         4
+  recording_triggers_part: [1.0227e-4, 1.0227e-5]   # Recording starts 100M and 10M years before a snapshot
+  recording_triggers_bpart: [1.0227e-4, 1.0227e-5]  # Recording starts 100M and 10M years before a snapshot
 
 # Parameters governing the conserved quantities statistics
 Statistics:
diff --git a/examples/parameter_example.yml b/examples/parameter_example.yml
index f79bb757ccd0409a3b4321a10f1f2a09bb4f04cb..e980b021603b6632c42f95b30336c2b7b21cc75a 100644
--- a/examples/parameter_example.yml
+++ b/examples/parameter_example.yml
@@ -34,29 +34,30 @@ Cosmology:
 
 # Parameters for the hydrodynamics scheme
 SPH:
-  resolution_eta:                    1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
-  CFL_condition:                     0.1      # Courant-Friedrich-Levy condition for time integration.
-  use_mass_weighted_num_ngb:         0        # (Optional) Are we using the mass-weighted definition of the number of neighbours?
-  h_tolerance:                       1e-4     # (Optional) Relative accuracy of the Netwon-Raphson scheme for the smoothing lengths.
-  h_max:                             10.      # (Optional) Maximal allowed smoothing length in internal units. Defaults to FLT_MAX if unspecified.
-  h_min_ratio:                       0.       # (Optional) Minimal allowed smoothing length in units of the softening. Defaults to 0 if unspecified.
-  max_volume_change:                 1.4      # (Optional) Maximal allowed change of kernel volume over one time-step.
-  max_ghost_iterations:              30       # (Optional) Maximal number of iterations allowed to converge towards the smoothing length.
-  particle_splitting:                1        # (Optional) Are we splitting particles that are too massive (default: 0)
-  particle_splitting_mass_threshold: 7e-4     # (Optional) Mass threshold for particle splitting (in internal units)
-  generate_random_ids:               0        # (Optional) When creating new particles via splitting, generate ids at random (1) or use new IDs beyond the current range (0) (default: 0)
-  initial_temperature:               0        # (Optional) Initial temperature (in internal units) to set the gas particles at start-up. Value is ignored if set to 0.
-  minimal_temperature:               0        # (Optional) Minimal temperature (in internal units) allowed for the gas particles. Value is ignored if set to 0.
-  H_mass_fraction:                   0.755    # (Optional) Hydrogen mass fraction used for initial conversion from temp to internal energy. Default value is derived from the physical constants.
-  H_ionization_temperature:          1e4      # (Optional) Temperature of the transition from neutral to ionized Hydrogen for primoridal gas.
-  viscosity_alpha:                   0.8      # (Optional) Override for the initial value of the artificial viscosity. In schemes that have a fixed AV, this remains as alpha throughout the run.
-  viscosity_alpha_max:               2.0      # (Optional) Maximal value for the artificial viscosity in schemes that allow alpha to vary.
-  viscosity_alpha_min:               0.1      # (Optional) Minimal value for the artificial viscosity in schemes that allow alpha to vary.
-  viscosity_length:                  0.1      # (Optional) Decay length for the artificial viscosity in schemes that allow alpha to vary.
-  diffusion_alpha:                   0.0      # (Optional) Override the initial value for the thermal diffusion coefficient in schemes with thermal diffusion.
-  diffusion_beta:                    0.01     # (Optional) Override the decay/rise rate tuning parameter for the thermal diffusion.
-  diffusion_alpha_max:               1.0      # (Optional) Override the maximal thermal diffusion coefficient that is allowed for a given particle.
-  diffusion_alpha_min:               0.0      # (Optional) Override the minimal thermal diffusion coefficient that is allowed for a given particle.
+  resolution_eta:                      1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:                       0.1      # Courant-Friedrich-Levy condition for time integration.
+  use_mass_weighted_num_ngb:           0        # (Optional) Are we using the mass-weighted definition of the number of neighbours?
+  h_tolerance:                         1e-4     # (Optional) Relative accuracy of the Netwon-Raphson scheme for the smoothing lengths.
+  h_max:                               10.      # (Optional) Maximal allowed smoothing length in internal units. Defaults to FLT_MAX if unspecified.
+  h_min_ratio:                         0.       # (Optional) Minimal allowed smoothing length in units of the softening. Defaults to 0 if unspecified.
+  max_volume_change:                   1.4      # (Optional) Maximal allowed change of kernel volume over one time-step.
+  max_ghost_iterations:                30       # (Optional) Maximal number of iterations allowed to converge towards the smoothing length.
+  particle_splitting:                  1        # (Optional) Are we splitting particles that are too massive (default: 0)
+  particle_splitting_mass_threshold:   7e-4     # (Optional) Mass threshold for particle splitting (in internal units)
+  particle_splitting_log_extra_splits: 0        # (Optional) Are we logging the splits beyond the maximal allowed into files? (default: 0)
+  generate_random_ids:                 0        # (Optional) When creating new particles via splitting, generate ids at random (1) or use new IDs beyond the current range (0) (default: 0)
+  initial_temperature:                 0        # (Optional) Initial temperature (in internal units) to set the gas particles at start-up. Value is ignored if set to 0.
+  minimal_temperature:                 0        # (Optional) Minimal temperature (in internal units) allowed for the gas particles. Value is ignored if set to 0.
+  H_mass_fraction:                     0.755    # (Optional) Hydrogen mass fraction used for initial conversion from temp to internal energy. Default value is derived from the physical constants.
+  H_ionization_temperature:            1e4      # (Optional) Temperature of the transition from neutral to ionized Hydrogen for primoridal gas.
+  viscosity_alpha:                     0.8      # (Optional) Override for the initial value of the artificial viscosity. In schemes that have a fixed AV, this remains as alpha throughout the run.
+  viscosity_alpha_max:                 2.0      # (Optional) Maximal value for the artificial viscosity in schemes that allow alpha to vary.
+  viscosity_alpha_min:                 0.1      # (Optional) Minimal value for the artificial viscosity in schemes that allow alpha to vary.
+  viscosity_length:                    0.1      # (Optional) Decay length for the artificial viscosity in schemes that allow alpha to vary.
+  diffusion_alpha:                     0.0      # (Optional) Override the initial value for the thermal diffusion coefficient in schemes with thermal diffusion.
+  diffusion_beta:                      0.01     # (Optional) Override the decay/rise rate tuning parameter for the thermal diffusion.
+  diffusion_alpha_max:                 1.0      # (Optional) Override the maximal thermal diffusion coefficient that is allowed for a given particle.
+  diffusion_alpha_min:                 0.0      # (Optional) Override the minimal thermal diffusion coefficient that is allowed for a given particle.
 
 # Parameters of the stars
 Stars:
@@ -94,6 +95,8 @@ Gravity:
   comoving_nu_softening:         0.0026994 # Comoving Plummer-equivalent softening length for neutrino particles (in internal units).
   max_physical_nu_softening:     0.0007    # Maximal Plummer-equivalent softening length in physical coordinates for neutrino particles (in internal units).
   softening_ratio_background:    0.04      # Fraction of the mean inter-particle separation to use as Plummer-equivalent softening for the background DM particles.
+  max_adaptive_softening:        FLT_MAX   # (Optional) Maximal Plummer-equivalent co-moving adaptive softening (in internal units).
+  min_adaptive_softening:        0.        # (Optional) Minimal Plummer-equivalent co-moving adaptive softening (in internal units).
   rebuild_frequency:             0.01      # (Optional) Frequency of the gravity-tree rebuild in units of the number of g-particles (this is the default value).
   rebuild_active_fraction:       1.01      # (Optional) Fraction of active gravity particles needed to trigger a gravity-tree rebuild (not triggered by this if > 1, which is the default value).
   a_smooth:                      1.25      # (Optional) Smoothing scale in top-level cell sizes to smooth the long-range forces over (this is the default value).
@@ -121,6 +124,8 @@ FOF:
   group_id_offset:                 1           # (Optional) Sets the offset of group ID labeling. Defaults to 1 if unspecified.
   output_list_on:                  0           # (Optional) Enable the output list
   output_list:       ./output_list_fof.txt     # (Optional) File containing the output times (see documentation in "Parameter File" section)
+  linking_types:   [0, 1, 0, 0, 0, 0, 0]       # Use DM as the primary FOF linking type
+  attaching_types: [1, 0, 0, 0, 1, 1, 0]       # Use gas, stars and black holes as FOF attachable types
 
 # Parameters for the task scheduling
 Scheduler:
@@ -133,6 +138,7 @@ Scheduler:
   cell_sub_size_pair_grav:   256000000 # (Optional) Maximal number of interactions per sub-pair gravity task  (this is the default value).
   cell_sub_size_self_grav:   32000     # (Optional) Maximal number of interactions per sub-self gravity task  (this is the default value).
   cell_split_size:           400       # (Optional) Maximal number of particles per cell (this is the default value).
+  grid_split_threshold:      400       # (Optional) Maximal number of particles per cell at construction level of Voronoi grid (this is the default value).
   cell_subdepth_diff_grav:   4         # (Optional) Maximal depth difference between leaves and a cell that gravity tasks can be pushed down to (this is the default value).
   cell_extra_parts:          0         # (Optional) Number of spare parts per top-level allocated at rebuild time for on-the-fly creation.
   cell_extra_gparts:         0         # (Optional) Number of spare gparts per top-level allocated at rebuild time for on-the-fly creation.
@@ -151,6 +157,7 @@ Scheduler:
   task_level_output_frequency:      0  # (Optional) Dumping frequency of the task level data. By default, writes only at the first step.
   free_foreign_during_restart:      0  # (Optional) Should the code free the foreign data when dumping restart files in order to get breathing space?
   free_foreign_during_rebuild:      0  # (Optional) Should the code free the foreign data when calling a rebuld in order to get breathing space?
+  deadlock_waiting_time_s:          0. # (Optional) If runners didn't fetch a new task from a queue after this many seconds, assume swift deadlocked and abort. Non-positive values turn the detector off. Needs --enable-debugging-checks and MPI to take effect.
 
 # Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
 TimeIntegration:
@@ -174,7 +181,9 @@ Snapshots:
   invoke_ps:  0           # (Optional) Call a power-spectrum calculation every time a snapshot is written
   compression: 0          # (Optional) Set the level of GZIP compression of the HDF5 datasets [0-9]. 0 does no compression. The lossless compression is applied to *all* the fields.
   distributed: 0          # (Optional) When running over MPI, should each rank write a partial snapshot or do we want a single file? 1 implies one file per MPI rank.
-  lustre_OST_count:  0    # (Optional) If > 0, the number of lustre OSTs to distribure the single-striped files over. Has no effect on non-Lustre filesystems. Has an effect only on distributed snapshots.
+  lustre_OST_checks: 0    # (Optional) Perform OST selection checks
+  lustre_OST_free: 0      # (Optional) OST free space requirement, -1 for guess.
+  lustre_OST_test: 0      # (Optional) Check that OSTs are writable.
   use_delta_from_edge: 0  # (Optional) Should particles close to the box edge be moved back towards 0 by a vector perpendicular to the box edge? This is useful in cases where lossy compression moves particle beyond the edge.
   delta_from_edge:     0. # (Optional) Norm of the vector to use when moving particles away from the edge
   UnitMass_in_cgs:     1  # (Optional) Unit system for the outputs (Grams)
@@ -201,13 +210,14 @@ CSDS:
 
 # Parameters governing the conserved quantities statistics
 Statistics:
-  delta_time:           1e-2        # Time between statistics output
-  scale_factor_first:     0.1       # (Optional) Scale-factor of the first statistics dump if cosmological time-integration.
-  time_first:             0.        # (Optional) Time of the first stats output if non-cosmological time-integration (in internal units)
-  energy_file_name:    statistics   # (Optional) File name for statistics output
-  timestep_file_name:  timesteps    # (Optional) File name for timing information output. Note: No underscores "_" allowed in file name
-  output_list_on:      0   	    # (Optional) Enable the output list
-  output_list:         statlist.txt # (Optional) File containing the output times (see documentation in "Parameter File" section)
+  delta_time:           1e-2           # Time between statistics output
+  scale_factor_first:     0.1          # (Optional) Scale-factor of the first statistics dump if cosmological time-integration.
+  time_first:             0.           # (Optional) Time of the first stats output if non-cosmological time-integration (in internal units)
+  energy_file_name:    statistics      # (Optional) File name for statistics output
+  timestep_file_name:  timesteps       # (Optional) File name for timing information output. Note: No underscores "_" allowed in file name
+  rt_subcycles_file_name: rtsubcycles  # (Optional) File name for RT subcycles information output. Note: No underscores "_" allowed in file name. Has no effect if not compiled with RT enabled.
+  output_list_on:      0   	       # (Optional) Enable the output list
+  output_list:         statlist.txt    # (Optional) File containing the output times (see documentation in "Parameter File" section)
 
 # Parameters related to the initial conditions
 InitialConditions:
@@ -237,7 +247,9 @@ Restarts:
   max_run_time:       24.0       # (optional) Maximal wall-clock time in hours. The application will exit when this limit is reached.
   resubmit_on_exit:   0          # (Optional) whether to run a command when exiting after the time limit has been reached.
   resubmit_command:   ./resub.sh # (Optional) Command to run when time limit is reached. Compulsory if resubmit_on_exit is switched on. Note potentially unsafe.
-  lustre_OST_count:  0           # (Optional) If > 0, the number of lustre OSTs to distribure the single-striped restart files over. Has no effect on non-Lustre filesystems.
+  lustre_OST_checks: 0           # (Optional) Perform OST selection checks
+  lustre_OST_free: 0             # (Optional) OST free space requirement, -1 for guess.
+  lustre_OST_test: 0             # (Optional) Check that OSTs are writable.
 
 # Parameters governing domain decomposition
 DomainDecomposition:
@@ -294,12 +306,25 @@ LineOfSight:
 
 EoS:
   isothermal_internal_energy: 20.26784      # Thermal energy per unit mass for the case of isothermal equation of state (in internal units).
+  barotropic_vacuum_sound_speed: 2e4        # Vacuum sound speed (in internal units)
+  barotropic_core_density:       1e-13      # Core density (in internal units)
   # Select which planetary EoS material(s) to enable for use.
   planetary_use_idg_def:    0               # Default ideal gas, material ID 0
   planetary_use_Til_iron:       1           # Tillotson iron, material ID 100
   planetary_use_Til_granite:    1           # Tillotson granite, material ID 101
   planetary_use_Til_water:      0           # Tillotson water, material ID 102
   planetary_use_Til_basalt:     0           # Tillotson basalt, material ID 103
+  planetary_use_Til_ice:        0           # Tillotson ice, material ID 104
+  planetary_use_Til_custom_0:   0           # Tillotson generic user-provided parameters, material IDs 19[0-9]
+  planetary_use_Til_custom_1:   0
+  planetary_use_Til_custom_2:   0
+  planetary_use_Til_custom_3:   0
+  planetary_use_Til_custom_4:   0
+  planetary_use_Til_custom_5:   0
+  planetary_use_Til_custom_6:   0
+  planetary_use_Til_custom_7:   0
+  planetary_use_Til_custom_8:   0
+  planetary_use_Til_custom_9:   0
   planetary_use_HM80_HHe:   0               # Hubbard & MacFarlane (1980) hydrogen-helium atmosphere, material ID 200
   planetary_use_HM80_ice:   0               # Hubbard & MacFarlane (1980) H20-CH4-NH3 ice mix, material ID 201
   planetary_use_HM80_rock:  0               # Hubbard & MacFarlane (1980) SiO2-MgO-FeS-FeO rock mix, material ID 202
@@ -307,6 +332,10 @@ EoS:
   planetary_use_SESAME_basalt:  0           # SESAME basalt 7530, material ID 301
   planetary_use_SESAME_water:   0           # SESAME water 7154, material ID 302
   planetary_use_SS08_water:     0           # Senft & Stewart (2008) SESAME-like water, material ID 303
+  planetary_use_AQUA:           0           # Haldemann, J. et al. (2020) water, material ID 304
+  planetary_use_CMS19_H:        0           # Chabrier, G. et al. (2019) Hydrogen, material ID 305
+  planetary_use_CMS19_He:       0           # Chabrier, G. et al. (2019) Hydrogen, material ID 306
+  planetary_use_CD21_HHe:       0           # Chabrier & Debras (2021) H/He mixture Y=0.245 (Jupiter), material ID 307
   planetary_use_ANEOS_forsterite:   0       # ANEOS forsterite (Stewart et al. 2019), material ID 400
   planetary_use_ANEOS_iron:         0       # ANEOS iron (Stewart 2020), material ID 401
   planetary_use_ANEOS_Fe85Si15:     0       # ANEOS Fe85Si15 (Stewart 2020), material ID 402
@@ -321,6 +350,16 @@ EoS:
   planetary_use_custom_8:   0
   planetary_use_custom_9:   0
   # Tablulated EoS file paths.
+  planetary_Til_custom_0_param_file:    ./EoSTables/Til_custom_0.txt
+  planetary_Til_custom_1_param_file:    ./EoSTables/Til_custom_1.txt
+  planetary_Til_custom_2_param_file:    ./EoSTables/Til_custom_2.txt
+  planetary_Til_custom_3_param_file:    ./EoSTables/Til_custom_3.txt
+  planetary_Til_custom_4_param_file:    ./EoSTables/Til_custom_4.txt
+  planetary_Til_custom_5_param_file:    ./EoSTables/Til_custom_5.txt
+  planetary_Til_custom_6_param_file:    ./EoSTables/Til_custom_6.txt
+  planetary_Til_custom_7_param_file:    ./EoSTables/Til_custom_7.txt
+  planetary_Til_custom_8_param_file:    ./EoSTables/Til_custom_8.txt
+  planetary_Til_custom_9_param_file:    ./EoSTables/Til_custom_9.txt
   planetary_HM80_HHe_table_file:    ./EoSTables/HM80_HHe.txt
   planetary_HM80_ice_table_file:    ./EoSTables/HM80_ice.txt
   planetary_HM80_rock_table_file:   ./EoSTables/HM80_rock.txt
@@ -419,7 +458,60 @@ SineWavePotential:
   timestep_limit:   1.      # Time-step dimensionless pre-factor.
   growth_time:      0.      # (Optional) Time for the potential to grow to its final size.
 
-
+# MWPotential2014 potential
+MWPotential2014Potential:
+  useabspos:        0          # 0 -> positions based on centre, 1 -> absolute positions
+  position:         [0.,0.,0.] # Location of centre of potential with respect to centre of the box (if 0) otherwise absolute (if 1) (internal units)
+  timestep_mult:    0.005      # Dimensionless pre-factor for the time-step condition, basically determines the fraction of the orbital time we use to do the time integration
+  epsilon:          0.001      # Softening size (internal units)
+  concentration:    9.823403437774843      # concentration of the Halo
+  M_200_Msun:       147.41031542774076e10  # M200 of the galaxy disk (in M_sun)
+  H:                1.2778254614201471     # Hubble constant in units of km/s/Mpc
+  Mdisk_Msun:       6.8e10                 # Mass of the disk (in M_sun)
+  Rdisk_kpc:        3.0                    # Effective radius of the disk (in kpc)
+  Zdisk_kpc:        0.280                  # Scale-height of the disk (in kpc)
+  amplitude_Msun_per_kpc3: 1.0e10          # Amplitude of the bulge (in M_sun/kpc^3)
+  r_1_kpc:          1.0                    # Reference radius for amplitude of the bulge (in kpc)
+  alpha:            1.8                    # Exponent of the power law of the bulge
+  r_c_kpc:          1.9                    # Cut-off radius of the bulge (in kpc)
+  potential_factors: [0.4367419745056084, 1.002641971008805, 0.022264787598364262] # Coefficients that adjust the strength of the halo (1st component), the disk (2nd component) and the bulge (3rd component)
+  with_dynamical_friction: 0               # Are we running with dynamical friction ? 0 -> no, 1 -> yes
+  df_lnLambda: 5.0                         # Coulomb logarithm
+  df_sigma_floor_km_p_s : 10.0             # Minimum velocity dispersion for the velocity dispersion model
+  df_satellite_mass_in_Msun : 1.0e10       # Satellite mass in solar mass
+  df_core_radius_in_kpc: 10                # Radius below which the dynamical friction vanishes.
+  df_polyfit_coeffs00: -2.96536595e-31     # Polynomial fit coefficient for the velocity dispersion model (order 16)
+  df_polyfit_coeffs01:  8.88944631e-28     # Polynomial fit coefficient for the velocity dispersion model (order 15)
+  df_polyfit_coeffs02: -1.18280578e-24     # Polynomial fit coefficient for the velocity dispersion model (order 14)
+  df_polyfit_coeffs03:  9.29479457e-22     # Polynomial fit coefficient for the velocity dispersion model (order 13)
+  df_polyfit_coeffs04: -4.82805265e-19     # Polynomial fit coefficient for the velocity dispersion model (order 12)
+  df_polyfit_coeffs05:  1.75460211e-16     # Polynomial fit coefficient for the velocity dispersion model (order 11)
+  df_polyfit_coeffs06: -4.59976540e-14     # Polynomial fit coefficient for the velocity dispersion model (order 10)
+  df_polyfit_coeffs07:  8.83166045e-12     # Polynomial fit coefficient for the velocity dispersion model (order 9)
+  df_polyfit_coeffs08: -1.24747700e-09     # Polynomial fit coefficient for the velocity dispersion model (order 8)
+  df_polyfit_coeffs09:  1.29060404e-07     # Polynomial fit coefficient for the velocity dispersion model (order 7)
+  df_polyfit_coeffs10: -9.65315026e-06     # Polynomial fit coefficient for the velocity dispersion model (order 6)
+  df_polyfit_coeffs11:  5.10187806e-04     # Polynomial fit coefficient for the velocity dispersion model (order 5)
+  df_polyfit_coeffs12: -1.83800281e-02     # Polynomial fit coefficient for the velocity dispersion model (order 4)
+  df_polyfit_coeffs13:  4.26501444e-01     # Polynomial fit coefficient for the velocity dispersion model (order 3)
+  df_polyfit_coeffs14: -5.78038064e+00     # Polynomial fit coefficient for the velocity dispersion model (order 2)
+  df_polyfit_coeffs15:  3.57956721e+01     # Polynomial fit coefficient for the velocity dispersion model (order 1)
+  df_polyfit_coeffs16:  1.85478908e+02     # Polynomial fit coefficient for the velocity dispersion model (order 0)
+  df_timestep_mult : 0.1                   # Dimensionless pre-factor for the time-step condition for the dynamical friction force
+
+# Parameters related to forcing terms     ----------------------------------------------
+
+# Roberts' flow (Tilgner & Brandenburg, 2008, MNRAS, 391, 1477) where the velocity is imposed
+RobertsFlowForcing:
+  u0:              1.5      # Forcing velocity (internal units)
+  Vz_factor:       1.0      # (optional) Scaling of the velocity along the z axis
+
+# Roberts' flow (Tilgner & Brandenburg, 2008, MNRAS, 391, 1477) where the forcing is an acceleration
+RobertsFlowAccelerationForcing:
+  u0:              1.5      # Forcing velocity (internal units)
+  nu:              1.0      # Viscosity (internal units) to convert velocities to accelerations.
+  Vz_factor:       1.0      # (optional) Scaling of the velocity along the z axis
+  
 # Parameters related to entropy floors    ----------------------------------------------
 
 EAGLEEntropyFloor:
@@ -503,6 +595,34 @@ GrackleCooling:
   thermal_time_myr: 5                          # (optional) Time (in Myr) for adiabatic cooling after a feedback event.
   self_shielding_method: -1                    # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR)
   self_shielding_threshold_atom_per_cm3: 0.007 # Required only with GEAR's self shielding. Density threshold of the self shielding
+  maximal_density_Hpcm3:   1e4                 # Maximal density (in hydrogen atoms/cm^3) for cooling. Higher densities are floored to this value to ensure grackle works properly when interpolating beyond the cloudy_table maximal density. A value < 0 deactivates this parameter.
+  HydrogenFractionByMass : 1.                  # Hydrogen fraction by mass (default is 0.76)
+  use_radiative_transfer : 1                   # Arrays of ionization and heating rates are provided
+  RT_heating_rate_cgs    : 0                   # heating         rate in units of / nHI_cgs 
+  RT_HI_ionization_rate_cgs  : 0               # HI ionization   rate in cgs [1/s]
+  RT_HeI_ionization_rate_cgs : 0               # HeI ionization  rate in cgs [1/s]
+  RT_HeII_ionization_rate_cgs: 0               # HeII ionization rate in cgs [1/s]
+  RT_H2_dissociation_rate_cgs: 0               # H2 dissociation rate in cgs [1/s]
+  volumetric_heating_rates_cgs: 0              # Volumetric heating rate in cgs  [erg/s/cm3]
+  specific_heating_rates_cgs: 0                # Specific heating rate in cgs    [erg/s/g]
+  H2_three_body_rate : 1                       # Specific the H2 formation three body rate (0->5,see Grackle documentation)
+  H2_cie_cooling : 0                           # Enable/disable H2 collision-induced emission cooling from Ripamonti & Abel (2004)
+  H2_on_dust: 0                                # Flag to enable H2 formation on dust grains
+  local_dust_to_gas_ratio : -1                 # The ratio of total dust mass to gas mass in the local Universe (-1 to use the Grackle default value). 
+  cmb_temperature_floor : 1                    # Enable/disable an effective CMB temperature floor
+  initial_nHII_to_nH_ratio:    -1              # initial nHII   to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHeI_to_nH_ratio:    -1              # initial nHeI   to nH ratio (number density ratio). Value is ignored if set to -1.  
+  initial_nHeII_to_nH_ratio:   -1              # initial nHeII  to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHeIII_to_nH_ratio:  -1              # initial nHeIII to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nDI_to_nH_ratio:     -1              # initial nDI    to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nDII_to_nH_ratio:    -1              # initial nDII   to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHM_to_nH_ratio:     -1              # initial nHM    to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nH2I_to_nH_ratio:    -1              # initial nH2I   to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nH2II_to_nH_ratio:   -1              # initial nH2II  to nH ratio (number density ratio). Value is ignored if set to -1.
+  initial_nHDI_to_nH_ratio:    -1              # initial nHDI   to nH ratio (number density ratio). Value is ignored if set to -1.
+
+
+  
 
 
 # Parameters related to chemistry models  -----------------------------------------------
@@ -522,7 +642,7 @@ EAGLEChemistry:
 
 # GEAR chemistry model (Revaz and Jablonka 2018)
 GEARChemistry:
-  initial_metallicity: 1         # Initial metallicity of the gas (mass fraction)
+  initial_metallicity: 1         # Initial metallicity of the gas (mass fraction). If < 0, then read the metallicities from the initial conditions. This applies to all particles.
   scale_initial_metallicity: 1   # Should we scale the initial metallicity with the solar one?
   diffusion_coefficient: 1e-3    # Coefficient for the diffusion (see Shen et al. 2010; differs by gamma^2 due to h^2).
 
@@ -556,7 +676,8 @@ EAGLEStarFormation:
   threshold_temperature1_K:          1000      # When using subgrid-based SF threshold, subgrid temperature below which gas is star-forming.
   threshold_temperature2_K:          31622     # When using subgrid-based SF threshold, subgrid temperature below which gas is star-forming if also above the density limit.
   threshold_number_density_H_p_cm3:  10        # When using subgrid-based SF threshold, subgrid number density above which gas is star-forming if also below the second temperature limit.
-
+  num_of_stars_per_gas_particle:     1         # (Optional) The number star particles to form per gas particle converted to stars. (Defaults to 1. Must be > 0)
+  
 # Quick Lyman-alpha star formation parameters
 QLAStarFormation:
   over_density:              1000      # The over-density above which gas particles turn into stars.
@@ -565,10 +686,10 @@ QLAStarFormation:
 GEARStarFormation:
   star_formation_mode: default      # default (use the pressure floor limit) or agora (do not use the pressure floor limit)
   star_formation_efficiency: 0.01   # star formation efficiency (c_*)
-  maximal_temperature:  3e4         # Upper limit to the temperature of a star forming particle
+  maximal_temperature_K:     3e4    # Upper limit to the temperature of a star forming particle
+  density_threshold_Hpcm3:   10     # Density threshold (Hydrogen atoms/cm^3) for star formation
   n_stars_per_particle: 4           # Number of stars that an hydro particle can generate
   min_mass_frac: 0.5                # Minimal mass for a stellar particle as a fraction of the average mass for the stellar particles.
-  density_threshold: 1.67e-23       # Density threshold for star formation
 
 # Parameters related to feedback models  -----------------------------------------------
 
@@ -624,11 +745,14 @@ EAGLEFeedback:
 # GEAR feedback model
 GEARFeedback:
   supernovae_energy_erg: 0.1e51                            # Energy released by a single supernovae.
+  supernovae_efficiency: 0.1                               # Supernovae energy efficiency, used for both SNIa and SNII. The energy released effectively is E_sn = supernovae_efficiency*E_sn
   yields_table: chemistry-AGB+OMgSFeZnSrYBaEu-16072013.h5  # Table containing the yields.
   yields_table_first_stars: chemistry-PopIII.hdf5          # Table containing the yields of the first stars.
   metallicity_max_first_stars: -1                          # Maximal metallicity (in mass fraction) for a first star (-1 to deactivate).
+  imf_transition_metallicity: -5                           # Maximal metallicity ([Fe/H]) for a first star (0 to deactivate).
   discrete_yields: 0                                       # Should we use discrete yields or the IMF integrated one?
   elements: [Fe, Mg, O, S, Zn, Sr, Y, Ba, Eu]              # Elements to read in the yields table. The number of element should be one less than the number of elements (N) requested during the configuration (--with-chemistry=GEAR_N).
+  discrete_star_minimal_gravity_mass_Msun: 0.1             # Minimal gravity mass after a discrete star completely explodes. In M_sun. (Default: 0.1)
 
 # AGORA feedback model
 AGORAFeedback:
@@ -722,7 +846,11 @@ SPINJETAGN:
   use_nibbling:                       1          # Continuously transfer small amounts of mass from all gas neighbours to a black hole [1] or stochastically swallow whole gas particles [0]? 
   min_gas_mass_for_nibbling_Msun:     9e5        # Minimum mass for a gas particle to be nibbled from [M_Sun]. Only used if use_nibbling is 1.
   coupling_efficiency:                0.15       # Fraction of the radiated energy that couples to the gas in feedback events.
-  AGN_delta_T_K:                      3.16228e8  # Change in temperature to apply to the gas particle in an AGN feedback event in Kelvin.
+  AGN_heating_temperature_model:      Constant   # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported.
+  AGN_delta_T_K:                      3.16228e8  # Change in temperature to apply to the gas particle in an AGN feedback event in Kelvin, if 'AGN_heating_temperature_model' is 'Constant'.
+  delta_T_xi:                         1.         # The numerical multiplier by which the heating temperature formula is scaled, if 'AGN_heating_temperature_model' is 'Local'. If a value of 1 is used, the formulas are used as derived, i.e. they are not rescaled.
+  delta_T_min_K:                      1e8        # The minimal heating temperature in Kelvin. This is used if 'AGN_heating_temperature_model' is 'Local'.
+  delta_T_max_K:                      1e11       # The maximal heating temperature in Kelvin. This is used if 'AGN_heating_temperature_model' is 'Local'.
   AGN_num_ngb_to_heat:                1.         # Target number of gas neighbours to heat in an AGN feedback event.
   with_potential_correction:          1          # Subtract BH's own contribution to the potential of neighbours when determining repositioning targets.
   max_reposition_mass_Msun:           2e8        # Maximal BH mass considered for BH repositioning in solar masses.
@@ -742,35 +870,36 @@ SPINJETAGN:
   minimum_timestep_yr:                1000.0     # Minimum time-step of black-hole particles
   include_jets:                       1          # Global switch whether to include jet feedback [1] or not [0].
   turn_off_radiative_feedback:        0          # Global switch whether to turn off radiative (thermal) feedback [1] or not [0]. This should only be used if 'include_jets' is set to 1, since we want feedback in some form or another.
-  alpha_acc:                          0.1        # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between the thin and thick disk, as dot(m) = 0.2 * alpha^2.
-  seed_spin:                          0.01        # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
-  AGN_jet_velocity_model:             BlackHoleMass          # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
-  v_jet_km_p_s:                       10000.     # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
-  v_jet_cs_ratio:                     10.        # This sets the jet velocity to v_jet_cs_ratio times the sound speed of the hot gas of the parent halo the black hole is in. This is used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
-  v_jet_BH_mass_scaling_reference_mass_Msun: 3.4e3 # The reference mass used in the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
-  v_jet_BH_mass_scaling_slope:        0.65       # The slope of the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
-  v_jet_mass_loading:                 400.       # The constant mass loading to use if 'AGN_jet_velocity_model' is MassLoading.
-  v_jet_xi:                           0.707       # The numerical multiplier by which the jet velocity formula is scaled, if 'AGN_jet_velocity_model' is 'Local' or 'SoundSpeed'. The appropriate values (to exactly obtain the formulas as derived) are 0.63 and 0.707 for the two, respectively.
-  v_jet_min_km_p_s:                   500        # The minimal jet velocity. This is used if  'AGN_jet_velocity_model' is 'BlackHoleMass', 'MassLoading', 'Local' or 'SoundSpeed'.
+  alpha_acc:                          0.2        # Viscous alpha of the subgrid accretion disks. Likely to be within the 0.1-0.3 range. The main effect is that it sets the transition accretion rate between the thin and thick disk, as dot(m) = 0.2 * alpha^2.
+  mdot_crit_ADAF:                     0.01       # The transition normalized accretion rate (Eddington ratio) at which the disc goes from thick (low accretion rates) to thin (high accretion rates). The feedback also changes from kinetic jets to thermal isotropic, respectively.
+  seed_spin:                          0.01       # The (randomly-directed) black hole spin assigned to BHs when they are seeded. Should be strictly between 0 and 1.
+  AGN_jet_velocity_model:             Constant   # How AGN jet velocities are calculated. If 'Constant', a single value is used. If 'BlackHoleMass', then an empirical relation between halo mass and black hole mass is used to calculate jet velocities. 'HaloMass' is currently not supported. 
+  v_jet_km_p_s:                       3160.      # Jet velocity to use if 'AGN_jet_velocity_model' is 'Constant'. Units are km/s.
+  v_jet_BH_mass_scaling_reference_mass_Msun: 1e9 # The reference mass used in the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
+  v_jet_BH_mass_scaling_slope:        0.5        # The slope of the relation between halo mass and BH mass used to calculate jet velocities. Only used if 'AGN_jet_velocity_model' is 'BlackHoleMass'.
+  v_jet_min_km_p_s:                   500        # The minimal jet velocity. This is used if 'AGN_jet_velocity_model' is 'BlackHoleMass', 'MassLoading' or 'Local'.
+  v_jet_max_km_p_s:                   1e4        # The maximal jet velocity. This is used if 'AGN_jet_velocity_model' is 'BlackHoleMass', 'MassLoading' or 'Local'.
   opening_angle_in_degrees:           7.5        # The half-opening angle of the jet in degrees. Should use values < 15 unless for tests.
   N_jet:                              2          # Target number of particles to kick as part of a single jet feedback event. Should be a multiple of 2 to ensure approximate momentum conservation (we always kick particles in pairs, one from each 'side' of the BH, relative to the spin vector).
-  AGN_jet_feedback_model:             MinimumDistance   # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
+  AGN_jet_feedback_model:             MinimumDistance # Which particles to kick from the black hole smoothing kernels. Should be 'SpinAxis', 'MinimumDistance', 'MaximumDistance' or 'MinimumDensity'
   eps_f_jet:                          1.         # Coupling efficiency for jet feedback. No reason to expect this to be less than 1.
-  fix_jet_efficiency:                 0          # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0]. If used, jets will be launched exclusively along the z axis. Should be set to 1 only for tests.
+  fix_jet_efficiency:                 0          # Global switch whether to fix jet efficiency to a particular value [1], or use a spin-dependant formula [0].
   jet_efficiency:                     0.1        # The constant jet efficiency used if 'fix_jet_efficiency' is set to 1.
+  fix_jet_direction:                  0          # Global switch whether to fix the jet direction to be along the z-axis, instead of along the spin vector.
+  accretion_efficiency_mode:          Variable   # How the accretion efficiencies are calculated for the thick accretion disc. If 'Constant', the value of 'accretion_efficiency_thick' will be used. If 'Variable', the accretion efficiency will scale with Eddington ratio.
+  accretion_efficiency_thick:         0.01       # The accretion efficiency (suppression factor of the accretion rate) to use in the thick disc (ADAF), to represent the effects of subgrid ADIOS winds that take away most of the mass flowing through the accretion disc.
+  accretion_efficiency_slim:          1          # The constant accretion efficiency to use in the slim disc, at super-Eddington rates.
+  ADIOS_s:                            0.5        # The exponent of the scaling between accretion efficiency and transition radius of the accretion disc, used if 'accretion_efficiency_mode' is 'Variable'.
+  ADIOS_R_in:                         1e4        # The normalisation (the value) of the transition radius of the accretion disc at the critical Eddington ratio (0.01), used if 'accretion_efficiency_mode' is 'Variable'.
   fix_radiative_efficiency:           0          # Global switch whether to fix the radiative efficiency to a particular value [1], or use a spin-dependant formula [0]. 
   radiative_efficiency:               0.1        # The constant jet efficiency used if 'fix_radiative_efficiency' is set to 1. Otherwise, this value is used to define the Eddington accretion rate.
   TD_region:                          B          # How to treat the subgrid accretion disk if it is thin, according to the Shakura & Sunyaev (1973) model. If set to B, region b will be used. If set to C, region c will be used.
   include_GRMHD_spindown:             1          # Whether to include high jet spindown rates from GRMHD simulations [1], or use an analytical formula that assumes extraction of energy from the rotational mass/energy of the BH.
-  include_ADIOS_suppression:          0          # Whether to suppress the accretion rate in the fully thick disc regime [1] (Eddington rate below 0.2alpha^2) by the amount expected to be taken away by isotropic kinetic disk winds.
-  ADIOS_R_in:                         30.        # If include_ADIOS_accr_suppression is set to 1, this parameter controls the inner radius within which winds are not important.
-  ADIOS_s:                            0.4        # Slope of the accretion rate - radius relationship if include_ADIOS_accr_suppression is set to 1.
-  turn_off_secondary_feedback:        1          # If set to 1, there will be only radiative (thermal) feedback in the thin disk mode, and only jets in the thick disk mode.
-  jet_h_r_slope:                      1.         # The slope of the dependence of jet efficiency on aspect ratio of the subgrid accretion disk, H/R. Default value is 1, and another reasonable value is 0 (same jet efficiency for all disks). Reality could be anything in between. This parameter is only used if turn_off_secondary_feedback is set to 0.
   delta_ADAF:                         0.2        # Electron heating parameter, which controls the strength of radiative feedback in thick disks. Should be between 0.1 and 0.5. This parameter is only used if turn_off_secondary_feedback is set to 0.
-  include_slim_disk:                  0          # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
-  TD_SD_eps_r_threshold:              0.75       # Parameter controlling the transition from thin to slim disk. Accretion disk will be slim if radiative efficiency satisfies eps_slim < TD_SD_eps_r_threshold * eps_thin. This parameter is only used if include_slim_disk is set to 1.
-
+  include_slim_disk:                  1          # Global switch whether to include super-Eddington accretion, modeled as the slim disk. If set to 0, disks will be considered thin even at very large accretion rates.
+  use_jets_in_thin_disc:              1          # Whether to use jets alongside radiation in the thin disc at moderate Eddington ratios.
+  use_ADIOS_winds:                    1          # Whether to include ADIOS winds in the thick disc as thermal isotropic feedback (same channel as thin disc quasar feedback, but with a different efficiency). 
+  slim_disc_wind_factor:              1          # The relative efficiency of slim disc winds at super-Eddington rates. If '1', full winds will be used, while '0' will lead to no winds. Any value in between those can also be used. The wind is implemented in the thermal isotropic feedback channel.
   
 # Parameters related to the neutrinos --------------------------------------------
 Neutrino:
@@ -799,9 +928,36 @@ XrayEmissivity:
 DefaultSink:
   cut_off_radius:        1e-3       # Cut off radius of the sink particles (in internal units). This parameter should be adapted with the resolution.
 
+BasicSink:
+  use_nibbling: 1                             # Use nibbling to accrete gas? If zero, do swallowing instead.
+  min_gas_mass_for_nibbling_Msun: 5e4         # The mass in Msun that particles cannot be nibbled below. A good default is half the initial mass.
+
 # GEAR sink particles
 GEARSink:
-  cut_off_radius:        1e-3       # Cut off radius of the sink particles (in internal units). This parameter should be adapted with the resolution.
+  cut_off_radius:        1e-3                 # Cut off radius of the sink particles (in internal units). In GEAR, if this is specified, the cutoff radius is fixed, and the sink smoothing length is fixed at this value divided by kernel_gamma. If not, the cutoff radius varies with the sink smoothing length as r_cut = h*kernel_gamma.
+  f_acc: 0.1                                  # (Optional) Fraction of the cut_off_radius that determines if a gas particle should be swallowed wihtout additional check. It has to respect 0 <= f_acc <= 1. (Default: 0.1)
+  temperature_threshold_K:        100         # Max temperature (in K) for forming a sink when density_threshold_Hpcm3 <= density <= maximal_density_threshold_Hpcm3.
+  density_threshold_Hpcm3: 1e3                # Minimum gas density (in Hydrogen atoms/cm3) required to form a sink particle.
+  maximal_density_threshold_Hpcm3: 1e5        # If the gas density exceeds this value (in Hydrogen atoms/cm3), a sink forms regardless of temperature if all other criteria are passed. (Default: FLT_MAX)
+  sink_minimal_mass_Msun:      0.             # (Optional) Sink minimal mass in Msun. This parameter prevents m_sink << m_gas in low resolution simulations. (Default: 0.0)
+  stellar_particle_mass_Msun:  20             # Mass of the stellar particle representing the low mass stars (continuous IMF sampling) (in solar mass)
+  minimal_discrete_mass_Msun: 8               # Minimal mass of stars represented by discrete particles (in solar mass)
+  stellar_particle_mass_first_stars_Msun: 20      # Mass of the stellar particle representing the low mass stars (continuous IMF sampling) (in solar mass). First stars
+  minimal_discrete_mass_first_stars_Msun: 8       # Minimal mass of stars represented by discrete particles (in solar mass). First stars
+  star_spawning_sigma_factor: 0.2                 # Factor to rescale the velocity dispersion of the stars when they are spawned. (Default: 0.2)
+  sink_formation_contracting_gas_criterion: 1     # (Optional) Activate the contracting gas check for sink formation. (Default: 1)
+  sink_formation_smoothing_length_criterion: 1    # (Optional) Activate the smoothing length check for sink formation. (Default: 1)
+  sink_formation_jeans_instability_criterion: 1   # (Optional) Activate the two Jeans instability checks for sink formation. (Default: 1)
+  sink_formation_bound_state_criterion: 1         # (Optional) Activate the bound state check for sink formation. (Default: 1)
+  sink_formation_overlapping_sink_criterion: 1    # (Optional) Activate the overlapping sink check for sink formation. (Default: 1)
+  disable_sink_formation: 0                       # (Optional) Disable sink formation. (Default: 0)
+  CFL_condition:                        0.5       # Courant-Friedrich-Levy condition for time integration.
+  timestep_age_threshold_unlimited_Myr: 100.      # (Optional) Age above which sinks no longer have time-step restrictions, except for 2-body encounters involving another young/old sink and gravity (in Mega-years). (Default: FLT_MAX)
+  timestep_age_threshold_Myr:           25.       # (Optional) Age at which sink switch from young to old for time-stepping purposes (in Mega-years). (Default: FLT_MAX)
+  max_timestep_young_Myr:               1.0       # (Optional) Maximal time-step length of young sinks (in Mega-years). (Default: FLT_MAX)
+  max_timestep_old_Myr:                 5.0       # (Optional) Maximal time-step length of old sinks (in Mega-years). (Default: FLT_MAX)
+  n_IMF:                                 2        # (Optional) Number of times the IMF mass can be swallowed in a single timestep. (Default: FLTM_MAX)
+  tolerance_SF_timestep:                 0.5      # (Optional) Tolerance parameter for SF timestep constraint. (Default: 0.5)
 
 # Parameters related to radiative transfer ---------------------------------------
 
@@ -874,6 +1030,7 @@ PowerSpectrum:
   num_folds:         6                    # Number of foldings (1 means no foldings), determines the max k
   fold_factor:       4                    # (Optional) factor by which to reduce the box along each side each folding (default: 4)
   window_order:      3                    # (Optional) order of the mass assignment scheme (default: 3, TSC)
+  shift_centre_small_k_bins: 1            # (Optional) Correct the centre of the bins with a small k to account for the small number of modes entering the bin.
   output_list_on:    0                    # (Optional) Enable the output list
   output_list:       ./output_list_ps.txt # (Optional) File containing the output times (see documentation in "Parameter File" section)
   requested_spectra: ["matter-matter","cdm-cdm","starBH-starBH","gas-matter","pressure-pressure","matter-pressure", "neutrino0-neutrino1"] # Array of strings indicating which components should be correlated for power spectra
diff --git a/format.sh b/format.sh
index 6d7c710001f5d98de7b136d0aeb5239840b31465..b25a073c70f38dd8e87b842cac61621aa64b2c5a 100755
--- a/format.sh
+++ b/format.sh
@@ -1,17 +1,31 @@
 #!/bin/bash
 
-# Clang format command, can be overridden using CLANG_FORMAT_CMD.
-# We currrently use version 13.0 so any overrides should provide that.
-clang=${CLANG_FORMAT_CMD:="clang-format-13"}
+# The clang-format command can be overridden using CLANG_FORMAT_CMD.
+# We currrently use version 18.0 so any overrides should use that version
+# or one known to be compatible with it for instance if your standard
+# command is version 18 use:
+#    CLANG_FORMAT_CMD=clang-format ./format.sh
+clang=${CLANG_FORMAT_CMD:="clang-format-18"}
+
+# Only works in a git checkout. Eat error and just make a report
+# so we don't break CI.
+git status > /dev/null 2>&1
+if test "$?" != "0"; then
+   echo "Not operating in a git checkout. Cannot procede."
+   exit
+fi
 
 # Formatting command
-cmd="$clang -style=file $(git ls-files | grep '\.[ch]$')"
+cmd="$clang -style=file $(git ls-files | grep '\.[ch]$' | grep -v csds_io\.h | grep -v argparse\.h | grep -v vector\.h)"
 
-# Test if `clang-format-13` works
+# Test if `clang-format-18` works
 command -v $clang > /dev/null
 if [[ $? -ne 0 ]]
 then
-    echo "ERROR: cannot find $clang"
+    echo "ERROR: cannot find the command $clang."
+    echo
+    head -8 "$0" | grep -v "/bin/bash" | grep '^#'
+    echo
     exit 1
 fi
 
diff --git a/m4/ax_cc_maxopt.m4 b/m4/ax_cc_maxopt.m4
index 282668af72e638b0f73e9d84b1822b60a0ddda21..c27c4160dd1d79bb986faa191866d2e59cbb7982 100644
--- a/m4/ax_cc_maxopt.m4
+++ b/m4/ax_cc_maxopt.m4
@@ -104,7 +104,7 @@ if test "$ac_test_CFLAGS" != "set"; then
                 echo "******************************************************"])
          ;;
 
-    intel) CFLAGS="$CFLAGS -O3 -ansi-alias"
+    intel | oneapi) CFLAGS="$CFLAGS -O3 -ansi-alias"
 	if test "x$acx_maxopt_portable" = xno; then
 	  icc_archflag=unknown
 	  icc_flags=""
@@ -125,11 +125,11 @@ if test "$ac_test_CFLAGS" != "set"; then
 		    *2?6[[ad]]?:*:*:*) icc_flags="-xAVX -SSE4.2 -xS -xT -xB -xK" ;; # Sandy-bridge
 		    *3?6[[ae]]?:*:*:*) icc_flags="-xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;; #Ivy-bridge
 		    *3?6[[cf]]?:*:*:*|*4?6[[56]]?:*:*:*|*4?6[[ef]]?:*:*:*) icc_flags="-xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;; # Haswell
-		    *3?6d?:*:*:*|*4?6[[7f]]?:*:*:*|*5?66?:*:*:*) icc_flags="-xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;; # Broadwell
+		    *3?6d?:*:*:*|*4?6[[7f]]?:*:*:*|*5?66?:*:*:*) icc_flags=" -xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;; # Broadwell
 		    *4?6[[de]]?:*:*:*) icc_flags="-xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;; # Skylake
 		    *5?6[[56]]?:*:*:*) icc_flags="-xCORE-AVX512 -xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;; # Skylake-AVX512
 		    *5?67?:*:*:*) icc_flags="-xMIC-AVX512 -xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;; # Knights-Landing
-		    *8?6[[de]]?:*:*:*|*9?6[[de]]?:*:*:*) icc_flags="-xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;;# Kabylake 
+		    *8?6[[de]]?:*:*:*|*9?6[[de]]?:*:*:*) icc_flags="-xCORE-AVX2 -xCORE-AVX-I -xAVX -SSE4.2 -xS -xT -xB -xK" ;;# Kabylake
 		    *000?f[[346]]?:*:*:*|?f[[346]]?:*:*:*|f[[346]]?:*:*:*) icc_flags="-xSSE3 -xP -xO -xN -xW -xK" ;;
 		    *00??f??:*:*:*|??f??:*:*:*|?f??:*:*:*|f??:*:*:*) icc_flags="-xSSE2 -xN -xW -xK" ;;
                   esac ;;
@@ -139,7 +139,14 @@ if test "$ac_test_CFLAGS" != "set"; then
                     *06??f??:*:*:*|6??f??:*:*:*) icc_flags="-march=core-avx2" ;;
                     *070?f??:*:*:*|70?f??:*:*:*) icc_flags="-march=core-avx2" ;;
                                    83?f??:*:*:*) icc_flags="-march=core-avx2"
-                                                 CFLAGS="$CFLAGS -fma -ftz -fomit-frame-pointer";; # EPYC
+                                                 CFLAGS="$CFLAGS -fma -ftz -fomit-frame-pointer";; # ROME
+                                   a0?f??:*:*:*) icc_flags="-march=core-avx2"
+                                                 CFLAGS="$CFLAGS -fma -ftz -fomit-frame-pointer";; # MILAN
+                                   a1?f??:*:*:*) icc_flags="-axCORE-AVX512"
+                                                 CFLAGS="$CFLAGS -march=skylake-avx512 -fma -ftz -fomit-frame-pointer";; # GENOA
+                                   aa?f??:*:*:*) icc_flags="-axCORE-AVX512"
+                                                 CFLAGS="$CFLAGS -march=skylake-avx512 -fma -ftz -fomit-frame-pointer";; # BERGAMO
+
                   esac ;;
               esac ;;
           esac
diff --git a/m4/ax_compiler_vendor.m4 b/m4/ax_compiler_vendor.m4
index 6b67c4f93116916943d1abd92a485bb33330515f..06dba23c0770705a565763cdaef06d5bd81c8851 100644
--- a/m4/ax_compiler_vendor.m4
+++ b/m4/ax_compiler_vendor.m4
@@ -71,6 +71,7 @@ AC_DEFUN([AX_COMPILER_VENDOR], [dnl
 
 	vendors="
 		intel:		__ICC,__ECC,__INTEL_COMPILER
+                oneapi:         __INTEL_LLVM_COMPILER
 		ibm:		__xlc__,__xlC__,__IBMC__,__IBMCPP__,__ibmxl__
 		pathscale:	__PATHCC__,__PATHSCALE__
 		clang:		__clang__
diff --git a/m4/ax_compiler_version.m4 b/m4/ax_compiler_version.m4
index 4995beb6032e792c209b1e045c7f58b8712f89a2..901487a6f9850590647243736329c7632e6f7f10 100644
--- a/m4/ax_compiler_version.m4
+++ b/m4/ax_compiler_version.m4
@@ -52,6 +52,19 @@ AC_DEFUN([_AX_COMPILER_VERSION_INTEL],
     AC_MSG_FAILURE([[[$0]] unknown intel compiler version]))
   ax_cv_[]_AC_LANG_ABBREV[]_compiler_version="$_ax_[]_AC_LANG_ABBREV[]_compiler_version_major.$_ax_[]_AC_LANG_ABBREV[]_compiler_version_minor.$_ax_[]_AC_LANG_ABBREV[]_compiler_version_patch"
   ])
+AC_DEFUN([_AX_COMPILER_VERSION_ONEAPI],
+  [ dnl
+  AC_COMPUTE_INT(_ax_[]_AC_LANG_ABBREV[]_compiler_version_major,
+    [__INTEL_LLVM_COMPILER/10000],,
+    AC_MSG_FAILURE([[[$0]] unknown oneapi compiler version]))
+  AC_COMPUTE_INT(_ax_[]_AC_LANG_ABBREV[]_compiler_version_minor,
+    [(__INTEL_LLVM_COMPILER%10000)/100],,
+    AC_MSG_FAILURE([[[$0]] unknown oneapi compiler version]))
+  AC_COMPUTE_INT(_ax_[]_AC_LANG_ABBREV[]_compiler_version_patch,
+    [(__INTEL_LLVM_COMPILER%100)],,
+    AC_MSG_FAILURE([[[$0]] unknown oneapi compiler version]))
+  ax_cv_[]_AC_LANG_ABBREV[]_compiler_version="$_ax_[]_AC_LANG_ABBREV[]_compiler_version_major.$_ax_[]_AC_LANG_ABBREV[]_compiler_version_minor.$_ax_[]_AC_LANG_ABBREV[]_compiler_version_patch"
+  ])
 
 # for IBM
 AC_DEFUN([_AX_COMPILER_VERSION_IBM],
@@ -505,6 +518,7 @@ AC_DEFUN([AX_COMPILER_VERSION],[dnl
     [ dnl
       AS_CASE([$ax_cv_[]_AC_LANG_ABBREV[]_compiler_vendor],
         [intel],[_AX_COMPILER_VERSION_INTEL],
+        [oneapi],[_AX_COMPILER_VERSION_ONEAPI],
 	[ibm],[_AX_COMPILER_VERSION_IBM],
 	[pathscale],[_AX_COMPILER_VERSION_PATHSCALE],
 	[clang],[_AX_COMPILER_VERSION_CLANG],
diff --git a/src/Makefile.am b/src/Makefile.am
index b0ad938f55121ccf5320452aea44c49ac5a2b656..8ef49d50705cbc7018f2d347bb190a02679858a5 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -16,7 +16,8 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # Add the non-standard paths to the included library headers
-AM_CFLAGS = $(HDF5_CPPFLAGS) $(GSL_INCS) $(FFTW_INCS) $(NUMA_INCS) $(GRACKLE_INCS)  $(SUNDIALS_INCS) $(OPENMP_CFLAGS) $(CHEALPIX_CFLAGS)
+AM_CFLAGS = $(HDF5_CPPFLAGS) $(GSL_INCS) $(FFTW_INCS) $(NUMA_INCS) \
+        $(GRACKLE_INCS)  $(SUNDIALS_INCS) $(CHEALPIX_CFLAGS) $(LUSTREAPI_INCS)
 
 # Assign a "safe" version number
 AM_LDFLAGS = $(HDF5_LDFLAGS) $(FFTW_LIBS)
@@ -25,7 +26,9 @@ AM_LDFLAGS = $(HDF5_LDFLAGS) $(FFTW_LIBS)
 GIT_CMD = @GIT_CMD@
 
 # Additional dependencies for shared libraries.
-EXTRA_LIBS = $(GSL_LIBS) $(HDF5_LIBS) $(FFTW_LIBS) $(NUMA_LIBS) $(PROFILER_LIBS) $(TCMALLOC_LIBS) $(JEMALLOC_LIBS) $(TBBMALLOC_LIBS) $(GRACKLE_LIBS)  $(SUNDIALS_LIBS) $(CHEALPIX_LIBS)
+EXTRA_LIBS = $(GSL_LIBS) $(GMP_LIBS) $(HDF5_LIBS) $(FFTW_LIBS) $(NUMA_LIBS) \
+        $(PROFILER_LIBS) $(TCMALLOC_LIBS) $(JEMALLOC_LIBS) $(TBBMALLOC_LIBS) \
+        $(GRACKLE_LIBS)  $(SUNDIALS_LIBS) $(CHEALPIX_LIBS) $(LUSTREAPI_LIBS)
 
 # MPI libraries.
 MPI_LIBS = $(PARMETIS_LIBS) $(METIS_LIBS) $(MPI_THREAD_LIBS)
@@ -33,7 +36,7 @@ MPI_FLAGS = -DWITH_MPI $(PARMETIS_INCS) $(METIS_INCS)
 
 # Build the libswiftsim library and a convenience library just for the gravity tasks
 lib_LTLIBRARIES = libswiftsim.la
-noinst_LTLIBRARIES = libgrav.la 
+noinst_LTLIBRARIES = libgrav.la
 # Build a MPI-enabled version too?
 if HAVEMPI
 lib_LTLIBRARIES += libswiftsim_mpi.la
@@ -41,31 +44,32 @@ noinst_LTLIBRARIES += libgrav_mpi.la
 endif
 
 # List required headers
-include_HEADERS = space.h runner.h queue.h task.h lock.h cell.h part.h const.h 
-include_HEADERS += cell_hydro.h cell_stars.h cell_grav.h cell_sinks.h cell_black_holes.h cell_rt.h
-include_HEADERS += engine.h swift.h serial_io.h timers.h debug.h scheduler.h proxy.h parallel_io.h 
-include_HEADERS += common_io.h single_io.h distributed_io.h map.h tools.h  partition_fixed_costs.h 
-include_HEADERS += partition.h clocks.h parser.h physical_constants.h physical_constants_cgs.h potential.h version.h 
+include_HEADERS = space.h runner.h queue.h task.h lock.h cell.h part.h const.h
+include_HEADERS += cell_hydro.h cell_stars.h cell_grav.h cell_sinks.h cell_black_holes.h cell_rt.h cell_grid.h
+include_HEADERS += engine.h swift.h serial_io.h timers.h debug.h scheduler.h proxy.h parallel_io.h
+include_HEADERS += common_io.h single_io.h distributed_io.h map.h tools.h  partition_fixed_costs.h
+include_HEADERS += partition.h clocks.h parser.h physical_constants.h physical_constants_cgs.h potential.h version.h
 include_HEADERS += hydro_properties.h riemann.h threadpool.h cooling_io.h cooling.h cooling_struct.h cooling_properties.h cooling_debug.h
-include_HEADERS += statistics.h memswap.h cache.h runner_doiact_hydro_vec.h runner_doiact_undef.h profiler.h entropy_floor.h 
-include_HEADERS += csds.h active.h timeline.h xmf.h gravity_properties.h gravity_derivatives.h 
-include_HEADERS += gravity_softened_derivatives.h vector_power.h collectgroup.h hydro_space.h sort_part.h 
-include_HEADERS += chemistry.h chemistry_io.h chemistry_struct.h chemistry_debug.h cosmology.h restart.h space_getsid.h utilities.h 
-include_HEADERS += cbrt.h exp10.h velociraptor_interface.h swift_velociraptor_part.h output_list.h 
+include_HEADERS += statistics.h memswap.h cache.h runner_doiact_hydro_vec.h runner_doiact_undef.h profiler.h entropy_floor.h
+include_HEADERS += csds.h active.h timeline.h xmf.h gravity_properties.h gravity_derivatives.h
+include_HEADERS += gravity_softened_derivatives.h vector_power.h collectgroup.h hydro_space.h sort_part.h
+include_HEADERS += chemistry.h chemistry_additions.h chemistry_io.h chemistry_struct.h chemistry_debug.h
+include_HEADERS += cosmology.h restart.h space_getsid.h utilities.h
+include_HEADERS += cbrt.h exp10.h velociraptor_interface.h swift_velociraptor_part.h output_list.h
 include_HEADERS += csds_io.h
 include_HEADERS += tracers_io.h tracers.h tracers_triggers.h tracers_struct.h tracers_debug.h
 include_HEADERS += star_formation_io.h star_formation_debug.h extra_io.h
 include_HEADERS += fof.h fof_struct.h fof_io.h fof_catalogue_io.h
-include_HEADERS += multipole.h multipole_accept.h multipole_struct.h binomial.h integer_power.h sincos.h 
-include_HEADERS += star_formation_struct.h star_formation.h star_formation_iact.h 
-include_HEADERS += star_formation_logger.h star_formation_logger_struct.h 
+include_HEADERS += multipole.h multipole_accept.h multipole_struct.h binomial.h integer_power.h sincos.h
+include_HEADERS += star_formation_struct.h star_formation.h star_formation_iact.h
+include_HEADERS += star_formation_logger.h star_formation_logger_struct.h
 include_HEADERS += pressure_floor.h pressure_floor_struct.h pressure_floor_iact.h pressure_floor_debug.h
-include_HEADERS += velociraptor_struct.h velociraptor_io.h random.h memuse.h mpiuse.h memuse_rnodes.h 
+include_HEADERS += velociraptor_struct.h velociraptor_io.h random.h memuse.h mpiuse.h memuse_rnodes.h
 include_HEADERS += black_holes.h black_holes_iact.h black_holes_io.h black_holes_properties.h black_holes_struct.h black_holes_debug.h
 include_HEADERS += feedback.h feedback_new_stars.h feedback_struct.h feedback_properties.h feedback_debug.h feedback_iact.h
 include_HEADERS += space_unique_id.h line_of_sight.h io_compression.h
 include_HEADERS += rays.h rays_struct.h
-include_HEADERS += sink.h sink_struct.h sink_io.h sink_properties.h sink_debug.h
+include_HEADERS += sink.h sink_iact.h sink_struct.h sink_io.h sink_properties.h sink_debug.h
 include_HEADERS += particle_splitting.h particle_splitting_struct.h
 include_HEADERS += chemistry_csds.h star_formation_csds.h
 include_HEADERS += mesh_gravity.h mesh_gravity_mpi.h mesh_gravity_patch.h mesh_gravity_sort.h row_major_id.h
@@ -74,8 +78,11 @@ include_HEADERS += lightcone/lightcone.h lightcone/lightcone_particle_io.h light
 include_HEADERS += lightcone/lightcone_crossing.h lightcone/lightcone_array.h lightcone/lightcone_map.h
 include_HEADERS += lightcone/lightcone_map_types.h lightcone/projected_kernel.h lightcone/lightcone_shell.h
 include_HEADERS += lightcone/healpix_util.h lightcone/pixel_index.h
+include_HEADERS += adaptive_softening.h adaptive_softening_iact.h adaptive_softening_struct.h
+include_HEADERS += forcing.h
 include_HEADERS += power_spectrum.h
 include_HEADERS += ghost_stats.h
+include_HEADERS += swift_lustre_api.h
 
 # source files for EAGLE extra I/O
 EAGLE_EXTRA_IO_SOURCES=
@@ -122,11 +129,17 @@ if HAVEGRACKLECOOLING
 GRACKLE_COOLING_SOURCES += cooling/grackle/cooling.c
 endif
 
+# source files for EAGLE floor
+EAGLE_FLOOR_SOURCES =
+if HAVEEAGLEFLOOR
+EAGLE_FLOOR_SOURCES += entropy_floor/EAGLE/entropy_floor.c
+endif
+
 # source files for GEAR feedback
 GEAR_FEEDBACK_SOURCES =
 if HAVEGEARFEEDBACK
-GEAR_FEEDBACK_SOURCES += feedback/GEAR/stellar_evolution.c feedback/GEAR/feedback.c \
-	feedback/GEAR/initial_mass_function.c feedback/GEAR/supernovae_ia.c feedback/GEAR/supernovae_ii.c
+GEAR_FEEDBACK_SOURCES += feedback/GEAR/stellar_evolution.c feedback/GEAR/feedback.c
+GEAR_FEEDBACK_SOURCES += feedback/GEAR/initial_mass_function.c feedback/GEAR/supernovae_ia.c feedback/GEAR/supernovae_ii.c
 endif
 
 # source files for AGORA feedback
@@ -149,348 +162,354 @@ SPHM1RT_RT_SOURCES += rt/SPHM1RT/rt_cooling.c
 endif
 
 # Common source files
-AM_SOURCES = space.c space_rebuild.c space_regrid.c space_unique_id.c 
-AM_SOURCES += space_sort.c space_split.c space_extras.c space_first_init.c space_init.c 
-AM_SOURCES += space_cell_index.c space_recycle.c 
-AM_SOURCES += runner_main.c runner_doiact_hydro.c runner_doiact_limiter.c 
-AM_SOURCES += runner_doiact_stars.c runner_doiact_black_holes.c runner_ghost.c
+AM_SOURCES = space.c space_rebuild.c space_regrid.c space_unique_id.c
+AM_SOURCES += space_sort.c space_split.c space_extras.c space_first_init.c space_init.c
+AM_SOURCES += space_cell_index.c space_recycle.c
+AM_SOURCES += runner_main.c runner_doiact_hydro.c runner_doiact_limiter.c
+AM_SOURCES += runner_doiact_stars.c runner_doiact_black_holes.c runner_doiact_sinks.c runner_ghost.c
 AM_SOURCES += runner_recv.c runner_pack.c
-AM_SOURCES += runner_sort.c runner_drift.c runner_black_holes.c runner_time_integration.c 
+AM_SOURCES += runner_sort.c runner_drift.c runner_black_holes.c runner_time_integration.c
 AM_SOURCES += runner_doiact_hydro_vec.c runner_others.c
 AM_SOURCES += runner_sinks.c
-AM_SOURCES += cell.c cell_convert_part.c cell_drift.c cell_lock.c cell_pack.c cell_split.c 
-AM_SOURCES += cell_unskip.c 
-AM_SOURCES += engine.c engine_maketasks.c engine_split_particles.c engine_strays.c 
-AM_SOURCES += engine_marktasks.c engine_drift.c engine_unskip.c engine_collect_end_of_step.c 
-AM_SOURCES += engine_redistribute.c engine_fof.c engine_proxy.c engine_io.c engine_config.c 
-AM_SOURCES += queue.c task.c timers.c debug.c scheduler.c proxy.c version.c 
-AM_SOURCES += common_io.c common_io_copy.c common_io_cells.c common_io_fields.c 
-AM_SOURCES += single_io.c serial_io.c distributed_io.c parallel_io.c 
-AM_SOURCES += output_options.c line_of_sight.c restart.c parser.c xmf.c 
-AM_SOURCES += kernel_hydro.c tools.c map.c part.c partition.c clocks.c  
-AM_SOURCES += physical_constants.c units.c potential.c hydro_properties.c 
-AM_SOURCES += threadpool.c cooling.c star_formation.c 
-AM_SOURCES += hydro.c stars.c
-AM_SOURCES += statistics.c profiler.c csds.c part_type.c 
-AM_SOURCES += gravity_properties.c gravity.c multipole.c 
-AM_SOURCES += collectgroup.c hydro_space.c equation_of_state.c io_compression.c 
-AM_SOURCES += chemistry.c cosmology.c velociraptor_interface.c 
-AM_SOURCES += output_list.c velociraptor_dummy.c csds_io.c memuse.c mpiuse.c memuse_rnodes.c
+AM_SOURCES += cell.c cell_convert_part.c cell_drift.c cell_lock.c cell_pack.c cell_split.c
+AM_SOURCES += cell_unskip.c cell_grid.c
+AM_SOURCES += engine.c engine_maketasks.c engine_split_particles.c engine_strays.c
+AM_SOURCES += engine_drift.c engine_unskip.c engine_collect_end_of_step.c
+AM_SOURCES += engine_redistribute.c engine_fof.c engine_proxy.c engine_io.c engine_config.c
+AM_SOURCES += queue.c task.c timers.c debug.c scheduler.c proxy.c version.c
+AM_SOURCES += common_io.c common_io_copy.c common_io_cells.c common_io_fields.c
+AM_SOURCES += single_io.c serial_io.c distributed_io.c parallel_io.c
+AM_SOURCES += output_options.c line_of_sight.c restart.c parser.c xmf.c
+AM_SOURCES += kernel_hydro.c tools.c map.c part.c partition.c clocks.c
+AM_SOURCES += physical_constants.c units.c potential.c hydro_properties.c
+AM_SOURCES += threadpool.c cooling.c star_formation.c
+AM_SOURCES += hydro.c stars.c sink.c
+AM_SOURCES += statistics.c profiler.c csds.c part_type.c
+AM_SOURCES += gravity_properties.c gravity.c multipole.c
+AM_SOURCES += collectgroup.c hydro_space.c equation_of_state.c io_compression.c
+AM_SOURCES += chemistry.c cosmology.c velociraptor_interface.c
+AM_SOURCES += output_list.c csds_io.c memuse.c mpiuse.c memuse_rnodes.c
 AM_SOURCES += fof.c fof_catalogue_io.c
 AM_SOURCES += hashmap.c
 AM_SOURCES += mesh_gravity.c mesh_gravity_mpi.c mesh_gravity_patch.c mesh_gravity_sort.c
 AM_SOURCES += runner_neutrino.c
-AM_SOURCES += neutrino/Default/fermi_dirac.c neutrino/Default/neutrino.c neutrino/Default/neutrino_response.c 
+AM_SOURCES += neutrino/Default/fermi_dirac.c neutrino/Default/neutrino.c neutrino/Default/neutrino_response.c
 AM_SOURCES += rt_parameters.c hdf5_object_to_blob.c ic_info.c exchange_structs.c particle_buffer.c
 AM_SOURCES += lightcone/lightcone.c lightcone/lightcone_particle_io.c lightcone/lightcone_replications.c
 AM_SOURCES += lightcone/healpix_util.c lightcone/lightcone_array.c lightcone/lightcone_map.c
 AM_SOURCES += lightcone/lightcone_map_types.c lightcone/projected_kernel.c lightcone/lightcone_shell.c
 AM_SOURCES += power_spectrum.c
+AM_SOURCES += forcing.c
 AM_SOURCES += ghost_stats.c
 AM_SOURCES += $(EAGLE_EXTRA_IO_SOURCES)
-AM_SOURCES += $(QLA_COOLING_SOURCES) $(QLA_EAGLE_COOLING_SOURCES) 
-AM_SOURCES += $(EAGLE_COOLING_SOURCES) $(EAGLE_FEEDBACK_SOURCES) 
-AM_SOURCES += $(GRACKLE_COOLING_SOURCES) $(GEAR_FEEDBACK_SOURCES) 
+AM_SOURCES += $(QLA_COOLING_SOURCES) $(QLA_EAGLE_COOLING_SOURCES)
+AM_SOURCES += $(EAGLE_COOLING_SOURCES) $(EAGLE_FEEDBACK_SOURCES)
+AM_SOURCES += $(GRACKLE_COOLING_SOURCES) $(GEAR_FEEDBACK_SOURCES)
+AM_SOURCES += $(EAGLE_FLOOR_SOURCES)
 AM_SOURCES += $(AGORA_FEEDBACK_SOURCES)
 AM_SOURCES += $(PS2020_COOLING_SOURCES)
 AM_SOURCES += $(SPHM1RT_RT_SOURCES)
 AM_SOURCES += $(GEAR_RT_SOURCES)
+AM_SOURCES += swift_lustre_api.c
 
 # Include files for distribution, not installation.
-nobase_noinst_HEADERS = align.h approx_math.h atomic.h barrier.h cycle.h error.h inline.h kernel_hydro.h kernel_gravity.h 
+nobase_noinst_HEADERS = align.h approx_math.h atomic.h barrier.h cycle.h error.h inline.h kernel_hydro.h kernel_gravity.h
 nobase_noinst_HEADERS += gravity_iact.h kernel_long_gravity.h vector.h accumulate.h cache.h exp.h log.h
-nobase_noinst_HEADERS += runner_doiact_nosort.h runner_doiact_hydro.h runner_doiact_stars.h runner_doiact_black_holes.h runner_doiact_grav.h 
-nobase_noinst_HEADERS += runner_doiact_functions_hydro.h runner_doiact_functions_stars.h runner_doiact_functions_black_holes.h 
-nobase_noinst_HEADERS += runner_doiact_functions_limiter.h runner_doiact_limiter.h units.h intrinsics.h minmax.h 
+nobase_noinst_HEADERS += runner_doiact_nosort.h runner_doiact_hydro.h runner_doiact_stars.h runner_doiact_black_holes.h runner_doiact_grav.h
+nobase_noinst_HEADERS += runner_doiact_functions_hydro.h runner_doiact_functions_stars.h runner_doiact_functions_black_holes.h
+nobase_noinst_HEADERS += runner_doiact_functions_limiter.h runner_doiact_functions_sinks.h runner_doiact_limiter.h units.h swift_intrinsics.h minmax.h
 nobase_noinst_HEADERS += runner_doiact_sinks.h
-nobase_noinst_HEADERS += kick.h timestep.h drift.h adiabatic_index.h io_properties.h dimension.h part_type.h periodic.h memswap.h 
-nobase_noinst_HEADERS += timestep_limiter.h timestep_limiter_iact.h timestep_sync.h timestep_sync_part.h timestep_limiter_struct.h 
+nobase_noinst_HEADERS += kick.h timestep.h drift.h adiabatic_index.h io_properties.h dimension.h part_type.h periodic.h memswap.h
+nobase_noinst_HEADERS += timestep_limiter.h timestep_limiter_iact.h timestep_sync.h timestep_sync_part.h timestep_limiter_struct.h
 nobase_noinst_HEADERS += csds.h sign.h csds_io.h hashmap.h gravity.h gravity_io.h gravity_csds.h  gravity_cache.h output_options.h
-nobase_noinst_HEADERS += gravity/Default/gravity.h gravity/Default/gravity_iact.h gravity/Default/gravity_io.h 
-nobase_noinst_HEADERS += gravity/Default/gravity_debug.h gravity/Default/gravity_part.h  
-nobase_noinst_HEADERS += gravity/MultiSoftening/gravity.h gravity/MultiSoftening/gravity_iact.h gravity/MultiSoftening/gravity_io.h 
-nobase_noinst_HEADERS += gravity/MultiSoftening/gravity_debug.h gravity/MultiSoftening/gravity_part.h 
-nobase_noinst_HEADERS += gravity/MultiSoftening/gravity_csds.h 
-nobase_noinst_HEADERS += equation_of_state.h 
-nobase_noinst_HEADERS += equation_of_state/ideal_gas/equation_of_state.h equation_of_state/isothermal/equation_of_state.h
+nobase_noinst_HEADERS += gravity/Default/gravity.h gravity/Default/gravity_iact.h gravity/Default/gravity_io.h
+nobase_noinst_HEADERS += gravity/Default/gravity_debug.h gravity/Default/gravity_part.h
+nobase_noinst_HEADERS += gravity/MultiSoftening/gravity.h gravity/MultiSoftening/gravity_iact.h gravity/MultiSoftening/gravity_io.h
+nobase_noinst_HEADERS += gravity/MultiSoftening/gravity_debug.h gravity/MultiSoftening/gravity_part.h
+nobase_noinst_HEADERS += gravity/MultiSoftening/gravity_csds.h
+nobase_noinst_HEADERS += equation_of_state.h
+nobase_noinst_HEADERS += equation_of_state/ideal_gas/equation_of_state.h equation_of_state/isothermal/equation_of_state.h equation_of_state/barotropic/equation_of_state.h
 nobase_noinst_HEADERS += signal_velocity.h
-nobase_noinst_HEADERS += hydro.h hydro_io.h hydro_csds.h hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/None/hydro.h hydro/None/hydro_iact.h hydro/None/hydro_io.h 
-nobase_noinst_HEADERS += hydro/None/hydro_debug.h hydro/None/hydro_part.h 
-nobase_noinst_HEADERS += hydro/None/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/Minimal/hydro.h hydro/Minimal/hydro_iact.h hydro/Minimal/hydro_io.h 
-nobase_noinst_HEADERS += hydro/Minimal/hydro_debug.h hydro/Minimal/hydro_part.h 
-nobase_noinst_HEADERS += hydro/Minimal/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/Phantom/hydro.h hydro/Phantom/hydro_iact.h hydro/Phantom/hydro_io.h 
-nobase_noinst_HEADERS += hydro/Phantom/hydro_debug.h hydro/Phantom/hydro_part.h 
-nobase_noinst_HEADERS += hydro/Phantom/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/Gadget2/hydro.h hydro/Gadget2/hydro_iact.h hydro/Gadget2/hydro_io.h 
-nobase_noinst_HEADERS += hydro/Gadget2/hydro_debug.h hydro/Gadget2/hydro_part.h 
-nobase_noinst_HEADERS += hydro/Gadget2/hydro_parameters.h hydro/Gadget2/hydro_csds.h 
-nobase_noinst_HEADERS += hydro/PressureEntropy/hydro.h hydro/PressureEntropy/hydro_iact.h hydro/PressureEntropy/hydro_io.h 
-nobase_noinst_HEADERS += hydro/PressureEntropy/hydro_debug.h hydro/PressureEntropy/hydro_part.h 
-nobase_noinst_HEADERS += hydro/PressureEntropy/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/PressureEnergy/hydro.h hydro/PressureEnergy/hydro_iact.h hydro/PressureEnergy/hydro_io.h 
-nobase_noinst_HEADERS += hydro/PressureEnergy/hydro_debug.h hydro/PressureEnergy/hydro_part.h 
-nobase_noinst_HEADERS += hydro/PressureEnergy/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/PressureEnergyMorrisMonaghanAV/hydro.h hydro/PressureEnergyMorrisMonaghanAV/hydro_iact.h hydro/PressureEnergyMorrisMonaghanAV/hydro_io.h 
-nobase_noinst_HEADERS += hydro/PressureEnergyMorrisMonaghanAV/hydro_debug.h hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h 
-nobase_noinst_HEADERS += hydro/PressureEnergyMorrisMonaghanAV/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/AnarchyPU/hydro.h hydro/AnarchyPU/hydro_iact.h hydro/AnarchyPU/hydro_io.h 
-nobase_noinst_HEADERS += hydro/AnarchyPU/hydro_debug.h hydro/AnarchyPU/hydro_part.h 
-nobase_noinst_HEADERS += hydro/AnarchyPU/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/SPHENIX/hydro.h hydro/SPHENIX/hydro_iact.h hydro/SPHENIX/hydro_io.h 
+nobase_noinst_HEADERS += hydro.h hydro_io.h hydro_csds.h hydro_parameters.h
+nobase_noinst_HEADERS += hydro/None/hydro.h hydro/None/hydro_iact.h hydro/None/hydro_io.h
+nobase_noinst_HEADERS += hydro/None/hydro_debug.h hydro/None/hydro_part.h
+nobase_noinst_HEADERS += hydro/None/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/Minimal/hydro.h hydro/Minimal/hydro_iact.h hydro/Minimal/hydro_io.h
+nobase_noinst_HEADERS += hydro/Minimal/hydro_debug.h hydro/Minimal/hydro_part.h
+nobase_noinst_HEADERS += hydro/Minimal/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/Phantom/hydro.h hydro/Phantom/hydro_iact.h hydro/Phantom/hydro_io.h
+nobase_noinst_HEADERS += hydro/Phantom/hydro_debug.h hydro/Phantom/hydro_part.h
+nobase_noinst_HEADERS += hydro/Phantom/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/Gadget2/hydro.h hydro/Gadget2/hydro_iact.h hydro/Gadget2/hydro_io.h
+nobase_noinst_HEADERS += hydro/Gadget2/hydro_debug.h hydro/Gadget2/hydro_part.h
+nobase_noinst_HEADERS += hydro/Gadget2/hydro_parameters.h hydro/Gadget2/hydro_csds.h
+nobase_noinst_HEADERS += hydro/PressureEntropy/hydro.h hydro/PressureEntropy/hydro_iact.h hydro/PressureEntropy/hydro_io.h
+nobase_noinst_HEADERS += hydro/PressureEntropy/hydro_debug.h hydro/PressureEntropy/hydro_part.h
+nobase_noinst_HEADERS += hydro/PressureEntropy/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/PressureEnergy/hydro.h hydro/PressureEnergy/hydro_iact.h hydro/PressureEnergy/hydro_io.h
+nobase_noinst_HEADERS += hydro/PressureEnergy/hydro_debug.h hydro/PressureEnergy/hydro_part.h
+nobase_noinst_HEADERS += hydro/PressureEnergy/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/PressureEnergyMorrisMonaghanAV/hydro.h hydro/PressureEnergyMorrisMonaghanAV/hydro_iact.h hydro/PressureEnergyMorrisMonaghanAV/hydro_io.h
+nobase_noinst_HEADERS += hydro/PressureEnergyMorrisMonaghanAV/hydro_debug.h hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h
+nobase_noinst_HEADERS += hydro/PressureEnergyMorrisMonaghanAV/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/AnarchyPU/hydro.h hydro/AnarchyPU/hydro_iact.h hydro/AnarchyPU/hydro_io.h
+nobase_noinst_HEADERS += hydro/AnarchyPU/hydro_debug.h hydro/AnarchyPU/hydro_part.h
+nobase_noinst_HEADERS += hydro/AnarchyPU/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/SPHENIX/hydro.h hydro/SPHENIX/hydro_iact.h hydro/SPHENIX/hydro_io.h
 nobase_noinst_HEADERS += hydro/SPHENIX/hydro_debug.h hydro/SPHENIX/hydro_part.h hydro/SPHENIX/hydro_csds.h
 nobase_noinst_HEADERS += hydro/SPHENIX/hydro_parameters.h
-nobase_noinst_HEADERS += hydro/Gasoline/hydro.h hydro/Gasoline/hydro_iact.h hydro/Gasoline/hydro_io.h 
+nobase_noinst_HEADERS += hydro/Gasoline/hydro.h hydro/Gasoline/hydro_iact.h hydro/Gasoline/hydro_io.h
 nobase_noinst_HEADERS += hydro/Gasoline/hydro_debug.h hydro/Gasoline/hydro_part.h
-nobase_noinst_HEADERS += hydro/Gasoline/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_parameters.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_io.h hydro/Gizmo/hydro_debug.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro.h hydro/Gizmo/hydro_iact.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_part.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_gradients.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_getters.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_setters.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_flux.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_slope_limiters.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_slope_limiters_face.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_slope_limiters_cell.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_unphysical.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_gradients_sph.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_gradients_gizmo.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_velocities.h 
-nobase_noinst_HEADERS += hydro/Gizmo/hydro_lloyd.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_debug.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_part.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_velocities.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_flux.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_debug.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_part.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_flux.h 
-nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_velocities.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_debug.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_gradients.h hydro/Shadowswift/hydro.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_iact.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_io.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_part.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_slope_limiters_cell.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_slope_limiters_face.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_slope_limiters.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi1d_algorithm.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi1d_cell.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi2d_algorithm.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi2d_cell.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi3d_algorithm.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi3d_cell.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi_algorithm.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/voronoi_cell.h 
-nobase_noinst_HEADERS += hydro/Shadowswift/hydro_parameters.h 
+nobase_noinst_HEADERS += hydro/Gasoline/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_parameters.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_io.h hydro/Gizmo/hydro_debug.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro.h hydro/Gizmo/hydro_iact.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_part.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_gradients.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_getters.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_setters.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_flux.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_slope_limiters.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_slope_limiters_face.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_slope_limiters_cell.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_unphysical.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_gradients_sph.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_gradients_gizmo.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_velocities.h
+nobase_noinst_HEADERS += hydro/Gizmo/hydro_lloyd.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_debug.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_part.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_velocities.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFV/hydro_flux.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_debug.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_part.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_flux.h
+nobase_noinst_HEADERS += hydro/Gizmo/MFM/hydro_velocities.h
+nobase_noinst_HEADERS += hydro/Shadowswift/hydro_debug.h
+nobase_noinst_HEADERS += hydro/Shadowswift/hydro.h
+nobase_noinst_HEADERS += hydro/Shadowswift/hydro_iact.h
+nobase_noinst_HEADERS += hydro/Shadowswift/hydro_io.h
+nobase_noinst_HEADERS += hydro/Shadowswift/hydro_part.h
+nobase_noinst_HEADERS += hydro/Shadowswift/hydro_parameters.h
 nobase_noinst_HEADERS += mhd.h mhd_struct.h mhd_io.h
 nobase_noinst_HEADERS += mhd/None/mhd.h mhd/None/mhd_iact.h mhd/None/mhd_struct.h mhd/None/mhd_io.h mhd/None/mhd_debug.h mhd/None/mhd_parameters.h
-nobase_noinst_HEADERS += riemann/riemann_hllc.h riemann/riemann_trrs.h 
-nobase_noinst_HEADERS += riemann/riemann_exact.h riemann/riemann_vacuum.h 
-nobase_noinst_HEADERS += riemann/riemann_checks.h 
-nobase_noinst_HEADERS += rt.h  
-nobase_noinst_HEADERS += rt_additions.h  
-nobase_noinst_HEADERS += rt_io.h 
-nobase_noinst_HEADERS += rt_parameters.h 
-nobase_noinst_HEADERS += rt_properties.h 
-nobase_noinst_HEADERS += rt_struct.h 
-nobase_noinst_HEADERS += rt/none/rt.h  
-nobase_noinst_HEADERS += rt/none/rt_additions.h 
-nobase_noinst_HEADERS += rt/none/rt_iact.h 
-nobase_noinst_HEADERS += rt/none/rt_io.h 
+nobase_noinst_HEADERS += riemann/riemann_hllc.h riemann/riemann_trrs.h
+nobase_noinst_HEADERS += riemann/riemann_exact.h riemann/riemann_vacuum.h
+nobase_noinst_HEADERS += riemann/riemann_checks.h
+nobase_noinst_HEADERS += rt.h
+nobase_noinst_HEADERS += rt_additions.h
+nobase_noinst_HEADERS += rt_io.h
+nobase_noinst_HEADERS += rt_parameters.h
+nobase_noinst_HEADERS += rt_properties.h
+nobase_noinst_HEADERS += rt_struct.h
+nobase_noinst_HEADERS += rt/none/rt.h
+nobase_noinst_HEADERS += rt/none/rt_additions.h
+nobase_noinst_HEADERS += rt/none/rt_iact.h
+nobase_noinst_HEADERS += rt/none/rt_io.h
 nobase_noinst_HEADERS += rt/none/rt_parameters.h
-nobase_noinst_HEADERS += rt/none/rt_properties.h 
+nobase_noinst_HEADERS += rt/none/rt_properties.h
 nobase_noinst_HEADERS += rt/none/rt_struct.h
-nobase_noinst_HEADERS += rt/debug/rt.h  
-nobase_noinst_HEADERS += rt/debug/rt_additions.h 
-nobase_noinst_HEADERS += rt/debug/rt_debugging.h 
-nobase_noinst_HEADERS += rt/debug/rt_gradients.h 
-nobase_noinst_HEADERS += rt/debug/rt_iact.h 
-nobase_noinst_HEADERS += rt/debug/rt_io.h 
+nobase_noinst_HEADERS += rt/debug/rt.h
+nobase_noinst_HEADERS += rt/debug/rt_additions.h
+nobase_noinst_HEADERS += rt/debug/rt_debugging.h
+nobase_noinst_HEADERS += rt/debug/rt_gradients.h
+nobase_noinst_HEADERS += rt/debug/rt_iact.h
+nobase_noinst_HEADERS += rt/debug/rt_io.h
 nobase_noinst_HEADERS += rt/debug/rt_parameters.h
-nobase_noinst_HEADERS += rt/debug/rt_properties.h 
+nobase_noinst_HEADERS += rt/debug/rt_properties.h
 nobase_noinst_HEADERS += rt/debug/rt_struct.h
-nobase_noinst_HEADERS += rt/GEAR/rt_additions.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_blackbody.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_debugging.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_flux.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_getters.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_grackle_utils.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_gradients.h 
-nobase_noinst_HEADERS += rt/GEAR/rt.h  
-nobase_noinst_HEADERS += rt/GEAR/rt_iact.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_interaction_cross_sections.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_interaction_rates.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_io.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_ionization_equilibrium.h 
+nobase_noinst_HEADERS += rt/GEAR/rt_additions.h
+nobase_noinst_HEADERS += rt/GEAR/rt_blackbody.h
+nobase_noinst_HEADERS += rt/GEAR/rt_debugging.h
+nobase_noinst_HEADERS += rt/GEAR/rt_flux.h
+nobase_noinst_HEADERS += rt/GEAR/rt_getters.h
+nobase_noinst_HEADERS += rt/GEAR/rt_grackle_utils.h
+nobase_noinst_HEADERS += rt/GEAR/rt_gradients.h
+nobase_noinst_HEADERS += rt/GEAR/rt.h
+nobase_noinst_HEADERS += rt/GEAR/rt_iact.h
+nobase_noinst_HEADERS += rt/GEAR/rt_interaction_cross_sections.h
+nobase_noinst_HEADERS += rt/GEAR/rt_interaction_rates.h
+nobase_noinst_HEADERS += rt/GEAR/rt_io.h
+nobase_noinst_HEADERS += rt/GEAR/rt_ionization_equilibrium.h
 nobase_noinst_HEADERS += rt/GEAR/rt_parameters.h
-nobase_noinst_HEADERS += rt/GEAR/rt_properties.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_riemann_GLF.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_riemann_HLL_eigenvalues.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_riemann_HLL.h 
+nobase_noinst_HEADERS += rt/GEAR/rt_properties.h
+nobase_noinst_HEADERS += rt/GEAR/rt_riemann_GLF.h
+nobase_noinst_HEADERS += rt/GEAR/rt_riemann_HLL_eigenvalues.h
+nobase_noinst_HEADERS += rt/GEAR/rt_riemann_HLL.h
 nobase_noinst_HEADERS += rt/GEAR/rt_slope_limiters_cell.h
-nobase_noinst_HEADERS += rt/GEAR/rt_slope_limiters_face.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_species.h 
-nobase_noinst_HEADERS += rt/GEAR/rt_stellar_emission_model.h 
+nobase_noinst_HEADERS += rt/GEAR/rt_slope_limiters_face.h
+nobase_noinst_HEADERS += rt/GEAR/rt_species.h
+nobase_noinst_HEADERS += rt/GEAR/rt_stellar_emission_model.h
 nobase_noinst_HEADERS += rt/GEAR/rt_struct.h
 nobase_noinst_HEADERS += rt/GEAR/rt_thermochemistry.h
 nobase_noinst_HEADERS += rt/GEAR/rt_thermochemistry_utils.h
 nobase_noinst_HEADERS += rt/GEAR/rt_unphysical.h
-nobase_noinst_HEADERS += rt/SPHM1RT/rt.h  
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_getters.h 
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_setters.h 
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_iact.h 
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_io.h 
+nobase_noinst_HEADERS += rt/SPHM1RT/rt.h
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_getters.h
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_setters.h
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_iact.h
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_io.h
 nobase_noinst_HEADERS += rt/SPHM1RT/rt_parameters.h
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_properties.h 
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_properties.h
 nobase_noinst_HEADERS += rt/SPHM1RT/rt_struct.h
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_gradients.h 
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_additions.h 
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_gradients.h
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_additions.h
 nobase_noinst_HEADERS += rt/SPHM1RT/rt_cooling.h
 nobase_noinst_HEADERS += rt/SPHM1RT/rt_cooling_rates.h
 nobase_noinst_HEADERS += rt/SPHM1RT/rt_unphysical.h
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_species_and_elements.h 
-nobase_noinst_HEADERS += rt/SPHM1RT/rt_stellar_emission_rate.h 
-nobase_noinst_HEADERS += stars.h stars_io.h stars_csds.h 
-nobase_noinst_HEADERS += stars/None/stars.h stars/None/stars_iact.h stars/None/stars_io.h 
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_species_and_elements.h
+nobase_noinst_HEADERS += rt/SPHM1RT/rt_stellar_emission_rate.h
+nobase_noinst_HEADERS += shadowswift/voronoi.h
+nobase_noinst_HEADERS += stars.h stars_io.h stars_csds.h
+nobase_noinst_HEADERS += stars/None/stars.h stars/None/stars_iact.h stars/None/stars_io.h
 nobase_noinst_HEADERS += stars/None/stars_debug.h stars/None/stars_part.h
-nobase_noinst_HEADERS += stars/Basic/stars.h stars/Basic/stars_iact.h stars/Basic/stars_io.h 
-nobase_noinst_HEADERS += stars/Basic/stars_debug.h stars/Basic/stars_part.h stars/Basic/stars_csds.h  
-nobase_noinst_HEADERS += stars/EAGLE/stars.h stars/EAGLE/stars_iact.h stars/EAGLE/stars_io.h 
-nobase_noinst_HEADERS += stars/EAGLE/stars_debug.h stars/EAGLE/stars_part.h 
-nobase_noinst_HEADERS += stars/GEAR/stars.h stars/GEAR/stars_iact.h stars/GEAR/stars_io.h 
+nobase_noinst_HEADERS += stars/Basic/stars.h stars/Basic/stars_iact.h stars/Basic/stars_io.h
+nobase_noinst_HEADERS += stars/Basic/stars_debug.h stars/Basic/stars_part.h stars/Basic/stars_csds.h
+nobase_noinst_HEADERS += stars/EAGLE/stars.h stars/EAGLE/stars_iact.h stars/EAGLE/stars_io.h
+nobase_noinst_HEADERS += stars/EAGLE/stars_debug.h stars/EAGLE/stars_part.h
+nobase_noinst_HEADERS += stars/GEAR/stars.h stars/GEAR/stars_iact.h stars/GEAR/stars_io.h
 nobase_noinst_HEADERS += stars/GEAR/stars_debug.h stars/GEAR/stars_csds.h stars/GEAR/stars_part.h
-nobase_noinst_HEADERS += potential/none/potential.h potential/point_mass/potential.h 
-nobase_noinst_HEADERS += potential/isothermal/potential.h potential/disc_patch/potential.h 
-nobase_noinst_HEADERS += potential/sine_wave/potential.h potential/constant/potential.h 
-nobase_noinst_HEADERS += potential/hernquist/potential.h potential/nfw/potential.h 
-nobase_noinst_HEADERS += potential/nfw_mn/potential.h potential/point_mass_softened/potential.h 
-nobase_noinst_HEADERS += star_formation/none/star_formation.h star_formation/none/star_formation_struct.h 
+nobase_noinst_HEADERS += stars/GEAR/stars_stellar_type.h
+nobase_noinst_HEADERS += forcing/none/forcing.h forcing/roberts_flow/forcing.h forcing/roberts_flow_acceleration/forcing.h
+nobase_noinst_HEADERS += forcing/ABC_flow/forcing.h
+nobase_noinst_HEADERS += potential/none/potential.h potential/point_mass/potential.h
+nobase_noinst_HEADERS += potential/isothermal/potential.h potential/disc_patch/potential.h
+nobase_noinst_HEADERS += potential/sine_wave/potential.h potential/constant/potential.h
+nobase_noinst_HEADERS += potential/hernquist/potential.h potential/nfw/potential.h
+nobase_noinst_HEADERS += potential/nfw_mn/potential.h potential/point_mass_softened/potential.h
+nobase_noinst_HEADERS += star_formation/none/star_formation.h star_formation/none/star_formation_struct.h
 nobase_noinst_HEADERS += star_formation/none/star_formation_io.h star_formation/none/star_formation_iact.h
 nobase_noinst_HEADERS += star_formation/none/star_formation_csds.h star_formation/none/star_formation_debug.h
-nobase_noinst_HEADERS += star_formation/QLA/star_formation.h star_formation/QLA/star_formation_struct.h 
-nobase_noinst_HEADERS += star_formation/QLA/star_formation_io.h star_formation/QLA/star_formation_iact.h 
+nobase_noinst_HEADERS += star_formation/QLA/star_formation.h star_formation/QLA/star_formation_struct.h
+nobase_noinst_HEADERS += star_formation/QLA/star_formation_io.h star_formation/QLA/star_formation_iact.h
 nobase_noinst_HEADERS += star_formation/QLA/star_formation_debug.h
-nobase_noinst_HEADERS += star_formation/EAGLE/star_formation.h star_formation/EAGLE/star_formation_struct.h 
-nobase_noinst_HEADERS += star_formation/EAGLE/star_formation_io.h star_formation/EAGLE/star_formation_iact.h 
+nobase_noinst_HEADERS += star_formation/EAGLE/star_formation.h star_formation/EAGLE/star_formation_struct.h
+nobase_noinst_HEADERS += star_formation/EAGLE/star_formation_io.h star_formation/EAGLE/star_formation_iact.h
 nobase_noinst_HEADERS += star_formation/EAGLE/star_formation_debug.h
-nobase_noinst_HEADERS += star_formation/GEAR/star_formation.h star_formation/GEAR/star_formation_struct.h 
-nobase_noinst_HEADERS += star_formation/GEAR/star_formation_io.h star_formation/GEAR/star_formation_iact.h 
+nobase_noinst_HEADERS += star_formation/GEAR/star_formation.h star_formation/GEAR/star_formation_struct.h
+nobase_noinst_HEADERS += star_formation/GEAR/star_formation_io.h star_formation/GEAR/star_formation_iact.h
 nobase_noinst_HEADERS += star_formation/GEAR/star_formation_csds.h star_formation/GEAR/star_formation_debug.h
-nobase_noinst_HEADERS += star_formation/EAGLE/star_formation_logger.h star_formation/EAGLE/star_formation_logger_struct.h 
-nobase_noinst_HEADERS += star_formation/GEAR/star_formation_logger.h star_formation/GEAR/star_formation_logger_struct.h 
-nobase_noinst_HEADERS += star_formation/none/star_formation_logger.h star_formation/none/star_formation_logger_struct.h 
-nobase_noinst_HEADERS += cooling/none/cooling.h cooling/none/cooling_struct.h 
+nobase_noinst_HEADERS += star_formation/GEAR/star_formation_setters.h
+nobase_noinst_HEADERS += star_formation/EAGLE/star_formation_logger.h star_formation/EAGLE/star_formation_logger_struct.h
+nobase_noinst_HEADERS += star_formation/GEAR/star_formation_logger.h star_formation/GEAR/star_formation_logger_struct.h
+nobase_noinst_HEADERS += star_formation/none/star_formation_logger.h star_formation/none/star_formation_logger_struct.h
+nobase_noinst_HEADERS += cooling/none/cooling.h cooling/none/cooling_struct.h
 nobase_noinst_HEADERS += cooling/none/cooling_io.h cooling/none/cooling_properties.h  cooling/none/cooling_debug.h
-nobase_noinst_HEADERS += cooling/const_du/cooling.h cooling/const_du/cooling_struct.h 
+nobase_noinst_HEADERS += cooling/const_du/cooling.h cooling/const_du/cooling_struct.h
 nobase_noinst_HEADERS += cooling/const_du/cooling_io.h cooling/const_du/cooling_properties.h cooling/const_du/cooling_debug.h
-nobase_noinst_HEADERS += cooling/const_lambda/cooling.h cooling/const_lambda/cooling_struct.h 
+nobase_noinst_HEADERS += cooling/const_lambda/cooling.h cooling/const_lambda/cooling_struct.h
 nobase_noinst_HEADERS += cooling/const_lambda/cooling_io.h cooling/const_lambda/cooling_properties.h cooling/const_lambda/cooling_debug.h
-nobase_noinst_HEADERS += cooling/grackle/cooling.h cooling/grackle/cooling_struct.h 
+nobase_noinst_HEADERS += cooling/grackle/cooling.h cooling/grackle/cooling_struct.h
 nobase_noinst_HEADERS += cooling/grackle/cooling_io.h cooling/grackle/cooling_properties.h cooling/grackle/cooling_debug.h
-nobase_noinst_HEADERS += cooling/EAGLE/cooling.h cooling/EAGLE/cooling_struct.h cooling/EAGLE/cooling_tables.h 
-nobase_noinst_HEADERS += cooling/EAGLE/cooling_io.h cooling/EAGLE/interpolate.h cooling/EAGLE/cooling_rates.h 
+nobase_noinst_HEADERS += cooling/EAGLE/cooling.h cooling/EAGLE/cooling_struct.h cooling/EAGLE/cooling_tables.h
+nobase_noinst_HEADERS += cooling/EAGLE/cooling_io.h cooling/EAGLE/interpolate.h cooling/EAGLE/cooling_rates.h
 nobase_noinst_HEADERS += cooling/EAGLE/cooling_properties.h cooling/EAGLE/cooling_debug.h
-nobase_noinst_HEADERS += cooling/QLA_EAGLE/cooling.h cooling/QLA_EAGLE/cooling_struct.h cooling/QLA_EAGLE/cooling_tables.h 
-nobase_noinst_HEADERS += cooling/QLA_EAGLE/cooling_io.h cooling/QLA_EAGLE/interpolate.h cooling/QLA_EAGLE/cooling_rates.h 
+nobase_noinst_HEADERS += cooling/QLA_EAGLE/cooling.h cooling/QLA_EAGLE/cooling_struct.h cooling/QLA_EAGLE/cooling_tables.h
+nobase_noinst_HEADERS += cooling/QLA_EAGLE/cooling_io.h cooling/QLA_EAGLE/interpolate.h cooling/QLA_EAGLE/cooling_rates.h
 nobase_noinst_HEADERS += cooling/QLA_EAGLE/cooling_properties.h cooling/QLA_EAGLE/cooling_debug.h
-nobase_noinst_HEADERS += cooling/QLA/cooling.h cooling/QLA/cooling_struct.h cooling/QLA/cooling_tables.h 
-nobase_noinst_HEADERS += cooling/QLA/cooling_io.h cooling/QLA/interpolate.h cooling/QLA/cooling_rates.h 
+nobase_noinst_HEADERS += cooling/QLA/cooling.h cooling/QLA/cooling_struct.h cooling/QLA/cooling_tables.h
+nobase_noinst_HEADERS += cooling/QLA/cooling_io.h cooling/QLA/interpolate.h cooling/QLA/cooling_rates.h
 nobase_noinst_HEADERS += cooling/QLA/cooling_properties.h cooling/QLA/cooling_debug.h
-nobase_noinst_HEADERS += cooling/PS2020/cooling.h cooling/PS2020/cooling_struct.h cooling/PS2020/cooling_subgrid.h 
-nobase_noinst_HEADERS += cooling/PS2020/cooling_io.h cooling/PS2020/interpolate.h cooling/PS2020/cooling_rates.h 
+nobase_noinst_HEADERS += cooling/PS2020/cooling.h cooling/PS2020/cooling_struct.h cooling/PS2020/cooling_subgrid.h
+nobase_noinst_HEADERS += cooling/PS2020/cooling_io.h cooling/PS2020/interpolate.h cooling/PS2020/cooling_rates.h
 nobase_noinst_HEADERS += cooling/PS2020/cooling_tables.h cooling/PS2020/cooling_subgrid.h
 nobase_noinst_HEADERS += cooling/PS2020/cooling_properties.h cooling/PS2020/cooling_debug.h
-nobase_noinst_HEADERS += chemistry/none/chemistry.h 
-nobase_noinst_HEADERS += chemistry/none/chemistry_io.h 
+nobase_noinst_HEADERS += chemistry/none/chemistry.h
+nobase_noinst_HEADERS += chemistry/none/chemistry_additions.h
+nobase_noinst_HEADERS += chemistry/none/chemistry_io.h
 nobase_noinst_HEADERS += chemistry/none/chemistry_csds.h
-nobase_noinst_HEADERS += chemistry/none/chemistry_struct.h 
-nobase_noinst_HEADERS += chemistry/none/chemistry_iact.h 
-nobase_noinst_HEADERS += chemistry/none/chemistry_debug.h 
-nobase_noinst_HEADERS += chemistry/GEAR/chemistry.h 
-nobase_noinst_HEADERS += chemistry/GEAR/chemistry_io.h 
+nobase_noinst_HEADERS += chemistry/none/chemistry_struct.h
+nobase_noinst_HEADERS += chemistry/none/chemistry_iact.h
+nobase_noinst_HEADERS += chemistry/none/chemistry_debug.h
+nobase_noinst_HEADERS += chemistry/GEAR/chemistry.h
+nobase_noinst_HEADERS += chemistry/GEAR/chemistry_io.h
 nobase_noinst_HEADERS += chemistry/GEAR/chemistry_csds.h
-nobase_noinst_HEADERS += chemistry/GEAR/chemistry_struct.h 
-nobase_noinst_HEADERS += chemistry/GEAR/chemistry_iact.h 
-nobase_noinst_HEADERS += chemistry/GEAR/chemistry_debug.h 
+nobase_noinst_HEADERS += chemistry/GEAR/chemistry_struct.h
+nobase_noinst_HEADERS += chemistry/GEAR/chemistry_iact.h
+nobase_noinst_HEADERS += chemistry/GEAR/chemistry_debug.h
 nobase_noinst_HEADERS += chemistry/GEAR_DIFFUSION/chemistry.h
 nobase_noinst_HEADERS += chemistry/GEAR_DIFFUSION/chemistry_io.h
 nobase_noinst_HEADERS += chemistry/GEAR_DIFFUSION/chemistry_struct.h
 nobase_noinst_HEADERS += chemistry/GEAR_DIFFUSION/chemistry_iact.h
 nobase_noinst_HEADERS += chemistry/GEAR_DIFFUSION/chemistry_debug.h
-nobase_noinst_HEADERS += chemistry/AGORA/chemistry.h 
-nobase_noinst_HEADERS += chemistry/AGORA/chemistry_io.h 
+nobase_noinst_HEADERS += chemistry/AGORA/chemistry.h
+nobase_noinst_HEADERS += chemistry/AGORA/chemistry_io.h
 nobase_noinst_HEADERS += chemistry/AGORA/chemistry_csds.h
-nobase_noinst_HEADERS += chemistry/AGORA/chemistry_struct.h 
-nobase_noinst_HEADERS += chemistry/AGORA/chemistry_iact.h 
-nobase_noinst_HEADERS += chemistry/AGORA/chemistry_debug.h 
-nobase_noinst_HEADERS += chemistry/EAGLE/chemistry.h 
-nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_io.h 
+nobase_noinst_HEADERS += chemistry/AGORA/chemistry_struct.h
+nobase_noinst_HEADERS += chemistry/AGORA/chemistry_iact.h
+nobase_noinst_HEADERS += chemistry/AGORA/chemistry_debug.h
+nobase_noinst_HEADERS += chemistry/EAGLE/chemistry.h
+nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_additions.h
+nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_io.h
 nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_struct.h
-nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_iact.h 
-nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_debug.h 
-nobase_noinst_HEADERS += chemistry/QLA/chemistry.h 
-nobase_noinst_HEADERS += chemistry/QLA/chemistry_io.h 
+nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_iact.h
+nobase_noinst_HEADERS += chemistry/EAGLE/chemistry_debug.h
+nobase_noinst_HEADERS += chemistry/QLA/chemistry.h
+nobase_noinst_HEADERS += chemistry/QLA/chemistry_io.h
 nobase_noinst_HEADERS += chemistry/QLA/chemistry_struct.h
-nobase_noinst_HEADERS += chemistry/QLA/chemistry_iact.h 
-nobase_noinst_HEADERS += chemistry/QLA/chemistry_debug.h 
-nobase_noinst_HEADERS += entropy_floor/none/entropy_floor.h 
-nobase_noinst_HEADERS += entropy_floor/EAGLE/entropy_floor.h 
-nobase_noinst_HEADERS += entropy_floor/QLA/entropy_floor.h 
-nobase_noinst_HEADERS += tracers/none/tracers.h tracers/none/tracers_struct.h 
+nobase_noinst_HEADERS += chemistry/QLA/chemistry_iact.h
+nobase_noinst_HEADERS += chemistry/QLA/chemistry_debug.h
+nobase_noinst_HEADERS += entropy_floor/none/entropy_floor.h
+nobase_noinst_HEADERS += entropy_floor/EAGLE/entropy_floor.h
+nobase_noinst_HEADERS += entropy_floor/QLA/entropy_floor.h
+nobase_noinst_HEADERS += tracers/none/tracers.h tracers/none/tracers_struct.h
 nobase_noinst_HEADERS += tracers/none/tracers_io.h tracers/none/tracers_debug.h
-nobase_noinst_HEADERS += tracers/EAGLE/tracers.h tracers/EAGLE/tracers_struct.h 
+nobase_noinst_HEADERS += tracers/EAGLE/tracers.h tracers/EAGLE/tracers_struct.h
 nobase_noinst_HEADERS += tracers/EAGLE/tracers_io.h tracers/EAGLE/tracers_debug.h
 nobase_noinst_HEADERS += extra_io/EAGLE/extra_io.h extra_io/EAGLE/extra.h
-nobase_noinst_HEADERS += feedback/none/feedback.h feedback/none/feedback_struct.h feedback/none/feedback_iact.h 
+nobase_noinst_HEADERS += feedback/none/feedback.h feedback/none/feedback_struct.h feedback/none/feedback_iact.h
 nobase_noinst_HEADERS += feedback/none/feedback_properties.h feedback/none/feedback.h
 nobase_noinst_HEADERS += feedback/none/feedback_debug.h
-nobase_noinst_HEADERS += feedback/AGORA/feedback.h feedback/AGORA/feedback_struct.h feedback/AGORA/feedback_iact.h 
+nobase_noinst_HEADERS += feedback/AGORA/feedback.h feedback/AGORA/feedback_struct.h feedback/AGORA/feedback_iact.h
 nobase_noinst_HEADERS += feedback/AGORA/feedback_properties.h feedback/AGORA/feedback.h
 nobase_noinst_HEADERS += feedback/AGORA/feedback_debug.h
-nobase_noinst_HEADERS += feedback/EAGLE_kinetic/feedback.h feedback/EAGLE_kinetic/feedback_struct.h 
-nobase_noinst_HEADERS += feedback/EAGLE_kinetic/feedback_properties.h feedback/EAGLE_kinetic/feedback_iact.h 
+nobase_noinst_HEADERS += feedback/EAGLE_kinetic/feedback.h feedback/EAGLE_kinetic/feedback_struct.h
+nobase_noinst_HEADERS += feedback/EAGLE_kinetic/feedback_properties.h feedback/EAGLE_kinetic/feedback_iact.h
 nobase_noinst_HEADERS += feedback/EAGLE_kinetic/feedback_debug.h
-nobase_noinst_HEADERS += feedback/EAGLE_thermal/feedback.h feedback/EAGLE_thermal/feedback_struct.h 
-nobase_noinst_HEADERS += feedback/EAGLE_thermal/feedback_properties.h feedback/EAGLE_thermal/feedback_iact.h 
+nobase_noinst_HEADERS += feedback/EAGLE_thermal/feedback.h feedback/EAGLE_thermal/feedback_struct.h
+nobase_noinst_HEADERS += feedback/EAGLE_thermal/feedback_properties.h feedback/EAGLE_thermal/feedback_iact.h
 nobase_noinst_HEADERS += feedback/EAGLE_thermal/feedback_debug.h
 nobase_noinst_HEADERS += feedback/EAGLE/yield_tables.h feedback/EAGLE/imf.h feedback/EAGLE/interpolate.h
 nobase_noinst_HEADERS += feedback/EAGLE/enrichment.h
-nobase_noinst_HEADERS += feedback/GEAR/stellar_evolution_struct.h feedback/GEAR/stellar_evolution.h 
-nobase_noinst_HEADERS += feedback/GEAR/feedback.h feedback/GEAR/feedback_iact.h 
-nobase_noinst_HEADERS += feedback/GEAR/feedback_properties.h feedback/GEAR/feedback_struct.h 
-nobase_noinst_HEADERS += feedback/GEAR/initial_mass_function.h feedback/GEAR/supernovae_ia.h feedback/GEAR/supernovae_ii.h 
-nobase_noinst_HEADERS += feedback/GEAR/lifetime.h feedback/GEAR/hdf5_functions.h feedback/GEAR/interpolation.h 
+nobase_noinst_HEADERS += feedback/GEAR/stellar_evolution_struct.h feedback/GEAR/stellar_evolution.h
+nobase_noinst_HEADERS += feedback/GEAR/feedback.h feedback/GEAR/feedback_iact.h
+nobase_noinst_HEADERS += feedback/GEAR/feedback_properties.h feedback/GEAR/feedback_struct.h
+nobase_noinst_HEADERS += feedback/GEAR/initial_mass_function.h feedback/GEAR/supernovae_ia.h feedback/GEAR/supernovae_ii.h
+nobase_noinst_HEADERS += feedback/GEAR/lifetime.h feedback/GEAR/hdf5_functions.h feedback/GEAR/interpolation.h
 nobase_noinst_HEADERS += feedback/GEAR/feedback_debug.h
-nobase_noinst_HEADERS += black_holes/Default/black_holes.h black_holes/Default/black_holes_io.h 
-nobase_noinst_HEADERS += black_holes/Default/black_holes_part.h black_holes/Default/black_holes_iact.h 
-nobase_noinst_HEADERS += black_holes/Default/black_holes_properties.h 
+nobase_noinst_HEADERS += black_holes/Default/black_holes.h black_holes/Default/black_holes_io.h
+nobase_noinst_HEADERS += black_holes/Default/black_holes_part.h black_holes/Default/black_holes_iact.h
+nobase_noinst_HEADERS += black_holes/Default/black_holes_properties.h
 nobase_noinst_HEADERS += black_holes/Default/black_holes_struct.h
 nobase_noinst_HEADERS += black_holes/Default/black_holes_debug.h
-nobase_noinst_HEADERS += black_holes/EAGLE/black_holes.h black_holes/EAGLE/black_holes_io.h 
-nobase_noinst_HEADERS += black_holes/EAGLE/black_holes_part.h black_holes/EAGLE/black_holes_iact.h 
-nobase_noinst_HEADERS += black_holes/EAGLE/black_holes_properties.h black_holes/EAGLE/black_holes_parameters.h 
+nobase_noinst_HEADERS += black_holes/EAGLE/black_holes.h black_holes/EAGLE/black_holes_io.h
+nobase_noinst_HEADERS += black_holes/EAGLE/black_holes_part.h black_holes/EAGLE/black_holes_iact.h
+nobase_noinst_HEADERS += black_holes/EAGLE/black_holes_properties.h black_holes/EAGLE/black_holes_parameters.h
 nobase_noinst_HEADERS += black_holes/EAGLE/black_holes_struct.h black_holes/EAGLE/black_holes_debug.h
-nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes.h black_holes/SPIN_JET/black_holes_io.h 
-nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes_part.h black_holes/SPIN_JET/black_holes_iact.h 
+nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes.h black_holes/SPIN_JET/black_holes_io.h
+nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes_part.h black_holes/SPIN_JET/black_holes_iact.h
 nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes_properties.h black_holes/SPIN_JET/black_holes_parameters.h
-nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes_spin.h black_holes/SPIN_JET/black_holes_struct.h 
-nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes_debug.h 
-nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor.h pressure_floor/none/pressure_floor.h 
-nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor_iact.h pressure_floor/none/pressure_floor_iact.h 
-nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor_struct.h pressure_floor/none/pressure_floor_struct.h 
+nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes_spin.h black_holes/SPIN_JET/black_holes_struct.h
+nobase_noinst_HEADERS += black_holes/SPIN_JET/black_holes_debug.h
+nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor.h pressure_floor/none/pressure_floor.h
+nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor_iact.h pressure_floor/none/pressure_floor_iact.h
+nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor_struct.h pressure_floor/none/pressure_floor_struct.h
 nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor_debug.h pressure_floor/none/pressure_floor_debug.h
-nobase_noinst_HEADERS += sink/Default/sink.h sink/Default/sink_io.h sink/Default/sink_part.h sink/Default/sink_properties.h 
+nobase_noinst_HEADERS += sink/Default/sink.h sink/Default/sink_io.h sink/Default/sink_part.h sink/Default/sink_properties.h
 nobase_noinst_HEADERS += sink/Default/sink_iact.h sink/Default/sink_struct.h sink/Default/sink_debug.h
-nobase_noinst_HEADERS += sink/GEAR/sink.h sink/GEAR/sink_io.h sink/GEAR/sink_part.h sink/GEAR/sink_properties.h 
+nobase_noinst_HEADERS += sink/GEAR/sink.h sink/GEAR/sink_io.h sink/GEAR/sink_part.h
+nobase_noinst_HEADERS += sink/GEAR/sink_properties.h sink/GEAR/sink_setters.h sink/GEAR/sink_getters.h
 nobase_noinst_HEADERS += sink/GEAR/sink_iact.h sink/GEAR/sink_struct.h sink/GEAR/sink_debug.h
+nobase_noinst_HEADERS += sink/Basic/sink.h sink/Basic/sink_io.h sink/Basic/sink_part.h sink/Basic/sink_properties.h
+nobase_noinst_HEADERS += sink/Basic/sink_iact.h sink/Basic/sink_struct.h sink/Basic/sink_debug.h
 nobase_noinst_HEADERS += neutrino.h neutrino_properties.h neutrino_io.h
 nobase_noinst_HEADERS += neutrino/Default/neutrino.h neutrino/Default/relativity.h neutrino/Default/fermi_dirac.h
 nobase_noinst_HEADERS += neutrino/Default/neutrino_properties.h neutrino/Default/neutrino_io.h
 nobase_noinst_HEADERS += neutrino/Default/neutrino_response.h
+nobase_noinst_HEADERS += fvpm_geometry.h fvpm_geometry_struct.h
+nobase_noinst_HEADERS += fvpm_geometry/None/fvpm_geometry.h fvpm_geometry/None/fvpm_geometry_struct.h
+nobase_noinst_HEADERS += fvpm_geometry/Gizmo/fvpm_geometry.h fvpm_geometry/Gizmo/fvpm_geometry_struct.h
+nobase_noinst_HEADERS += fvpm_geometry/Gizmo/MFM/fvpm_geometry.h fvpm_geometry/Gizmo/MFV/fvpm_geometry.h
 
 # Sources and special flags for the gravity library
 libgrav_la_SOURCES = runner_doiact_grav.c
diff --git a/src/active.h b/src/active.h
index ae2c2423907f2e94b2e5073307efff5e228f21f9..bb26bd8824b32b9fac117ff099848d68fd678b59 100644
--- a/src/active.h
+++ b/src/active.h
@@ -196,7 +196,7 @@ __attribute__((always_inline)) INLINE static int cell_is_active_hydro(
  * @return 1 if the #cell contains at least an active particle, 0 otherwise.
  */
 __attribute__((always_inline)) INLINE static int cell_is_rt_active(
-    struct cell *c, const struct engine *e) {
+    const struct cell *c, const struct engine *e) {
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (c->rt.ti_rt_end_min < e->ti_current_subcycle) {
@@ -301,6 +301,9 @@ __attribute__((always_inline)) INLINE static int cell_is_active_sinks(
  *
  * @param c The #cell.
  * @param e The #engine containing information about the current time.
+ * @param with_star_formation Are we running with standard star formation?
+ * @param with_star_formation_sink Are we running with star formation from
+ * sinks?
  * @return 1 if the #cell contains at least an active particle, 0 otherwise.
  */
 __attribute__((always_inline)) INLINE static int cell_need_activating_stars(
diff --git a/src/adaptive_softening.h b/src/adaptive_softening.h
new file mode 100644
index 0000000000000000000000000000000000000000..196f2d393c945519ac593faa3e92689c3f90679c
--- /dev/null
+++ b/src/adaptive_softening.h
@@ -0,0 +1,152 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_ADAPTIVE_SOFTENING_H
+#define SWIFT_ADAPTIVE_SOFTENING_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Local headers. */
+#include "error.h"
+#include "gravity_properties.h"
+#include "inline.h"
+#include "kernel_hydro.h"
+
+#ifdef ADAPTIVE_SOFTENING
+
+/* Force the gravity tasks to take place after the density loop */
+#define gravity_after_hydro_density 1
+
+/* Verify the correct hydro kernel is being used. */
+#ifndef WENDLAND_C2_KERNEL
+#error \
+    "The adaptive softening terms are only defined for the Wendland-C2 kernel used in the gravity scheme."
+#endif
+
+/* Verify we are using the allowed hydro schemes */
+#if defined(GIZMO_MFM_SPH) || defined(GIZMO_MFV_SPH) || defined(SHADOWFAX_SPH)
+#error "Adaptive softening only implemented for the SPH schemes."
+#endif
+
+/* Verify we are using the correct gravity scheme */
+#ifndef MULTI_SOFTENING_GRAVITY
+#error \
+    "Adaptive softening only implemented for the Multi-softening gravity scheme."
+#endif
+
+/**
+ * @ifdef Update the gravity softening based on the gas' smoothing length.
+ *
+ * @param gp the #gpart to update.
+ * @param p The #part.
+ * @param grav_props The properties of the gravity scheme.
+ */
+INLINE static void gravity_update_softening(
+    struct gpart* gp, const struct part* p,
+    const struct gravity_props* grav_props) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (gp != p->gpart) error("Unlinked part and gpart!");
+#endif
+
+  const float new_softening = p->h * kernel_gamma;
+
+  /* Update the softening but respect limits */
+  if (new_softening > grav_props->max_adaptive_softening)
+    gp->epsilon = grav_props->max_adaptive_softening;
+  else if (new_softening < grav_props->min_adaptive_softening)
+    gp->epsilon = grav_props->min_adaptive_softening;
+  else
+    gp->epsilon = new_softening;
+}
+
+/**
+ * @brief Prepares a particle for the density calculation.
+ *
+ * @param p The particle to act upon
+ */
+INLINE static void adaptive_softening_init_part(struct part* p) {
+
+  p->adaptive_softening_data.zeta = 0.f;
+}
+
+/**
+ * @brief Finishes the density calculation.
+ *
+ * Finish the equation 26 of Price & Monaghan 2007, MNRAS, 374, 4
+ * by adding the pre-factors. Also adds the G/2 factor of eq. 27.
+ *
+ * @param p The particle to act upon.
+ * @param props The properties of the gravity scheme.
+ */
+INLINE static void adaptive_softening_end_density(
+    struct part* p, const struct gravity_props* props) {
+
+  /* Finish calculation of the adaptive softening prefactor (zeta)
+   * Pre-factor in eq. 26 of Price & Monaghan 2007 */
+  const float rho_inv = 1.f / p->rho;
+  const float h_drho = -p->h * rho_inv * hydro_dimension_inv;
+  p->adaptive_softening_data.zeta *= h_drho;
+
+  /* Multiply in the Gravitational constant
+   * See the prefactor in eq. 27 of Price & Monaghan 2007 */
+  p->adaptive_softening_data.zeta *= 0.5f * props->G_Newton;
+}
+
+#else
+
+/* Gravity tasks can take place at the same time as hydro */
+#define gravity_after_hydro_density 0
+
+/**
+ * @ifdef Update the gravity softening based on the gas' smoothing length.
+ *
+ * Softening is fixed: Nothing to do here.
+ *
+ * @param gp the #gpart to update.
+ * @param p The #part.
+ * @param grav_props The properties of the gravity scheme.
+ */
+INLINE static void gravity_update_softening(
+    struct gpart* gp, const struct part* p,
+    const struct gravity_props* grav_props) {}
+
+/**
+ * @brief Prepares a particle for the density calculation.
+ *
+ * Nothing to do here.
+ *
+ * @param p The particle to act upon
+ */
+INLINE static void adaptive_softening_init_part(struct part* p) {}
+
+/**
+ * @brief Finishes the density calculation.
+ *
+ * Nothing to do here.
+ *
+ * @param p The particle to act upon
+ * @param props The properties of the gravity scheme.
+ */
+INLINE static void adaptive_softening_end_density(
+    struct part* p, const struct gravity_props* props) {}
+
+#endif /* ADAPTIVE_SOFTENING */
+
+#endif /* SWIFT_ADAPTIVE_SOFTENING_H */
diff --git a/src/adaptive_softening_iact.h b/src/adaptive_softening_iact.h
new file mode 100644
index 0000000000000000000000000000000000000000..f81230479bcbe8815f1924f452af3c1f1099c481
--- /dev/null
+++ b/src/adaptive_softening_iact.h
@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+/* Config parameters. */
+#ifndef SWIFT_ADAPTIVE_SOFTENING_IACT_H
+#define SWIFT_ADAPTIVE_SOFTENING_IACT_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Local headers. */
+#include "adaptive_softening_struct.h"
+#include "inline.h"
+#include "kernel_hydro.h"
+
+#ifdef ADAPTIVE_SOFTENING
+
+/**
+ * @brief Computes the contribution to the softening length change term.
+ *
+ * Computes equation 26 of Price & Monaghan 2007, MNRAS, 374, 4.
+ * without the prefactor. The prefactor gets added in end_density().
+ *
+ * @param pi The #part for which we compute terms.
+ * @param ui The ratio of the inter-particle distance to the smoothing length.
+ * @param hi_inv The inverse the particle's smoothing length.
+ * @param mj The mass of the other particle.
+ */
+__attribute__((always_inline)) INLINE static void
+adaptive_softening_add_correction_term(struct part *pi, const float ui,
+                                       const float hi_inv, const float mj) {
+
+  pi->adaptive_softening_data.zeta += mj * potential_dh(ui, hi_inv);
+}
+
+/**
+ * @brief Computes the norm of the accleration due to the change in softening.
+ *
+ * Computes equation 27 of Price & Monaghan 2007, MNRAS, 374, 4.
+ * without the mass and the distance vector. These are multiplied in
+ * by the parent function.
+ *
+ * @param pi The first #part.
+ * @param pj The second #part.
+ * @param wi_dr The norm of the kernel gradient for i.
+ * @param wj_dr The norm of the kernel gradient for j.
+ * @param f_ij The smoothling-length correction term for i.
+ * @param f_ji The smoothling-length correction term for j.
+ * @param r_inv the inverse of the distance linking the particles.
+ */
+__attribute__((always_inline)) INLINE static float
+adaptive_softening_get_acc_term(const struct part *restrict pi,
+                                const struct part *restrict pj,
+                                const float wi_dr, const float wj_dr,
+                                const float f_ij, const float f_ji,
+                                const float r_inv) {
+  /* Recover some data */
+  const float zetai = pi->adaptive_softening_data.zeta;
+  const float zetaj = pj->adaptive_softening_data.zeta;
+
+  /* Adaptive softening acceleration term
+   * Price & Monaghan 2007, eq. 27 (second term)
+   * Note that G/2 is included in the zeta terms.
+   * Note also that f_ij is 1/Omega_i in Price's notation. */
+  const float adap_acc_term =
+      0.5f * (zetai * f_ij * wi_dr + zetaj * f_ji * wj_dr) * r_inv;
+
+  return adap_acc_term;
+}
+
+#else
+
+/**
+ * @brief Computes the contribution to the softening length change term.
+ *
+ * No adaptive softening --> Nothing to do.
+ *
+ * @param pi The #part for which we compute terms.
+ * @param ui The ratio of the inter-particle distance to the smoothing length.
+ * @param hi_inv The inverse the particle's smoothing length.
+ * @param mj The mass of the other particle.
+ */
+__attribute__((always_inline)) INLINE static void
+adaptive_softening_add_correction_term(struct part *pi, const float ui,
+                                       const float hi_inv, const float mj) {}
+
+/**
+ * @brief Computes the norm of the accleration due to the change in softening.
+ *
+ * No adaptive softening --> Nothing to do --> Return 0.
+ *
+ * @param pi The first #part.
+ * @param pj The second #part.
+ * @param wi_dr The norm of the kernel gradient for i.
+ * @param wj_dr The norm of the kernel gradient for j.
+ * @param f_ij The smoothling-length correction term for i.
+ * @param f_ji The smoothling-length correction term for j.
+ * @param r_inv the inverse of the distance linking the particles.
+ */
+__attribute__((always_inline)) INLINE static float
+adaptive_softening_get_acc_term(const struct part *restrict pi,
+                                const struct part *restrict pj,
+                                const float wi_dr, const float wj_dr,
+                                const float f_ij, const float f_ji,
+                                const float r_inv) {
+  return 0.f;
+}
+
+#endif /* ADAPTIVE_SOFTENING */
+
+#endif /* SWIFT_ADAPTIVE_SOFTENING_IACT_H */
diff --git a/src/hydro/Shadowswift/voronoi1d_cell.h b/src/adaptive_softening_struct.h
similarity index 56%
rename from src/hydro/Shadowswift/voronoi1d_cell.h
rename to src/adaptive_softening_struct.h
index 29fff097c4129b1b3ac81f6e1d4291c4efcbce90..c8301af8e800c555f1e2449380f52e046b73fcb2 100644
--- a/src/hydro/Shadowswift/voronoi1d_cell.h
+++ b/src/adaptive_softening_struct.h
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,33 +16,30 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
+#ifndef SWIFT_ADAPTIVE_SOFTENING_STRUCT_H
+#define SWIFT_ADAPTIVE_SOFTENING_STRUCT_H
 
-#ifndef SWIFT_VORONOIXD_CELL_H
-#define SWIFT_VORONOIXD_CELL_H
+/* Config parameters. */
+#include <config.h>
 
-/* 1D Voronoi cell */
-struct voronoi_cell {
+#ifdef ADAPTIVE_SOFTENING
 
-  /* The position of the generator of the cell. */
-  double x;
+/**
+ * @brief Particle-carried fields for the adaptive softening scheme.
+ */
+struct adaptive_softening_part_data {
 
-  /* The position of the left neighbour of the cell. */
-  double xL;
-
-  /* The position of the right neighbour of the cell. */
-  double xR;
-
-  /* The particle ID of the left neighbour. */
-  unsigned long long idL;
+  /*! Correction term for energy conservation */
+  float zeta;
+};
 
-  /* The particle ID of the right neighbour. */
-  unsigned long long idR;
+#else
 
-  /* The "volume" of the 1D cell. */
-  float volume;
+/**
+ * @brief Particle-carried fields for the adaptive softening scheme.
+ */
+struct adaptive_softening_part_data {};
 
-  /* The centroid of the cell. */
-  float centroid;
-};
+#endif
 
-#endif  // SWIFT_VORONOIXD_CELL_H
+#endif /* SWIFT_ADAPTIVE_SOFTENING_STRUCT_H */
diff --git a/src/adiabatic_index.h b/src/adiabatic_index.h
index a623d40722278b41181642617f223218c1b5f156..d303c1805b0f50059558e4790e82c656ddaf1bff 100644
--- a/src/adiabatic_index.h
+++ b/src/adiabatic_index.h
@@ -117,8 +117,8 @@ __attribute__((always_inline, const)) INLINE static float pow_gamma(float x) {
   const float icbrt = icbrtf(x); /* x^(-1/3) */
   return icbrt * x * x;          /* x^(5/3) */
 #else
-  const float cbrt = cbrtf(x);                 /* x^(1/3) */
-  return cbrt * cbrt * x;                      /* x^(5/3) */
+  const float cbrt = cbrtf(x); /* x^(1/3) */
+  return cbrt * cbrt * x;      /* x^(5/3) */
 #endif  // WITH_ICBRTF
 
 #elif defined(HYDRO_GAMMA_7_5)
@@ -128,10 +128,10 @@ __attribute__((always_inline, const)) INLINE static float pow_gamma(float x) {
 #elif defined(HYDRO_GAMMA_4_3)
 
 #ifdef WITH_ICBRTF
-  const float icbrt = icbrtf(x);               /* x^(-1/3) */
-  return icbrt * icbrt * x * x;                /* x^(4/3) */
+  const float icbrt = icbrtf(x); /* x^(-1/3) */
+  return icbrt * icbrt * x * x;  /* x^(4/3) */
 #else
-  return cbrtf(x) * x;                   /* x^(4/3) */
+  return cbrtf(x) * x; /* x^(4/3) */
 #endif  // WITH_ICBRTF
 
 #elif defined(HYDRO_GAMMA_2_1)
@@ -161,8 +161,8 @@ __attribute__((always_inline, const)) INLINE static float pow_gamma_minus_one(
   const float icbrt = icbrtf(x); /* x^(-1/3) */
   return x * icbrt;              /* x^(2/3) */
 #else
-  const float cbrt = cbrtf(x);                 /* x^(1/3) */
-  return cbrt * cbrt;                          /* x^(2/3) */
+  const float cbrt = cbrtf(x); /* x^(1/3) */
+  return cbrt * cbrt;          /* x^(2/3) */
 #endif  // WITH_ICBRTF
 
 #elif defined(HYDRO_GAMMA_7_5)
@@ -172,10 +172,10 @@ __attribute__((always_inline, const)) INLINE static float pow_gamma_minus_one(
 #elif defined(HYDRO_GAMMA_4_3)
 
 #ifdef WITH_ICBRTF
-  const float icbrt = icbrtf(x);               /* x^(-1/3) */
-  return x * icbrt * icbrt;                    /* x^(1/3) */
+  const float icbrt = icbrtf(x); /* x^(-1/3) */
+  return x * icbrt * icbrt;      /* x^(1/3) */
 #else
-  return cbrtf(x);                       /* x^(1/3) */
+  return cbrtf(x); /* x^(1/3) */
 #endif  // WITH_ICBRTF
 
 #elif defined(HYDRO_GAMMA_2_1)
@@ -205,8 +205,8 @@ pow_minus_gamma_minus_one(float x) {
   const float icbrt = icbrtf(x); /* x^(-1/3) */
   return icbrt * icbrt;          /* x^(-2/3) */
 #else
-  const float cbrt_inv = 1.f / cbrtf(x);       /* x^(-1/3) */
-  return cbrt_inv * cbrt_inv;                  /* x^(-2/3) */
+  const float cbrt_inv = 1.f / cbrtf(x); /* x^(-1/3) */
+  return cbrt_inv * cbrt_inv;            /* x^(-2/3) */
 #endif  // WITH_ICBRTF
 
 #elif defined(HYDRO_GAMMA_7_5)
@@ -216,9 +216,9 @@ pow_minus_gamma_minus_one(float x) {
 #elif defined(HYDRO_GAMMA_4_3)
 
 #ifdef WITH_ICBRTF
-  return icbrtf(x);                            /* x^(-1/3) */
+  return icbrtf(x); /* x^(-1/3) */
 #else
-  return 1.f / cbrtf(x);                 /* x^(-1/3) */
+  return 1.f / cbrtf(x); /* x^(-1/3) */
 #endif  // WITH_ICBRTF
 
 #elif defined(HYDRO_GAMMA_2_1)
@@ -264,7 +264,7 @@ __attribute__((always_inline, const)) INLINE static float pow_minus_gamma(
 #elif defined(HYDRO_GAMMA_4_3)
 
 #ifdef WITH_ICBRTF
-  const float cbrt_inv = icbrtf(x);            /* x^(-1/3) */
+  const float cbrt_inv = icbrtf(x); /* x^(-1/3) */
 #else
   const float cbrt_inv = 1.f / cbrtf(x); /* x^(-1/3) */
 #endif  // WITH_ICBRTF
diff --git a/src/black_holes/Default/black_holes_io.h b/src/black_holes/Default/black_holes_io.h
index 43aa06f1fb0f6cb4206d72a2edbd4af47e018547..e74c44e6dd22b377d01db41acaefd84b5cdeaa5f 100644
--- a/src/black_holes/Default/black_holes_io.h
+++ b/src/black_holes/Default/black_holes_io.h
@@ -144,9 +144,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
   list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                                  bparts, mass, "Masses of the particles");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           bparts, id, "Unique ID of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, bparts, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
 
   list[4] = io_make_output_field(
       "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, bparts, h,
diff --git a/src/black_holes/Default/black_holes_part.h b/src/black_holes/Default/black_holes_part.h
index d8e379dcec878993c41c222ef7bc6de2c9e755ab..5783279ad6f8328fbb43b46d56658c86ec2f7e4b 100644
--- a/src/black_holes/Default/black_holes_part.h
+++ b/src/black_holes/Default/black_holes_part.h
@@ -54,6 +54,9 @@ struct bpart {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   struct {
 
     /* Number of neighbours. */
diff --git a/src/black_holes/EAGLE/black_holes.h b/src/black_holes/EAGLE/black_holes.h
index 468a72431d992e0890dbde152c2b4841bc3a94e4..8fdafadb9d53ce662c6b6d13c5db43d12c724c5e 100644
--- a/src/black_holes/EAGLE/black_holes.h
+++ b/src/black_holes/EAGLE/black_holes.h
@@ -23,6 +23,7 @@
 #include "black_holes_properties.h"
 #include "black_holes_struct.h"
 #include "cooling.h"
+#include "cooling/PS2020/cooling_tables.h"
 #include "cosmology.h"
 #include "dimension.h"
 #include "exp10.h"
diff --git a/src/black_holes/EAGLE/black_holes_io.h b/src/black_holes/EAGLE/black_holes_io.h
index 9a588be7eae825f4b9b7894755dfe08574defe31..5ab59fc77d73f8b74c216bccab376748c620324e 100644
--- a/src/black_holes/EAGLE/black_holes_io.h
+++ b/src/black_holes/EAGLE/black_holes_io.h
@@ -185,9 +185,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       io_make_output_field("DynamicalMasses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                            bparts, mass, "Dynamical masses of the particles");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           bparts, id, "Unique ID of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, bparts, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
 
   list[4] = io_make_output_field(
       "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, bparts, h,
@@ -198,9 +198,10 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
                                  "Subgrid masses of the particles");
 
   if (with_cosmology) {
-    list[6] = io_make_output_field(
+    list[6] = io_make_physical_output_field(
         "FormationScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-        formation_scale_factor, "Scale-factors at which the BHs were formed");
+        formation_scale_factor, /*can convert to comoving=*/0,
+        "Scale-factors at which the BHs were formed");
   } else {
     list[6] = io_make_output_field("FormationTimes", FLOAT, 1, UNIT_CONV_TIME,
                                    0.f, bparts, formation_time,
@@ -233,22 +234,23 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "birth. This does not include any mass accreted onto any merged black "
       "holes.");
 
-  list[12] = io_make_output_field(
+  list[12] = io_make_physical_output_field(
       "CumulativeNumberOfSeeds", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      cumulative_number_seeds,
+      cumulative_number_seeds, /*can convert to comoving=*/0,
       "Total number of BH seeds that have merged into this black hole");
 
-  list[13] =
-      io_make_output_field("NumberOfMergers", INT, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           bparts, number_of_mergers,
-                           "Number of mergers the black holes went through. "
-                           "This does not include the number of mergers "
-                           "accumulated by any merged black hole.");
+  list[13] = io_make_physical_output_field(
+      "NumberOfMergers", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
+      number_of_mergers, /*can convert to comoving=*/1,
+      "Number of mergers the black holes went through. "
+      "This does not include the number of mergers "
+      "accumulated by any merged black hole.");
 
   if (with_cosmology) {
-    list[14] = io_make_output_field(
+    list[14] = io_make_physical_output_field(
         "LastHighEddingtonFractionScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS,
         0.f, bparts, last_high_Eddington_fraction_scale_factor,
+        /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last reached a large Eddington "
         "ratio. -1 if never reached.");
   } else {
@@ -260,9 +262,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
   }
 
   if (with_cosmology) {
-    list[15] = io_make_output_field(
+    list[15] = io_make_physical_output_field(
         "LastMinorMergerScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
-        bparts, last_minor_merger_scale_factor,
+        bparts, last_minor_merger_scale_factor, /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last had a minor merger.");
   } else {
     list[15] = io_make_output_field(
@@ -272,9 +274,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
   }
 
   if (with_cosmology) {
-    list[16] = io_make_output_field(
+    list[16] = io_make_physical_output_field(
         "LastMajorMergerScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
-        bparts, last_major_merger_scale_factor,
+        bparts, last_major_merger_scale_factor, /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last had a major merger.");
   } else {
     list[16] = io_make_output_field(
@@ -308,30 +310,30 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       io_make_output_field("TimeBins", CHAR, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
                            time_bin, "Time-bins of the particles");
 
-  list[21] = io_make_output_field(
+  list[21] = io_make_physical_output_field(
       "NumberOfSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_gas_swallows,
+      number_of_gas_swallows, /*can convert to comoving=*/1,
       "Number of gas particles the black holes have swallowed. "
       "This includes the particles swallowed by any of the black holes that "
       "merged into this one.");
 
-  list[22] = io_make_output_field(
+  list[22] = io_make_physical_output_field(
       "NumberOfDirectSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_direct_gas_swallows,
+      number_of_direct_gas_swallows, /*can convert to comoving=*/1,
       "Number of gas particles the black holes have swallowed. "
       "This does not include any particles swallowed by any of the black holes "
       "that merged into this one.");
 
-  list[23] = io_make_output_field(
+  list[23] = io_make_physical_output_field(
       "NumberOfRepositions", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_repositions,
+      number_of_repositions, /*can convert to comoving=*/1,
       "Number of repositioning events the black holes went through. This does "
       "not include the number of reposition events accumulated by any merged "
       "black holes.");
 
-  list[24] = io_make_output_field(
+  list[24] = io_make_physical_output_field(
       "NumberOfRepositionAttempts", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_reposition_attempts,
+      number_of_reposition_attempts, /*can convert to comoving=*/1,
       "Number of time steps in which the black holes had an eligible particle "
       "to reposition to. They may or may not have ended up moving there, "
       "depending on their subgrid mass and on whether these particles were at "
@@ -339,9 +341,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "not include attempted repositioning events accumulated by any merged "
       "black holes.");
 
-  list[25] = io_make_output_field(
+  list[25] = io_make_physical_output_field(
       "NumberOfTimeSteps", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_time_steps,
+      number_of_time_steps, /*can convert to comoving=*/0,
       "Total number of time steps at which the black holes were active.");
 
   list[26] = io_make_output_field(
@@ -360,9 +362,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       sound_speed_subgrid_gas,
       "Physical subgrid sound-speeds used in the subgrid-Bondi model.");
 
-  list[29] = io_make_output_field(
+  list[29] = io_make_physical_output_field(
       "BirthGasDensities", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, bparts,
-      formation_gas_density,
+      formation_gas_density, /*can convert to comoving=*/0,
       "Physical densities of the converted part at the time of birth. "
       "We store the physical density at the birth redshift, no conversion is "
       "needed.");
@@ -373,9 +375,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "Physical angular momenta that the black holes have accumulated through "
       "subgrid accretion.");
 
-  list[31] = io_make_output_field(
+  list[31] = io_make_physical_output_field(
       "NumberOfGasNeighbours", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      num_ngbs,
+      num_ngbs, /*can convert to comoving=*/0,
       "Integer number of gas neighbour particles within the black hole "
       "kernels.");
 
@@ -392,23 +394,23 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "This is 0 for black holes that have never repositioned, or if the "
       "simulation has been run without prescribed repositioning speed.");
 
-  list[34] = io_make_output_field(
+  list[34] = io_make_physical_output_field(
       "NumberOfHeatingEvents", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      AGN_number_of_energy_injections,
+      AGN_number_of_energy_injections, /*can convert to comoving=*/1,
       "Integer number of (thermal) energy injections the black hole has had "
       "so far. This counts each heated gas particle separately, and so can "
       "increase by more than one during a single time step.");
 
-  list[35] = io_make_output_field(
+  list[35] = io_make_physical_output_field(
       "NumberOfAGNEvents", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      AGN_number_of_AGN_events,
+      AGN_number_of_AGN_events, /*can convert to comoving=*/1,
       "Integer number of AGN events the black hole has had so far"
       " (the number of time steps in which the BH did AGN feedback).");
 
   if (with_cosmology) {
-    list[36] = io_make_output_field(
+    list[36] = io_make_physical_output_field(
         "LastAGNFeedbackScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
-        bparts, last_AGN_event_scale_factor,
+        bparts, last_AGN_event_scale_factor, /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last had an AGN event.");
   } else {
     list[36] = io_make_output_field(
diff --git a/src/black_holes/EAGLE/black_holes_part.h b/src/black_holes/EAGLE/black_holes_part.h
index 51bd44ec8b12efc4df8c9ebc614c8aa08a7eed78..48e7b7a3fb8f260c0c056c7b3771e403c951bd0a 100644
--- a/src/black_holes/EAGLE/black_holes_part.h
+++ b/src/black_holes/EAGLE/black_holes_part.h
@@ -62,6 +62,9 @@ struct bpart {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   struct {
 
     /* Number of neighbours. */
diff --git a/src/black_holes/SPIN_JET/black_holes.h b/src/black_holes/SPIN_JET/black_holes.h
index 143958a469fb6409464c922c31a1cc309175c442..526dff63197fda91b067dc9880b34d38baa1c260 100644
--- a/src/black_holes/SPIN_JET/black_holes.h
+++ b/src/black_holes/SPIN_JET/black_holes.h
@@ -58,8 +58,9 @@ __attribute__((always_inline)) INLINE static float black_holes_compute_timestep(
 
     /* Compute instantaneous energy supply rate to the BH energy reservoir
      * which is proportional to the BH mass accretion rate */
-    const double Energy_rate = bp->radiative_efficiency * props->epsilon_f *
-                               bp->accretion_rate * c * c;
+    const double Energy_rate =
+        (bp->radiative_efficiency * props->epsilon_f + bp->wind_efficiency) *
+        bp->accretion_rate * c * c;
 
     /* Compute instantaneous jet energy supply rate to the BH jet reservoir
      * which is proportional to the BH mass accretion rate */
@@ -71,8 +72,7 @@ __attribute__((always_inline)) INLINE static float black_holes_compute_timestep(
     /* Average particle mass in BH's kernel */
     const double mean_ngb_mass = bp->ngb_mass / ((double)bp->num_ngbs);
     /* Without multiplying by mean_ngb_mass we'd get energy per unit mass */
-    const double E_heat =
-        props->AGN_delta_T_desired * props->temp_to_u_factor * mean_ngb_mass;
+    const double E_heat = bp->delta_T * props->temp_to_u_factor * mean_ngb_mass;
     const double E_jet = 0.5 * mean_ngb_mass * bp->v_jet * bp->v_jet;
 
     /* Compute average time between energy injections for the given accretion
@@ -151,7 +151,6 @@ __attribute__((always_inline)) INLINE static void black_holes_first_init_bpart(
   bp->AGN_number_of_AGN_events = 0;
   bp->AGN_number_of_energy_injections = 0;
   bp->AGN_cumulative_energy = 0.f;
-  bp->aspect_ratio = 0.01f;
   bp->jet_efficiency = 0.1f;
   bp->radiative_efficiency = 0.1f;
   bp->accretion_disk_angle = 0.01f;
@@ -159,17 +158,27 @@ __attribute__((always_inline)) INLINE static void black_holes_first_init_bpart(
   bp->eddington_fraction = 0.01f;
   bp->jet_reservoir = 0.f;
   bp->total_jet_energy = 0.f;
+  bp->wind_efficiency = 0.f;
+  bp->wind_energy = 0.f;
+  bp->radiated_energy = 0.f;
+  bp->accretion_efficiency = 1.f;
   bp->dt_jet = 0.f;
   bp->dt_ang_mom = 0.f;
   bp->AGN_number_of_AGN_jet_events = 0;
   bp->AGN_number_of_jet_injections = 0;
-  bp->group_mass = 0.f;
   for (int i = 0; i < BH_accretion_modes_count; ++i)
     bp->accreted_mass_by_mode[i] = 0.f;
   for (int i = 0; i < BH_accretion_modes_count; ++i)
     bp->thermal_energy_by_mode[i] = 0.f;
   for (int i = 0; i < BH_accretion_modes_count; ++i)
     bp->jet_energy_by_mode[i] = 0.f;
+  for (int i = 0; i < BH_accretion_modes_count; ++i)
+    bp->wind_energy_by_mode[i] = 0.f;
+  for (int i = 0; i < BH_accretion_modes_count; ++i)
+    bp->radiated_energy_by_mode[i] = 0.f;
+  bp->jet_direction[0] = bp->angular_momentum_direction[0];
+  bp->jet_direction[1] = bp->angular_momentum_direction[1];
+  bp->jet_direction[2] = bp->angular_momentum_direction[2];
 
   black_holes_mark_bpart_as_not_swallowed(&bp->merger_data);
 }
@@ -191,7 +200,10 @@ __attribute__((always_inline)) INLINE static void black_holes_init_bpart(
   bp->density.wcount = 0.f;
   bp->density.wcount_dh = 0.f;
   bp->rho_gas = 0.f;
+  bp->rho_gas_hot = 0.f;
   bp->sound_speed_gas = 0.f;
+  bp->sound_speed_gas_hot = 0.f;
+  bp->internal_energy_gas = 0.f;
   bp->velocity_gas[0] = 0.f;
   bp->velocity_gas[1] = 0.f;
   bp->velocity_gas[2] = 0.f;
@@ -212,6 +224,8 @@ __attribute__((always_inline)) INLINE static void black_holes_init_bpart(
   bp->accretion_rate = 0.f; /* Optionally accumulated ngb-by-ngb */
   bp->f_visc = FLT_MAX;
   bp->mass_at_start_of_step = bp->mass; /* bp->mass may grow in nibbling mode */
+  bp->rho_gas_hot = 0.f;
+  bp->sound_speed_gas_hot = 0.f;
 
   /* Reset the rays carried by this BH */
   ray_init(bp->rays, spinjet_blackhole_number_of_rays);
@@ -312,11 +326,18 @@ __attribute__((always_inline)) INLINE static void black_holes_end_density(
   bp->density.wcount *= h_inv_dim;
   bp->density.wcount_dh *= h_inv_dim_plus_one;
   bp->rho_gas *= h_inv_dim;
+  bp->rho_gas_hot *= h_inv_dim;
   const float rho_inv = 1.f / bp->rho_gas;
 
   /* For the following, we also have to undo the mass smoothing
    * (N.B.: bp->velocity_gas is in BH frame, in internal units). */
+  if (bp->rho_gas_hot > 0.)
+    bp->sound_speed_gas_hot *= h_inv_dim / bp->rho_gas_hot;
+  bp->internal_energy_gas *= h_inv_dim * rho_inv;
   bp->sound_speed_gas *= h_inv_dim * rho_inv;
+  if (bp->rho_gas_hot > 0.) {
+    bp->sound_speed_gas_hot *= h_inv_dim / bp->rho_gas_hot;
+  }
   bp->velocity_gas[0] *= h_inv_dim * rho_inv;
   bp->velocity_gas[1] *= h_inv_dim * rho_inv;
   bp->velocity_gas[2] *= h_inv_dim * rho_inv;
@@ -375,6 +396,8 @@ black_holes_bpart_has_no_neighbours(struct bpart* bp,
   bp->curl_v_gas[0] = FLT_MAX;
   bp->curl_v_gas[1] = FLT_MAX;
   bp->curl_v_gas[2] = FLT_MAX;
+
+  bp->internal_energy_gas = -FLT_MAX;
 }
 
 /**
@@ -546,7 +569,7 @@ __attribute__((always_inline)) INLINE static void black_holes_swallow_bpart(
   }
 
   /* Evolve the black hole spin. */
-  merger_spin_evolve(bpi, bpj, constants);
+  black_hole_merger_spin_evolve(bpi, bpj, constants);
 
   /* Increase the masses of the BH. */
   bpi->mass += bpj->mass;
@@ -573,19 +596,19 @@ __attribute__((always_inline)) INLINE static void black_holes_swallow_bpart(
     dot_product = 0.;
   }
 
-  if (j_BH(bpi, constants) * dot_product <
-      -0.5 * j_warp(bpi, constants, props)) {
+  if (black_hole_angular_momentum_magnitude(bpi, constants) * dot_product <
+      -0.5 * black_hole_warp_angular_momentum(bpi, constants, props)) {
     bpi->spin = -1. * fabsf(bpi->spin);
   } else {
     bpi->spin = fabsf(bpi->spin);
   }
 
   /* Update various quantities with new spin */
-  decide_mode(bpi, props);
-  bpi->aspect_ratio = aspect_ratio(bpi, constants, props);
+  black_hole_select_accretion_mode(bpi, props);
   bpi->accretion_disk_angle = dot_product;
-  bpi->radiative_efficiency = rad_efficiency(bpi, props);
-  bpi->jet_efficiency = jet_efficiency(bpi, props);
+  bpi->radiative_efficiency = black_hole_radiative_efficiency(bpi, props);
+  bpi->jet_efficiency = black_hole_jet_efficiency(bpi, props);
+  bpi->wind_efficiency = black_hole_wind_efficiency(bpi, props);
 
   /* Collect the swallowed angular momentum */
   bpi->swallowed_angular_momentum[0] += bpj->swallowed_angular_momentum[0];
@@ -782,10 +805,19 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
     const double gas_rho_phys = bp->rho_gas * cosmo->a3_inv;
     const double n_H = gas_rho_phys * 0.75 / proton_mass;
     const double boost_ratio = n_H / props->boost_n_h_star;
-    const double boost_factor =
-        max(pow(boost_ratio, props->boost_beta), props->boost_alpha);
+    double boost_factor;
+    if (boost_ratio > 1.0) {
+      boost_factor = props->boost_alpha * pow(boost_ratio, props->boost_beta);
+    } else {
+      boost_factor = props->boost_alpha;
+    }
+
     accr_rate *= boost_factor;
+    bp->accretion_boost_factor = boost_factor;
+  } else {
+    bp->accretion_boost_factor = 1.;
   }
+
   /* Compute the reduction factor from Rosas-Guevara et al. (2015) */
   if (props->with_angmom_limiter) {
     const double Bondi_radius = G * BH_mass / gas_c_phys2;
@@ -806,10 +838,12 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
     bp->f_visc = 1.0;
   }
 
-  /* Compute the Eddington rate (internal units) */
+  /* Compute the Eddington rate (internal units). The radiative efficiency
+     is taken to be equal to the Novikov-Thorne (1973) one, and thus varies
+     with spin between 4% and 40%. */
   const double Eddington_rate =
       4. * M_PI * G * BH_mass * proton_mass /
-      (props->radiative_efficiency * c * sigma_Thomson);
+      (eps_Novikov_Thorne(bp->spin) * c * sigma_Thomson);
 
   /* Should we record this time as the most recent high accretion rate? */
   if (accr_rate > props->f_Edd_recording * Eddington_rate) {
@@ -821,26 +855,29 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
   }
 
   /* Limit the accretion rate to a fraction of the Eddington rate */
-  accr_rate = min(accr_rate, props->f_Edd * Eddington_rate);
-  bp->accretion_rate = accr_rate;
   bp->eddington_fraction = accr_rate / Eddington_rate;
+  accr_rate = min(accr_rate, props->f_Edd * Eddington_rate);
+
+  /* Include the effects of the accretion efficiency */
+  bp->accretion_rate = accr_rate * bp->accretion_efficiency;
+  bp->eddington_fraction *= bp->accretion_efficiency;
+
+  /* Change mode based on new accretion rates if necessary, and update
+     all important quantities. */
+  black_hole_select_accretion_mode(bp, props);
+  bp->accretion_efficiency =
+      black_hole_accretion_efficiency(bp, props, constants, cosmo);
+  bp->accretion_rate = accr_rate * bp->accretion_efficiency;
+  bp->eddington_fraction = bp->accretion_rate / Eddington_rate;
+
+  bp->radiative_efficiency = black_hole_radiative_efficiency(bp, props);
+  bp->jet_efficiency = black_hole_jet_efficiency(bp, props);
+  bp->wind_efficiency = black_hole_wind_efficiency(bp, props);
 
   /* Define feedback-related quantities that we will update and need later on */
   double luminosity = 0.;
   double jet_power = 0.;
 
-  /* Check whether we are including ADIOS winds in the thick disk */
-  if ((bp->accretion_mode == BH_thick_disc) &&
-      (props->include_ADIOS_suppression)) {
-    const double Bondi_R = G * BH_mass / gas_c_phys2;
-    const float ADIOS_suppression = powf(
-        Bondi_R / (props->ADIOS_R_in * R_gravitational(BH_mass, constants)),
-        props->ADIOS_s);
-    accr_rate = accr_rate / ADIOS_suppression;
-    bp->accretion_rate = accr_rate;
-    bp->eddington_fraction = accr_rate / Eddington_rate;
-  }
-
   /* How much mass will be consumed over this time step? */
   double delta_m_0 = bp->accretion_rate * dt;
 
@@ -866,8 +903,8 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
 
   /* Decide if accretion is prograde (spin positive) or retrograde
      (spin negative) based on condition from King et al. (2005) */
-  if ((j_BH(bp, constants) * dot_product <
-       -0.5 * j_warp(bp, constants, props)) &&
+  if ((black_hole_angular_momentum_magnitude(bp, constants) * dot_product <
+       -0.5 * black_hole_warp_angular_momentum(bp, constants, props)) &&
       (fabsf(bp->spin) > 0.01)) {
     bp->spin = -1. * fabsf(bp->spin);
   } else {
@@ -878,7 +915,7 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
      step */
   double n_i = 0.;
   if (bp->accretion_rate > 0.) {
-    n_i = delta_m_0 / m_warp(bp, constants, props);
+    n_i = delta_m_0 / black_hole_warp_mass(bp, constants, props);
   }
 
   /* Update the angular momentum vector of the BH based on how many
@@ -895,16 +932,19 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
       bp->angular_momentum_direction[2] =
           bp->spec_angular_momentum_gas[2] / spec_ang_mom_norm;
     } else {
+      const double j_warp =
+          black_hole_warp_angular_momentum(bp, constants, props);
+      const double j_BH = black_hole_angular_momentum_magnitude(bp, constants);
       const double ang_mom_total[3] = {
-          bp->angular_momentum_direction[0] * j_BH(bp, constants) +
+          bp->angular_momentum_direction[0] * j_BH +
               n_i * bp->spec_angular_momentum_gas[0] / spec_ang_mom_norm *
-                  j_warp(bp, constants, props),
-          bp->angular_momentum_direction[1] * j_BH(bp, constants) +
+                  j_warp,
+          bp->angular_momentum_direction[1] * j_BH +
               n_i * bp->spec_angular_momentum_gas[1] / spec_ang_mom_norm *
-                  j_warp(bp, constants, props),
-          bp->angular_momentum_direction[2] * j_BH(bp, constants) +
+                  j_warp,
+          bp->angular_momentum_direction[2] * j_BH +
               n_i * bp->spec_angular_momentum_gas[2] / spec_ang_mom_norm *
-                  j_warp(bp, constants, props)};
+                  j_warp};
 
       /* Modulus of the new J_BH */
       const double modulus = sqrt(ang_mom_total[0] * ang_mom_total[0] +
@@ -920,28 +960,21 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
   }
 
   /* Check if we are fixing the jet along the z-axis. */
-  if (props->fix_jet_efficiency) {
+  if (props->fix_jet_direction) {
     bp->angular_momentum_direction[0] = 0.;
     bp->angular_momentum_direction[1] = 0.;
     bp->angular_momentum_direction[2] = 1;
   }
 
-  /* The amount of mass the BH is actually swallowing, including the
-     effects of efficiencies. */
-  const double delta_m_real =
-      delta_m_0 * (1. - rad_efficiency(bp, props) - jet_efficiency(bp, props));
-
-  /* Increase the reservoir*/
-  bp->jet_reservoir +=
-      delta_m_0 * c * c * props->eps_f_jet * jet_efficiency(bp, props);
-  bp->energy_reservoir +=
-      delta_m_0 * c * c * props->epsilon_f * rad_efficiency(bp, props);
+  bp->jet_direction[0] = bp->angular_momentum_direction[0];
+  bp->jet_direction[1] = bp->angular_momentum_direction[1];
+  bp->jet_direction[2] = bp->angular_momentum_direction[2];
 
   float spin_final = -1.;
   /* Calculate the change in the BH spin */
   if (bp->subgrid_mass > 0.) {
     spin_final = bp->spin + delta_m_0 / bp->subgrid_mass *
-                                da_dln_mbh_0(bp, constants, props);
+                                black_hole_spinup_rate(bp, constants, props);
   } else {
     error(
         "Black hole with id %lld tried to evolve spin with zero "
@@ -967,14 +1000,53 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
     spin_final = 0.01;
   }
 
-  /* Update the spin and mass. */
+  /* Update the spin */
   bp->spin = spin_final;
+
+  /* Update other quantities */
+  bp->accretion_disk_angle = dot_product;
+
+  /* Update efficiencies given the new spin */
+  bp->radiative_efficiency = black_hole_radiative_efficiency(bp, props);
+  bp->jet_efficiency = black_hole_jet_efficiency(bp, props);
+  bp->wind_efficiency = black_hole_wind_efficiency(bp, props);
+
+  /* Final jet power at the end of the step */
+  jet_power = bp->jet_efficiency * bp->accretion_rate * c * c;
+
+  /* Final luminosity at the end of the step */
+  luminosity = bp->radiative_efficiency * bp->accretion_rate * c * c;
+
+  /* The amount of mass the BH is actually swallowing, including the
+     effects of the updated efficiencies. */
+  const double delta_m_real =
+      delta_m_0 * (1. - black_hole_radiative_efficiency(bp, props) -
+                   black_hole_jet_efficiency(bp, props) -
+                   black_hole_wind_efficiency(bp, props));
+
+  /* Increase the reservoir */
+  bp->jet_reservoir += delta_m_0 * c * c * props->eps_f_jet *
+                       black_hole_jet_efficiency(bp, props);
+  bp->energy_reservoir +=
+      delta_m_0 * c * c *
+      (props->epsilon_f * black_hole_radiative_efficiency(bp, props) +
+       black_hole_wind_efficiency(bp, props));
+
+  /* Update the masses */
   bp->subgrid_mass += delta_m_real;
   bp->total_accreted_mass += delta_m_real;
 
   /* Update the total accreted masses split by accretion mode of the BHs */
   bp->accreted_mass_by_mode[bp->accretion_mode] += delta_m_real;
 
+  /* Update total energies launched as radiation/winds, and by mode */
+  bp->radiated_energy += delta_m_0 * c * c * bp->radiative_efficiency;
+  bp->radiated_energy_by_mode[bp->accretion_mode] +=
+      delta_m_0 * c * c * bp->radiative_efficiency;
+  bp->wind_energy_by_mode[bp->accretion_mode] +=
+      delta_m_0 * c * c * bp->wind_efficiency;
+  bp->wind_energy += delta_m_0 * c * c * bp->wind_efficiency;
+
   if (bp->subgrid_mass < 0.) {
     warning(
         "Black hole %lld has reached a negative mass (%f) due"
@@ -983,26 +1055,6 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
     bp->subgrid_mass = props->subgrid_seed_mass;
   }
 
-  /* Update other quantities. */
-  bp->accretion_disk_angle = dot_product;
-  bp->aspect_ratio = aspect_ratio(bp, constants, props);
-  if (props->fix_radiative_efficiency) {
-    bp->radiative_efficiency = props->radiative_efficiency;
-  } else {
-    bp->radiative_efficiency = rad_efficiency(bp, props);
-  }
-  if (props->fix_jet_efficiency) {
-    bp->jet_efficiency = props->jet_efficiency;
-  } else {
-    bp->jet_efficiency = jet_efficiency(bp, props);
-  }
-
-  /* Final jet power at the end of the step */
-  jet_power = bp->jet_efficiency * accr_rate * c * c;
-
-  /* Final luminosity at the end of the step */
-  luminosity = bp->radiative_efficiency * accr_rate * c * c;
-
   /* Increase the subgrid angular momentum according to what we accreted
    * (already in physical units, a factors from velocity and radius cancel) */
   bp->accreted_angular_momentum[0] +=
@@ -1019,7 +1071,8 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
      * in proprtion to its current accretion rate. We do not account for this
      * in the swallowing approach, however. */
     bp->mass -=
-        (bp->radiative_efficiency + bp->jet_efficiency) * accr_rate * dt;
+        (bp->radiative_efficiency + bp->jet_efficiency + bp->wind_efficiency) *
+        bp->accretion_rate * dt;
     if (bp->mass < 0)
       error(
           "Black hole %lld has reached a negative mass (%f). This is "
@@ -1031,10 +1084,13 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
    * Note that we have subtracted the particles we swallowed from the ngb_mass
    * and num_ngbs accumulators. */
 
+  /* Update the heating temperature of the BH */
+  bp->delta_T = black_hole_feedback_delta_T(bp, props, cosmo, constants);
+
   /* Mean gas particle mass in the BH's kernel */
   const double mean_ngb_mass = bp->ngb_mass / ((double)bp->num_ngbs);
   /* Energy per unit mass corresponding to the temperature jump delta_T */
-  double delta_u = props->AGN_delta_T_desired * props->temp_to_u_factor;
+  double delta_u = bp->delta_T * props->temp_to_u_factor;
   /* Number of energy injections at this time-step (will be computed below) */
   int number_of_energy_injections;
   /* Average total energy needed to heat the target number of Ngbs */
@@ -1214,33 +1270,40 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
      */
     int N_unsuccessful_jet_injections = 0;
 
-    for (int i = 0; i < number_of_jet_injections; i++) {
+    /* Store all of this in the black hole for delivery onto the gas. */
+    bp->to_distribute.AGN_delta_u_jet = delta_u_jet;
+    bp->to_distribute.AGN_number_of_jet_injections = number_of_jet_injections;
+
+    /* Check for situations where the gas has been swallowed (not used in the
+     * nibbling scheme, or for situations where one of the BH hemispheres is
+     * empty. In this case we do not perform any jet kicks at all. */
+    for (int i = 0; i < number_of_jet_injections / 2; i++) {
 
       /* If the gas particle that the BH wants to heat has just been swallowed
-       * by the same BH, increment the counter of unsuccessful injections. If
-       * the particle has not been swallowed by the BH, increase the energy that
-       * will later be subtracted from the BH's energy reservoir.
-       * Loop over jet both rays.*/
-
-      if (i % 2 == 0) {
-        if (bp->rays_jet[i].id_min_length != -1) {
-          Jet_energy_deposited += delta_u_jet * bp->rays_jet[i].mass;
-        } else {
-          N_unsuccessful_jet_injections++;
-        }
+       * by the same BH (or if there are no particles in either of the jet
+       * jet rays, i.e. on either side of the BH hemisphere), increment the
+       * counter of unsuccessful injections. If the particle has not been
+       * swallowed by the BH and both hemispheres are populated, the energy
+       * to be deposited will be subracted from the jet reservoir. */
+
+      if ((bp->rays_jet[i].id_min_length != -1) &&
+          (bp->rays_jet_pos[i].id_min_length != -1)) {
+
+        /* Neither hemisphere of the BH is empty, nor have any particles been
+         * swallowed, so we can safely deposity the jet energy. */
+        Jet_energy_deposited += delta_u_jet * bp->rays_jet[i].mass;
+        Jet_energy_deposited += delta_u_jet * bp->rays_jet_pos[i].mass;
+
       } else {
-        if (bp->rays_jet_pos[i].id_min_length != -1) {
-          Jet_energy_deposited += delta_u_jet * bp->rays_jet_pos[i].mass;
-        } else {
-          N_unsuccessful_jet_injections++;
-        }
+
+        /* Track how many unsuccessful jet injections happened. */
+        N_unsuccessful_jet_injections += 2;
+
+        /* Reduce the number of jet injections to be handed out. */
+        bp->to_distribute.AGN_number_of_jet_injections -= 2;
       }
     }
 
-    /* Store all of this in the black hole for delivery onto the gas. */
-    bp->to_distribute.AGN_delta_u_jet = delta_u_jet;
-    bp->to_distribute.AGN_number_of_jet_injections = number_of_jet_injections;
-
     /* Subtract the deposited energy from the BH jet energy reservoir. Note
      * that in the stochastic case, the resulting value might be negative.
      * This happens when (due to the probabilistic nature of the model) the
@@ -1254,6 +1317,15 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
     const int N_successful_jet_injections =
         number_of_jet_injections - N_unsuccessful_jet_injections;
 
+    /* If we will be kicking something, choose a new direction for the jets.
+     * This is done by randomly generating a unit vector within a cone of a
+     * given opening angle around the BH spin vector. */
+    if (N_successful_jet_injections > 0) {
+      random_direction_in_cone(
+          bp->id, ti_begin, random_number_BH_kick, props->opening_angle,
+          bp->angular_momentum_direction, bp->jet_direction);
+    }
+
     /* Increase the number of jet energy injections the black hole has kicked
      * so far.  */
     bp->AGN_number_of_jet_injections += N_successful_jet_injections;
@@ -1288,15 +1360,69 @@ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback(
 
   /* Decide the accretion mode of the BH, based on the new spin and Eddington
    * fraction */
-  decide_mode(bp, props);
-
-  /* Calculate a BH angular momentum evolution time step. Ths timestep is
-     chosen so that the BH spin changes by around 10% relative to the current
-     spin value */
+  black_hole_select_accretion_mode(bp, props);
+  bp->accretion_efficiency =
+      black_hole_accretion_efficiency(bp, props, constants, cosmo);
+  bp->accretion_rate = accr_rate * bp->accretion_efficiency;
+  bp->eddington_fraction = bp->accretion_rate / Eddington_rate;
+
+  /* The accretion/feedback mode is now possibly different, so the feedback
+     efficiencies need to be updated. This is important for computing the
+     next time-step of the BH. */
+  bp->radiative_efficiency = black_hole_radiative_efficiency(bp, props);
+  bp->jet_efficiency = black_hole_jet_efficiency(bp, props);
+  bp->wind_efficiency = black_hole_wind_efficiency(bp, props);
+
+  /* Calculate a BH angular momentum evolution time step. Two conditions are
+     used, one ensures that the BH spin changes by a small amount over the
+     next time-step, while the other ensures that the spin is not wildly
+     redirected between two time-steps */
   if ((fabsf(bp->spin) > 0.01) && (bp->accretion_rate > 0.)) {
-    const float dt_ang_mom = 0.1 * fabsf(bp->spin) /
-                             fabsf(da_dln_mbh_0(bp, constants, props)) *
-                             bp->subgrid_mass / bp->accretion_rate;
+
+    /* How much do we want to allow the spin to change over the next time-
+       step? If the spin is large (above 0.1 in magnitude), we allow it
+       to only change by 1% of its current value. If it is small, we instead
+       use a fixed increment of 0.01. This is to prevent the code from
+       becoming very slow. */
+    const float spin_magnitude = fabsf(bp->spin);
+    float epsilon_spin = 0.01;
+    if (spin_magnitude > 0.1) {
+      epsilon_spin = 0.01 * spin_magnitude;
+    }
+    float dt_ang_mom = epsilon_spin /
+                       fabsf(black_hole_spinup_rate(bp, constants, props)) *
+                       bp->subgrid_mass / bp->accretion_rate;
+
+    /* We now compute the angular-momentum (direction) time-step. We allow
+       the anticipated warp angular momentum along the direction perpendicular
+       to the current spin vector to be 10% of the current BH angular momentum,
+       which should correspond to a change of direction of around 5 degrees. We
+       also apply this only if the spin is not low. */
+    if (spin_magnitude > 0.1) {
+
+      /* Angular momentum direction of gas in kernel along the direction of
+         the current spin vector */
+
+      if (spec_ang_mom_norm > 0.) {
+        const float cosine = (bp->spec_angular_momentum_gas[0] *
+                                  bp->angular_momentum_direction[0] +
+                              bp->spec_angular_momentum_gas[1] *
+                                  bp->angular_momentum_direction[1] +
+                              bp->spec_angular_momentum_gas[2] *
+                                  bp->angular_momentum_direction[2]) /
+                             spec_ang_mom_norm;
+        /* Compute sine, i.e. the componenent perpendicular to that. */
+        const float sine = fmaxf(0., sqrtf(1. - cosine * cosine));
+
+        const float dt_redirection =
+            0.1 * black_hole_warp_mass(bp, constants, props) *
+            black_hole_angular_momentum_magnitude(bp, constants) /
+            (bp->accretion_rate *
+             black_hole_warp_angular_momentum(bp, constants, props) * sine);
+        dt_ang_mom = min(dt_ang_mom, dt_redirection);
+      }
+    }
+
     bp->dt_ang_mom = dt_ang_mom;
   } else {
     bp->dt_ang_mom = FLT_MAX;
@@ -1368,6 +1494,7 @@ __attribute__((always_inline)) INLINE static void black_holes_end_reposition(
          * actual potential minimum. */
         if (repos_frac > 1) repos_frac = 1.;
 
+        bp->last_repos_vel = (float)repos_vel;
         bp->reposition.delta_x[0] *= repos_frac;
         bp->reposition.delta_x[1] *= repos_frac;
         bp->reposition.delta_x[2] *= repos_frac;
@@ -1487,9 +1614,10 @@ INLINE static void black_holes_create_from_gas(
   /* Initial seed mass */
   bp->subgrid_mass = props->subgrid_seed_mass;
 
-  /* Small initial spin in random direction*/
+  /* Small initial spin magnitude */
   bp->spin = props->seed_spin;
 
+  /* Generate a random unit vector for the spin direction */
   const float rand_cos_theta =
       2. *
       (0.5 - random_unit_interval(bp->id, ti_current, random_number_BH_spin));
@@ -1503,7 +1631,11 @@ INLINE static void black_holes_create_from_gas(
   bp->angular_momentum_direction[1] = rand_sin_theta * sin(rand_phi);
   bp->angular_momentum_direction[2] = rand_cos_theta;
 
-  bp->aspect_ratio = 0.01f;
+  /* Point the jets in the same direction to begin with */
+  bp->jet_direction[0] = bp->angular_momentum_direction[0];
+  bp->jet_direction[1] = bp->angular_momentum_direction[1];
+  bp->jet_direction[2] = bp->angular_momentum_direction[2];
+
   bp->jet_efficiency = 0.1f;
   bp->radiative_efficiency = 0.1f;
   bp->accretion_disk_angle = 0.01f;
@@ -1511,8 +1643,13 @@ INLINE static void black_holes_create_from_gas(
   bp->eddington_fraction = 0.01f;
   bp->jet_reservoir = 0.f;
   bp->total_jet_energy = 0.f;
+  bp->wind_efficiency = 0.f;
+  bp->wind_energy = 0.f;
+  bp->radiated_energy = 0.f;
+  bp->accretion_efficiency = 1.f;
   bp->dt_jet = 0.f;
   bp->dt_ang_mom = 0.f;
+  bp->delta_T = black_hole_feedback_delta_T(bp, props, cosmo, constants);
   bp->v_jet = black_hole_feedback_dv_jet(bp, props, cosmo, constants);
   bp->AGN_number_of_AGN_jet_events = 0;
   bp->AGN_number_of_jet_injections = 0;
@@ -1522,6 +1659,10 @@ INLINE static void black_holes_create_from_gas(
     bp->thermal_energy_by_mode[i] = 0.f;
   for (int i = 0; i < BH_accretion_modes_count; ++i)
     bp->jet_energy_by_mode[i] = 0.f;
+  for (int i = 0; i < BH_accretion_modes_count; ++i)
+    bp->wind_energy_by_mode[i] = 0.f;
+  for (int i = 0; i < BH_accretion_modes_count; ++i)
+    bp->radiated_energy_by_mode[i] = 0.f;
 
   /* We haven't accreted anything yet */
   bp->total_accreted_mass = 0.f;
@@ -1562,16 +1703,4 @@ INLINE static void black_holes_create_from_gas(
   black_holes_mark_bpart_as_not_swallowed(&bp->merger_data);
 }
 
-/**
- * @brief Store the halo mass in the fof algorithm for the black
- * hole particle.
- *
- * @param p_data The black hole particle data.
- * @param halo_mass The halo mass to update.
- */
-__attribute__((always_inline)) INLINE static void black_holes_update_halo_mass(
-    struct bpart* bp, float halo_mass) {
-  bp->group_mass = halo_mass;
-}
-
 #endif /* SWIFT_SPIN_JET_BLACK_HOLES_H */
diff --git a/src/black_holes/SPIN_JET/black_holes_iact.h b/src/black_holes/SPIN_JET/black_holes_iact.h
index f252f9d0350e7ee282af4f171547febb295f6938..dfa6ddd240f5f76e9633f6f12ba4e63b2f9ae550 100644
--- a/src/black_holes/SPIN_JET/black_holes_iact.h
+++ b/src/black_holes/SPIN_JET/black_holes_iact.h
@@ -53,7 +53,7 @@
  */
 __attribute__((always_inline)) INLINE static void
 runner_iact_nonsym_bh_gas_density(
-    const float r2, const float *dx, const float hi, const float hj,
+    const float r2, const float dx[3], const float hi, const float hj,
     struct bpart *bi, const struct part *pj, const struct xpart *xpj,
     const int with_cosmology, const struct cosmology *cosmo,
     const struct gravity_props *grav_props,
@@ -102,6 +102,17 @@ runner_iact_nonsym_bh_gas_density(
   /* Contribution to the smoothed sound speed */
   bi->sound_speed_gas += mj * cj * wi;
 
+  if (cj * cosmo->a_factor_sound_speed > bh_props->sound_speed_hot_gas_min) {
+    bi->sound_speed_gas_hot += mj * cj * wi;
+    bi->rho_gas_hot += mj * wi;
+  }
+
+  /* Neighbour internal energy */
+  const float uj = hydro_get_drifted_comoving_internal_energy(pj);
+
+  /* Contribution to the smoothed internal energy */
+  bi->internal_energy_gas += mj * uj * wi;
+
   /* Neighbour's (drifted) velocity in the frame of the black hole
    * (we do include a Hubble term) */
   const float dv[3] = {pj->v[0] - bi->v[0], pj->v[1] - bi->v[1],
@@ -293,7 +304,7 @@ runner_iact_nonsym_bh_gas_density(
       /* Check if the relative velocity is a significant fraction of the jet
          launching velocity. If it is, set the ray correction variable to some
          arbitrarily large value. */
-      if (relative_velocity > 0.3 * bi->v_jet) {
+      if (relative_velocity > 0.8 * bi->v_jet) {
         ray_jet_correction = 1e11 * bi->h;
       }
 
@@ -301,9 +312,7 @@ runner_iact_nonsym_bh_gas_density(
          the order closest --> farthest.  */
       if (cosine_theta < 0) {
         quantity_to_minimize = r + ray_jet_correction;
-        quantity_to_minimize_pos = 1e8 * r + ray_jet_correction;
       } else {
-        quantity_to_minimize = 1e8 * r + ray_jet_correction;
         quantity_to_minimize_pos = r + ray_jet_correction;
       }
       break;
@@ -313,7 +322,7 @@ runner_iact_nonsym_bh_gas_density(
       /* Check if the relative velocity is a significant fraction of the jet
          launching velocity. If it is, set the ray correction variable to some
          arbitrarily large value. */
-      if (relative_velocity > 0.3 * bi->v_jet) {
+      if (relative_velocity > 0.8 * bi->v_jet) {
         ray_jet_correction = 1e13 * 1. / bi->h;
       }
 
@@ -321,9 +330,7 @@ runner_iact_nonsym_bh_gas_density(
          the order farthest --> closest  */
       if (cosine_theta < 0) {
         quantity_to_minimize = r_inv + ray_jet_correction;
-        quantity_to_minimize_pos = 1e8 * r_inv + ray_jet_correction;
       } else {
-        quantity_to_minimize = 1e8 * r_inv + ray_jet_correction;
         quantity_to_minimize_pos = r_inv + ray_jet_correction;
       }
       break;
@@ -333,7 +340,7 @@ runner_iact_nonsym_bh_gas_density(
       /* Check if the relative velocity is a significant fraction of the jet
          launching velocity. If it is, set the ray correction variable to some
          arbitrarily large value. */
-      if (relative_velocity > 0.3 * bi->v_jet) {
+      if (relative_velocity > 0.8 * bi->v_jet) {
         ray_jet_correction = 1e3;
       }
 
@@ -341,8 +348,11 @@ runner_iact_nonsym_bh_gas_density(
          vector of the particle (relative to the BH) and the spin vector of the
          BH. I.e. we launch particles along the spin axis, regardless of the
          distances from the BH. */
-      quantity_to_minimize = cosine_theta + ray_jet_correction;
-      quantity_to_minimize_pos = -cosine_theta + ray_jet_correction;
+      if (cosine_theta < 0) {
+        quantity_to_minimize = cosine_theta + ray_jet_correction;
+      } else {
+        quantity_to_minimize_pos = -cosine_theta + ray_jet_correction;
+      }
       break;
     }
     case AGN_jet_minimum_density_model: {
@@ -350,7 +360,7 @@ runner_iact_nonsym_bh_gas_density(
       /* Check if the relative velocity is a significant fraction of the jet
          launching velocity. If it is, set the ray correction variable to some
          arbitrarily large value. */
-      if (relative_velocity > 0.3 * bi->v_jet) {
+      if (relative_velocity > 0.8 * bi->v_jet) {
         ray_jet_correction = 1e15 * pj->rho;
       }
 
@@ -358,19 +368,18 @@ runner_iact_nonsym_bh_gas_density(
          low-density gas.  */
       if (cosine_theta < 0) {
         quantity_to_minimize = pj->rho + ray_jet_correction;
-        quantity_to_minimize_pos = 1e8 * pj->rho + ray_jet_correction;
       } else {
-        quantity_to_minimize = 1e8 * pj->rho + ray_jet_correction;
         quantity_to_minimize_pos = pj->rho + ray_jet_correction;
       }
       break;
     }
   }
 
-  /* Loop over rays and do the actual minimization */
-  for (int i = 0; i < spinjet_blackhole_number_of_rays; i++) {
+  /* Do the actual minimization. */
+  if (cosine_theta < 0) {
     ray_minimise_distance(quantity_to_minimize, bi->rays_jet, arr_size_jet,
                           gas_id, pj->mass);
+  } else {
     ray_minimise_distance(quantity_to_minimize_pos, bi->rays_jet_pos,
                           arr_size_jet, gas_id, pj->mass);
   }
@@ -398,7 +407,7 @@ runner_iact_nonsym_bh_gas_density(
  */
 __attribute__((always_inline)) INLINE static void
 runner_iact_nonsym_bh_gas_repos(
-    const float r2, const float *dx, const float hi, const float hj,
+    const float r2, const float dx[3], const float hi, const float hj,
     struct bpart *bi, const struct part *pj, const struct xpart *xpj,
     const int with_cosmology, const struct cosmology *cosmo,
     const struct gravity_props *grav_props,
@@ -523,7 +532,7 @@ runner_iact_nonsym_bh_gas_repos(
  */
 __attribute__((always_inline)) INLINE static void
 runner_iact_nonsym_bh_gas_swallow(
-    const float r2, const float *dx, const float hi, const float hj,
+    const float r2, const float dx[3], const float hi, const float hj,
     struct bpart *bi, struct part *pj, struct xpart *xpj,
     const int with_cosmology, const struct cosmology *cosmo,
     const struct gravity_props *grav_props,
@@ -671,8 +680,8 @@ runner_iact_nonsym_bh_gas_swallow(
  * @param ti_current Current integer time value (for random numbers).
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_bh_bh_repos(const float r2, const float *dx, const float hi,
-                               const float hj, struct bpart *bi,
+runner_iact_nonsym_bh_bh_repos(const float r2, const float dx[3],
+                               const float hi, const float hj, struct bpart *bi,
                                struct bpart *bj, const struct cosmology *cosmo,
                                const struct gravity_props *grav_props,
                                const struct black_holes_props *bh_props,
@@ -778,7 +787,7 @@ runner_iact_nonsym_bh_bh_repos(const float r2, const float *dx, const float hi,
  * @param ti_current Current integer time value (for random numbers).
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_bh_bh_swallow(const float r2, const float *dx,
+runner_iact_nonsym_bh_bh_swallow(const float r2, const float dx[3],
                                  const float hi, const float hj,
                                  struct bpart *bi, struct bpart *bj,
                                  const struct cosmology *cosmo,
@@ -891,7 +900,7 @@ runner_iact_nonsym_bh_bh_swallow(const float r2, const float *dx,
  */
 __attribute__((always_inline)) INLINE static void
 runner_iact_nonsym_bh_gas_feedback(
-    const float r2, const float *dx, const float hi, const float hj,
+    const float r2, const float dx[3], const float hi, const float hj,
     const struct bpart *bi, struct part *pj, struct xpart *xpj,
     const int with_cosmology, const struct cosmology *cosmo,
     const struct gravity_props *grav_props,
@@ -1001,22 +1010,20 @@ runner_iact_nonsym_bh_gas_feedback(
       /* Get the (physical) kick velocity, and convert to code units */
       const float vel_kick = sqrtf(2. * delta_u_jet) * cosmo->a;
 
-      /* Compute velocity kick direction using a function for generating a
-       * random unit vector within a cone around the spin vector.*/
+      /* Compute velocity kick direction using the previously generated
+       * jet direction.*/
       float vel_kick_direction[3];
-      random_direction_in_cone(bi->id, pj->id, ti_current,
-                               random_number_BH_kick, bh_props->opening_angle,
-                               bi->angular_momentum_direction,
-                               vel_kick_direction);
 
       /* Include the -1./1. factor (direction) which accounts for kicks in the
        * opposite direction of the spin vector */
-      vel_kick_direction[0] = direction * vel_kick_direction[0];
-      vel_kick_direction[1] = direction * vel_kick_direction[1];
-      vel_kick_direction[2] = direction * vel_kick_direction[2];
+      vel_kick_direction[0] = direction * bi->jet_direction[0];
+      vel_kick_direction[1] = direction * bi->jet_direction[1];
+      vel_kick_direction[2] = direction * bi->jet_direction[2];
 
-      /* Get the initial velocity */
-      const float v_init[3] = {xpj->v_full[0], xpj->v_full[1], xpj->v_full[2]};
+      /* Get the initial velocity in the frame of the black hole */
+      const float v_init[3] = {xpj->v_full[0] - bi->v[0],
+                               xpj->v_full[1] - bi->v[1],
+                               xpj->v_full[2] - bi->v[2]};
 
       /* We compute this final velocity by requiring that the final energy and
        * the inital one differ by the energy received by the particle, i.e.
@@ -1061,10 +1068,11 @@ runner_iact_nonsym_bh_gas_feedback(
           bi->id, pj->id, xpj->v_full[0], xpj->v_full[1], xpj->v_full[2]);
 #endif
 
-      /* Store the jet energy */
+      /* Store the jet energy and other variables of interest */
       const double delta_energy_jet = delta_u_jet * hydro_get_mass(pj);
       tracers_after_jet_feedback(pj, xpj, with_cosmology, cosmo->a, time,
-                                 delta_energy_jet);
+                                 delta_energy_jet, vel_kick, bi->accretion_mode,
+                                 bi->id);
 
       /* Impose maximal viscosity */
       hydro_diffusive_feedback_reset(pj);
diff --git a/src/black_holes/SPIN_JET/black_holes_io.h b/src/black_holes/SPIN_JET/black_holes_io.h
index 56851bf06b33f834f14c550401e15c398b033eee..6fc5f789649a13681e822b7ca76e158f331e899f 100644
--- a/src/black_holes/SPIN_JET/black_holes_io.h
+++ b/src/black_holes/SPIN_JET/black_holes_io.h
@@ -22,6 +22,7 @@
 
 #include "adiabatic_index.h"
 #include "black_holes_part.h"
+#include "black_holes_properties.h"
 #include "io_properties.h"
 
 /**
@@ -162,6 +163,18 @@ INLINE static void convert_bpart_gas_velocity_curl(const struct engine* e,
   ret[2] = bp->curl_v_gas[2] * cosmo->a2_inv;
 }
 
+INLINE static void convert_bpart_gas_temperatures(const struct engine* e,
+                                                  const struct bpart* bp,
+                                                  float* ret) {
+
+  const struct black_holes_props* props = e->black_holes_properties;
+  const struct cosmology* cosmo = e->cosmology;
+
+  /* Conversion from specific internal energy to temperature */
+  ret[0] = bp->internal_energy_gas * cosmo->a_factor_internal_energy /
+           props->temp_to_u_factor;
+}
+
 /**
  * @brief Specifies which b-particle fields to write to a dataset
  *
@@ -176,7 +189,7 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
                                                const int with_cosmology) {
 
   /* Say how much we want to write */
-  *num_fields = 56;
+  *num_fields = 63;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_bpart(
@@ -192,9 +205,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       io_make_output_field("DynamicalMasses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                            bparts, mass, "Dynamical masses of the particles");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           bparts, id, "Unique ID of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, bparts, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
 
   list[4] = io_make_output_field(
       "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, bparts, h,
@@ -205,9 +218,10 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
                                  "Subgrid masses of the particles");
 
   if (with_cosmology) {
-    list[6] = io_make_output_field(
+    list[6] = io_make_physical_output_field(
         "FormationScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-        formation_scale_factor, "Scale-factors at which the BHs were formed");
+        formation_scale_factor, /*can convert to comoving=*/0,
+        "Scale-factors at which the BHs were formed");
   } else {
     list[6] = io_make_output_field("FormationTimes", FLOAT, 1, UNIT_CONV_TIME,
                                    0.f, bparts, formation_time,
@@ -223,9 +237,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       -1.5f * hydro_gamma_minus_one, bparts, sound_speed_gas,
       "Co-moving sound-speeds of the gas around the particles");
 
-  list[9] = io_make_output_field(
+  list[9] = io_make_physical_output_field(
       "EnergyReservoirs", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts,
-      energy_reservoir,
+      energy_reservoir, /*can convert to comoving=*/0,
       "Physcial energy contained in the feedback reservoir of the particles");
 
   list[10] = io_make_output_field(
@@ -240,22 +254,23 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "birth. This does not include any mass accreted onto any merged black "
       "holes.");
 
-  list[12] = io_make_output_field(
+  list[12] = io_make_physical_output_field(
       "CumulativeNumberOfSeeds", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      cumulative_number_seeds,
+      cumulative_number_seeds, /*can convert to comoving=*/1,
       "Total number of BH seeds that have merged into this black hole");
 
-  list[13] =
-      io_make_output_field("NumberOfMergers", INT, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           bparts, number_of_mergers,
-                           "Number of mergers the black holes went through. "
-                           "This does not include the number of mergers "
-                           "accumulated by any merged black hole.");
+  list[13] = io_make_physical_output_field(
+      "NumberOfMergers", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
+      number_of_mergers, /*can convert to comoving=*/1,
+      "Number of mergers the black holes went through. "
+      "This does not include the number of mergers "
+      "accumulated by any merged black hole.");
 
   if (with_cosmology) {
-    list[14] = io_make_output_field(
+    list[14] = io_make_physical_output_field(
         "LastHighEddingtonFractionScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS,
         0.f, bparts, last_high_Eddington_fraction_scale_factor,
+        /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last reached a large Eddington "
         "ratio. -1 if never reached.");
   } else {
@@ -267,32 +282,32 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
   }
 
   if (with_cosmology) {
-    list[15] = io_make_output_field(
+    list[15] = io_make_physical_output_field(
         "LastMinorMergerScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
-        bparts, last_minor_merger_scale_factor,
+        bparts, last_minor_merger_scale_factor, /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last had a minor merger.");
   } else {
     list[15] = io_make_output_field(
-        "LastMinorMergerScaleTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts,
+        "LastMinorMergerTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts,
         last_minor_merger_time,
         "Times at which the black holes last had a minor merger.");
   }
 
   if (with_cosmology) {
-    list[16] = io_make_output_field(
+    list[16] = io_make_physical_output_field(
         "LastMajorMergerScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
-        bparts, last_major_merger_scale_factor,
+        bparts, last_major_merger_scale_factor, /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last had a major merger.");
   } else {
     list[16] = io_make_output_field(
-        "LastMajorMergerScaleTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts,
+        "LastMajorMergerTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts,
         last_major_merger_time,
         "Times at which the black holes last had a major merger.");
   }
 
-  list[17] = io_make_output_field(
+  list[17] = io_make_physical_output_field(
       "SwallowedAngularMomenta", FLOAT, 3, UNIT_CONV_ANGULAR_MOMENTUM, 0.f,
-      bparts, swallowed_angular_momentum,
+      bparts, swallowed_angular_momentum, /*can convert to comoving=*/0,
       "Physical angular momenta that the black holes have accumulated by "
       "swallowing gas particles.");
 
@@ -315,30 +330,30 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       io_make_output_field("TimeBins", CHAR, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
                            time_bin, "Time-bins of the particles");
 
-  list[21] = io_make_output_field(
+  list[21] = io_make_physical_output_field(
       "NumberOfSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_gas_swallows,
+      number_of_gas_swallows, /*can convert to comoving=*/1,
       "Number of gas particles the black holes have swallowed. "
       "This includes the particles swallowed by any of the black holes that "
       "merged into this one.");
 
-  list[22] = io_make_output_field(
+  list[22] = io_make_physical_output_field(
       "NumberOfDirectSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_direct_gas_swallows,
+      number_of_direct_gas_swallows, /*can convert to comoving=*/1,
       "Number of gas particles the black holes have swallowed. "
       "This does not include any particles swallowed by any of the black holes "
       "that merged into this one.");
 
-  list[23] = io_make_output_field(
+  list[23] = io_make_physical_output_field(
       "NumberOfRepositions", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_repositions,
+      number_of_repositions, /*can convert to comoving=*/1,
       "Number of repositioning events the black holes went through. This does "
       "not include the number of reposition events accumulated by any merged "
       "black holes.");
 
-  list[24] = io_make_output_field(
+  list[24] = io_make_physical_output_field(
       "NumberOfRepositionAttempts", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_reposition_attempts,
+      number_of_reposition_attempts, /*can convert to comoving=*/1,
       "Number of time steps in which the black holes had an eligible particle "
       "to reposition to. They may or may not have ended up moving there, "
       "depending on their subgrid mass and on whether these particles were at "
@@ -346,9 +361,9 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "not include attempted repositioning events accumulated by any merged "
       "black holes.");
 
-  list[25] = io_make_output_field(
+  list[25] = io_make_physical_output_field(
       "NumberOfTimeSteps", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      number_of_time_steps,
+      number_of_time_steps, /*can convert to comoving=*/1,
       "Total number of time steps at which the black holes were active.");
 
   list[26] = io_make_output_field(
@@ -369,36 +384,37 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       convert_bpart_gas_velocity_curl,
       "Velocity curl (3D) of the gas particles around the black holes.");
 
-  list[29] = io_make_output_field(
+  list[29] = io_make_physical_output_field(
       "AccretedAngularMomenta", FLOAT, 3, UNIT_CONV_ANGULAR_MOMENTUM, 0.f,
-      bparts, accreted_angular_momentum,
+      bparts, accreted_angular_momentum, /*can convert to comoving=*/0,
       "Physical angular momenta that the black holes have accumulated through "
       "subgrid accretion.");
 
-  list[30] = io_make_output_field(
+  list[30] = io_make_physical_output_field(
       "NumberOfGasNeighbours", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      num_ngbs,
+      num_ngbs, /*can convert to comoving=*/1,
       "Integer number of gas neighbour particles within the black hole "
       "kernels.");
 
-  list[31] = io_make_output_field(
+  list[31] = io_make_physical_output_field(
       "NumberOfHeatingEvents", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      AGN_number_of_energy_injections,
+      AGN_number_of_energy_injections, /*can convert to comoving=*/1,
       "Integer number of (thermal) energy injections the black hole has had "
       "so far. This counts each heated gas particle separately, and so can "
       "increase by more than one during a single time step.");
 
-  list[32] = io_make_output_field(
+  list[32] = io_make_physical_output_field(
       "NumberOfAGNEvents", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      AGN_number_of_AGN_events,
-      "Integer number of AGN events the black hole has had so far"
-      " (the number of time steps the BH did AGN feedback)");
+      AGN_number_of_AGN_events, /*can convert to comoving=*/1,
+      "Integer number of heating AGN events the black hole has had so far"
+      " (the number of time steps the BH did thermal AGN feedback)");
 
   if (with_cosmology) {
-    list[33] = io_make_output_field(
+    list[33] = io_make_physical_output_field(
         "LastAGNFeedbackScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
-        bparts, last_AGN_event_scale_factor,
-        "Scale-factors at which the black holes last had an AGN event.");
+        bparts, last_AGN_event_scale_factor, /*can convert to comoving=*/0,
+        "Scale-factors at which the black holes last had a thermal AGN "
+        "event.");
   } else {
     list[33] = io_make_output_field(
         "LastAGNFeedbackTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts,
@@ -412,11 +428,11 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "Accretion-limited time steps of black holes. The actual time step of "
       "the particles may differ due to the minimum allowed value.");
 
-  list[35] = io_make_output_field(
+  list[35] = io_make_physical_output_field(
       "AGNTotalInjectedEnergies", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts,
-      AGN_cumulative_energy,
+      AGN_cumulative_energy, /*can convert to comoving=*/0,
       "Total (cumulative) physical energies injected into gas particles "
-      "in AGN feedback.");
+      "in AGN feedback, including the effects of both radiation and winds.");
 
   list[36] = io_make_output_field_convert_bpart(
       "Potentials", FLOAT, 1, UNIT_CONV_POTENTIAL, -1.f, bparts,
@@ -424,8 +440,8 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
 
   list[37] = io_make_output_field(
       "Spins", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts, spin,
-      "Dimensionless spins of the black holes. "
-      "Negative values indicate retrograde accretion.");
+      "Dimensionless spins of the black holes. Negative values indicate "
+      "retrograde accretion.");
 
   list[38] = io_make_output_field(
       "AngularMomentumDirections", FLOAT, 3, UNIT_CONV_NO_UNITS, 0.f, bparts,
@@ -433,102 +449,153 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "Direction of the black hole spin vector, normalised to unity.");
 
   list[39] = io_make_output_field(
-      "AccretionDiscAspectRatios", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      aspect_ratio,
-      "The aspect ratio, h/r, of the subgrid accretion disc "
-      "around the black hole.");
-
-  list[40] = io_make_output_field(
       "JetEfficiencies", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
       jet_efficiency, "Jet power divided by accretion rate.");
 
-  list[41] = io_make_output_field(
+  list[40] = io_make_output_field(
       "RadiativeEfficiencies", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
       radiative_efficiency, "AGN luminosity divided by accretion rate.");
 
-  list[42] = io_make_output_field("CosAccretionDiskAngle", FLOAT, 1,
+  list[41] = io_make_output_field("CosAccretionDiskAngle", FLOAT, 1,
                                   UNIT_CONV_NO_UNITS, 0.f, bparts,
                                   accretion_disk_angle,
                                   "Cosine of the angle between the spin vector "
                                   "and the accreting gas angular momentum.");
 
-  list[43] = io_make_output_field(
+  list[42] = io_make_output_field(
       "AccretionModes", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts, accretion_mode,
       "Accretion flow regime. 0 - Thick disk, 1 - Thin disk, 2 - Slim disk");
 
-  list[44] = io_make_output_field(
+  list[43] = io_make_output_field(
       "JetReservoir", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts, jet_reservoir,
       "Total jet energy waiting to be released (once it "
       "grows large enough to kick a single particle).");
 
-  list[45] = io_make_output_field(
+  list[44] = io_make_physical_output_field(
       "InjectedJetEnergies", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts,
-      total_jet_energy, "Total jet energy injected into AGN surroundings.");
+      total_jet_energy, /*can convert to comoving=*/0,
+      "Total jet energy injected into AGN surroundings.");
 
-  list[46] = io_make_output_field(
+  list[45] = io_make_output_field(
       "JetTimeSteps", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts, dt_jet,
       "Jet-launching-limited time-steps of black holes.");
 
-  list[47] = io_make_output_field(
+  list[46] = io_make_physical_output_field(
       "NumberOfJetParticlesLaunched", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      AGN_number_of_jet_injections,
+      AGN_number_of_jet_injections, /*can convert to comoving=*/1,
       "Integer number of (kinetic) energy injections the black hole has had "
       "so far");
 
-  list[48] = io_make_output_field(
+  list[47] = io_make_physical_output_field(
       "NumberOfAGNJetEvents", INT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      AGN_number_of_AGN_jet_events,
+      AGN_number_of_AGN_jet_events, /*can convert to comoving=*/1,
       "Integer number of AGN jet launching events the black hole has had"
       " (the number of times the BH did AGN jet feedback)");
 
   if (with_cosmology) {
-    list[49] = io_make_output_field(
+    list[48] = io_make_physical_output_field(
         "LastAGNJetScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-        last_AGN_jet_event_scale_factor,
+        last_AGN_jet_event_scale_factor, /*can convert to comoving=*/0,
         "Scale-factors at which the black holes last had an AGN jet event.");
   } else {
-    list[49] = io_make_output_field(
+    list[48] = io_make_output_field(
         "LastAGNJetTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts,
         last_AGN_jet_event_time,
         "Times at which the black holes last had an AGN jet event.");
   }
 
-  list[50] = io_make_output_field(
+  list[49] = io_make_output_field(
       "EddingtonFractions", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
       eddington_fraction,
       "Accretion rates of black holes in units of their Eddington rates. "
       "This is based on the unlimited accretion rates, so these fractions "
       "can be above the limiting fEdd.");
 
-  list[51] = io_make_output_field(
-      "FOFGroupMasses", FLOAT, 1, UNIT_CONV_MASS, 0.f, bparts, group_mass,
-      "Parent halo masses of the black holes, as determined from the FOF  "
-      "algorithm.");
-
-  list[52] =
+  list[50] =
       io_make_output_field("JetVelocities", FLOAT, 1, UNIT_CONV_VELOCITY, 0.f,
                            bparts, v_jet, "The current jet velocities.");
 
-  list[53] = io_make_output_field(
-      "TotalAccretedMassesByMode", FLOAT, 3, UNIT_CONV_MASS, 0.f, bparts,
-      accreted_mass_by_mode,
+  list[51] = io_make_output_field(
+      "TotalAccretedMassesByMode", FLOAT, BH_accretion_modes_count,
+      UNIT_CONV_MASS, 0.f, bparts, accreted_mass_by_mode,
       "The total accreted mass in each accretion mode. The components to the "
       "mass accreted in the thick, thin and slim disc modes, respectively.");
 
-  list[54] = io_make_output_field(
-      "AGNTotalInjectedEnergiesByMode", FLOAT, 3, UNIT_CONV_ENERGY, 0.f, bparts,
-      thermal_energy_by_mode,
-      "The total energy injected in the thermal AGN feedback mode, split by "
+  list[52] = io_make_physical_output_field(
+      "AGNTotalInjectedEnergiesByMode", FLOAT, BH_accretion_modes_count,
+      UNIT_CONV_ENERGY, 0.f, bparts, thermal_energy_by_mode,
+      /*can convert to comoving=*/0,
+      "The total energy injected in the thermal AGN feedback mode, including "
+      "the contributions of both radiation and wind feedback, split by "
       "accretion mode. The components correspond to the thermal energy dumped "
       "in the thick, thin and slim disc modes, respectively.");
 
-  list[55] = io_make_output_field(
-      "InjectedJetEnergiesByMode", FLOAT, 3, UNIT_CONV_ENERGY, 0.f, bparts,
-      jet_energy_by_mode,
+  list[53] = io_make_physical_output_field(
+      "InjectedJetEnergiesByMode", FLOAT, BH_accretion_modes_count,
+      UNIT_CONV_ENERGY, 0.f, bparts, jet_energy_by_mode,
+      /*can convert to comoving=*/0,
       "The total energy injected in the kinetic jet AGN feedback mode, split "
-      "by accretion mode. The components correspond to the thermal energy "
+      "by accretion mode. The components correspond to the jet energy "
+      "dumped in the thick, thin and slim disc modes, respectively.");
+
+  list[54] = io_make_output_field(
+      "WindEfficiencies", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
+      wind_efficiency, "The wind efficiencies of the black holes.");
+
+  list[55] = io_make_physical_output_field(
+      "TotalRadiatedEnergies", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts,
+      radiated_energy, /*can convert to comoving=*/0,
+      "The total energy launched into radiation by the black holes, "
+      "in all accretion modes. ");
+
+  list[56] = io_make_physical_output_field(
+      "RadiatedEnergiesByMode", FLOAT, BH_accretion_modes_count,
+      UNIT_CONV_ENERGY, 0.f, bparts, radiated_energy_by_mode,
+      /*can convert to comoving=*/0,
+      "The total energy launched into radiation by the black holes, split "
+      "by accretion mode. The components correspond to the radiative energy "
       "dumped in the thick, thin and slim disc modes, respectively.");
 
+  list[57] = io_make_physical_output_field(
+      "TotalWindEnergies", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts, wind_energy,
+      /*can convert to comoving=*/0,
+      "The total energy launched into accretion disc winds by the black holes, "
+      "in all accretion modes. ");
+
+  list[58] = io_make_physical_output_field(
+      "WindEnergiesByMode", FLOAT, BH_accretion_modes_count, UNIT_CONV_ENERGY,
+      0.f, bparts, wind_energy_by_mode, /*can convert to comoving=*/0,
+      "The total energy launched into accretion disc winds by the black "
+      "holes, split by accretion mode. The components correspond to the "
+      "radiative energy dumped in the thick, thin and slim disc modes, "
+      "respectively.");
+
+  list[59] = io_make_output_field(
+      "AccretionBoostFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
+      accretion_boost_factor,
+      "Multiplicative factors by which the Bondi-Hoyle-Lyttleton accretion "
+      "rates have been increased by the density-dependent Booth & Schaye "
+      "(2009) accretion model.");
+
+  list[60] = io_make_output_field_convert_bpart(
+      "GasTemperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE, 0.f, bparts,
+      convert_bpart_gas_temperatures,
+      "Temperature of the gas surrounding the black holes.");
+
+  list[61] = io_make_physical_output_field(
+      "LastRepositionVelocities", FLOAT, 1, UNIT_CONV_SPEED, 0.f, bparts,
+      last_repos_vel, /*can convert to comoving=*/0,
+      "Physical speeds at which the black holes repositioned most recently. "
+      "This is 0 for black holes that have never repositioned, or if the "
+      "simulation has been run without prescribed repositioning speed.");
+
+  list[62] = io_make_output_field(
+      "AccretionEfficiencies", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
+      accretion_efficiency,
+      "The accretion efficiencies of black holes. These are used to convert "
+      "from the large-scale accretion rate onto an accretion disc (the raw "
+      "Bondi-like accretion rate) to the accretion rate onto the BH itself.");
+
 #ifdef DEBUG_INTERACTIONS_BLACK_HOLES
 
   list += *num_fields;
diff --git a/src/black_holes/SPIN_JET/black_holes_part.h b/src/black_holes/SPIN_JET/black_holes_part.h
index 712c70332230f77ec5747554bacb37e1cffc866e..99d147e4ea5844ac2a42d8d009292a88f4d19a93 100644
--- a/src/black_holes/SPIN_JET/black_holes_part.h
+++ b/src/black_holes/SPIN_JET/black_holes_part.h
@@ -29,14 +29,6 @@
 #include "rays_struct.h"
 #include "timeline.h"
 
-/*! The possible accretion modes every black hole can take. */
-enum BH_accretion_modes {
-  BH_thick_disc = 0,       /* At low Eddington ratios */
-  BH_thin_disc,            /* At moderate Eddington ratios */
-  BH_slim_disc,            /* Super-Eddington accretion */
-  BH_accretion_modes_count /* Number of possible accretion modes */
-};
-
 /**
  * @brief Particle fields for the black hole particles.
  *
@@ -71,6 +63,9 @@ struct bpart {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   struct {
 
     /* Number of neighbours. */
@@ -107,9 +102,20 @@ struct bpart {
   /*! Density of the gas surrounding the black hole. */
   float rho_gas;
 
+  /*! Density of the gas surrounding the black hole, taking account of only the
+   *  hot particles. */
+  float rho_gas_hot;
+
+  /*! Internal energy of the gas surrounding the black hole. */
+  float internal_energy_gas;
+
   /*! Smoothed sound speed of the gas surrounding the black hole. */
   float sound_speed_gas;
 
+  /*! Smoothed sound speed of the gas surrounding the black hole, taking
+   * account of only the hot particles. */
+  float sound_speed_gas_hot;
+
   /*! Smoothed velocity of the gas surrounding the black hole,
    * in the frame of the black hole (internal units) */
   float velocity_gas[3];
@@ -161,6 +167,9 @@ struct bpart {
    * lower potential than all eligible neighbours) */
   int number_of_reposition_attempts;
 
+  /* Velocity of most recent reposition jump */
+  float last_repos_vel;
+
   /*! Total number of time steps in which the black hole was active. */
   int number_of_time_steps;
 
@@ -170,18 +179,18 @@ struct bpart {
   /*! Total (physical) angular momentum accumulated from subgrid accretion */
   float accreted_angular_momentum[3];
 
-  /*! Integer (cumulative) number of energy injections in AGN feedback. At a
-   * given time-step, an AGN-active BH may produce multiple energy injections.
-   * The number of energy injections is equal to or more than the number of
-   * particles heated by the BH during this time-step. */
+  /*! Integer (cumulative) number of thermal energy injections in AGN feedback.
+   * At a given time-step, an AGN-active BH may produce multiple energy
+   * injections. The number of energy injections is equal to or more than the
+   * number of particles heated by the BH during this time-step. */
   int AGN_number_of_energy_injections;
 
-  /*! Integer (cumulative) number of AGN events. If a BH does feedback at a
-   * given time-step, the number of its AGN events is incremented by 1. Each
-   * AGN event may have multiple energy injections. */
+  /*! Integer (cumulative) number of thermal AGN events. If a BH does feedback
+   * at a given time-step, the number of its AGN events is incremented by 1.
+   * Each AGN event may have multiple energy injections. */
   int AGN_number_of_AGN_events;
 
-  /* Total energy injected into the gas in AGN feedback by this BH */
+  /* Total thermal energy injected into the gas in AGN feedback by this BH */
   float AGN_cumulative_energy;
 
   /*! Union for the last AGN event time and the last AGN event scale factor */
@@ -194,9 +203,15 @@ struct bpart {
     float last_AGN_event_scale_factor;
   };
 
+  /*! Current heating temperature of the BH */
+  float delta_T;
+
   /*! BH accretion-limited time-step */
   float dt_heat;
 
+  /*! Accretion boost factor */
+  float accretion_boost_factor;
+
   /*! Eddington fraction */
   float eddington_fraction;
 
@@ -206,6 +221,9 @@ struct bpart {
   /*! The normalized spin/angular momentum vector of the BH */
   float angular_momentum_direction[3];
 
+  /* The current jet direction. */
+  float jet_direction[3];
+
   /*! The jet efficiency */
   float jet_efficiency;
 
@@ -216,9 +234,6 @@ struct bpart {
       of the gas in the smoothing kernel */
   float accretion_disk_angle;
 
-  /*! Aspect ratio of the subgrid accretion disk */
-  float aspect_ratio;
-
   /*! Which type is the subgrid accretion disk (thick, thin or slim) */
   enum BH_accretion_modes accretion_mode;
 
@@ -231,17 +246,31 @@ struct bpart {
   /*! The current jet kick velocity to be applied */
   float v_jet;
 
-  /*! The energ in the jet reservoir */
+  /*! The energy in the jet reservoir */
   float jet_reservoir;
 
   /*! Total jet energy launched so far */
   float total_jet_energy;
 
+  /*! Efficiency of wind launching */
+  float wind_efficiency;
+
+  /*! Energy launched into radiation */
+  float radiated_energy;
+
+  /*! Energy launched as winds */
+  float wind_energy;
+
+  /*! Accretion efficiency of this BH */
+  float accretion_efficiency;
+
   /*! Total accreted masses, radiated energies and jet energies launched
       by BHs, split by accretion mode */
   float accreted_mass_by_mode[BH_accretion_modes_count];
   float thermal_energy_by_mode[BH_accretion_modes_count];
   float jet_energy_by_mode[BH_accretion_modes_count];
+  float wind_energy_by_mode[BH_accretion_modes_count];
+  float radiated_energy_by_mode[BH_accretion_modes_count];
 
   /*! Total number of jet kicks */
   int AGN_number_of_jet_injections;
@@ -249,9 +278,6 @@ struct bpart {
   /*! Total number of jet kicking events */
   int AGN_number_of_AGN_jet_events;
 
-  /*! Halo mass the black hole is assigned to */
-  float group_mass;
-
   union {
 
     /*! Last time a jet event occurred */
@@ -294,16 +320,17 @@ struct bpart {
   /*! Properties used in the feedback loop to distribute to gas neighbours. */
   struct {
 
-    /*! Energy per unit mass in a single AGN energy-injection event */
+    /*! Thermal energy per unit mass in a single thermal AGN */
+    /* energy-injection event. */
     float AGN_delta_u;
 
-    /*! Number of energy injections per time-step */
+    /*! Number of thermal energy injections per time-step */
     int AGN_number_of_energy_injections;
 
-    /*! Number of energy injections per time-step */
+    /*! Number of thermal energy injections per time-step */
     int AGN_number_of_jet_injections;
 
-    /*! Change in energy from SNII feedback energy injection */
+    /*! Change in energy from thermal AGN energy injection */
     float AGN_delta_u_jet;
 
   } to_distribute;
diff --git a/src/black_holes/SPIN_JET/black_holes_properties.h b/src/black_holes/SPIN_JET/black_holes_properties.h
index fd01037c5d92d3ea2d72f984ccfc2b3db67c0b58..c0c7099f617a995d5d901cdba44187484e072ad8 100644
--- a/src/black_holes/SPIN_JET/black_holes_properties.h
+++ b/src/black_holes/SPIN_JET/black_holes_properties.h
@@ -29,6 +29,11 @@ enum AGN_feedback_models {
   AGN_minimum_distance_model /*< Minimum-distance model of AGN feedback */
 };
 
+enum AGN_heating_temperature_models {
+  AGN_heating_temperature_constant, /*< Use a constant delta_T */
+  AGN_heating_temperature_local,    /*< Variable delta_T */
+};
+
 enum AGN_jet_feedback_models {
   AGN_jet_minimum_distance_model, /*< Minimum-distance model of AGN feedback */
   AGN_jet_maximum_distance_model, /*< Maximum-distance model of AGN feedback */
@@ -42,12 +47,11 @@ enum AGN_jet_velocity_models {
   AGN_jet_velocity_BH_mass,      /*< Scale the jet velocity with BH mass */
   AGN_jet_velocity_mass_loading, /*< Assume constant mass loading */
   AGN_jet_velocity_local, /*< Scale the jet velocity such that particles clear
-                              the kernel exactly when a new one is launched */
-  AGN_jet_velocity_sound_speed, /*< Scale the jet velocity such that the
-                                    BH kernel is replenished on the same
-                                    time-scale as it is evacuated by jets */
-  AGN_jet_velocity_halo_mass    /*< Scale the jet velocity with halo mass
-                                    (NOT WORKING) */
+                              the kernel exactly when a new one is launched,
+                              as well as making sure that the kernel never
+                              runs out of particles */
+  AGN_jet_velocity_halo_mass /*< Scale the jet velocity with halo mass
+                                 (NOT WORKING) */
 };
 
 enum BH_merger_thresholds {
@@ -61,6 +65,11 @@ enum thin_disc_regions {
   TD_region_C  /*< Region C from Shakura & Sunyaev (1973) */
 };
 
+enum accretion_efficiency_modes {
+  BH_accretion_efficiency_constant, /*< Single number */
+  BH_accretion_efficiency_variable  /*< Scaling with Eddington ratio */
+};
+
 /**
  * @brief Properties of black holes and AGN feedback in the EAGEL model.
  */
@@ -154,9 +163,31 @@ struct black_holes_props {
   /*! Feedback coupling efficiency of the black holes. */
   float epsilon_f;
 
-  /*! Temperature increase induced by AGN feedback (Kelvin) */
+  /*! The type of jet velocity scaling to use. */
+  enum AGN_heating_temperature_models AGN_heating_temperature_model;
+
+  /*! Temperature increase induced by AGN feedback (Kelvin),
+      in the constant-temperature case */
   float AGN_delta_T_desired;
 
+  /* Numerical factor by which we rescale the variable delta_T formula,
+     fiducial value is 1 */
+  float delta_T_xi;
+
+  /* The minimum heating temperature to apply in the case of variable feedback,
+     expressed in Kelvin */
+  float delta_T_min;
+
+  /* The maximum heating temperature to apply in the case of variable feedback,
+     expressed in Kelvin */
+  float delta_T_max;
+
+  /* Constants used to parametrise the Dalla Vecchia & Schaye (2012)
+   condition - Eqn 18. */
+  float normalisation_Dalla_Vecchia;
+  float ref_ngb_mass_Dalla_Vecchia;
+  float ref_density_Dalla_Vecchia;
+
   /*! Number of gas neighbours to heat in a feedback event */
   float num_ngbs_to_heat;
 
@@ -297,11 +328,6 @@ struct black_holes_props {
                      free-free absorption dominates the opacity */
   enum thin_disc_regions TD_region;
 
-  /*! Parameter controlling when thin disk transitions into slim disk. This
-      occurs when the radiative efficiency of the slim disk falls below
-      TD_SD_eps_r_threshold times the radiative efficiency of the thin disk. */
-  float TD_SD_eps_r_threshold;
-
   /* ---- Jet feedback - related parameters ---------- */
 
   /*! Global switch for whether to include jets [1] or not [0]. */
@@ -311,28 +337,13 @@ struct black_holes_props {
    */
   int turn_off_radiative_feedback;
 
-  /*! Global switch for whether to turn off radiation in the thick disk and
-      jets in the thin disk [1] or not [0] */
-  int turn_off_secondary_feedback;
-
   /* Whether we want to include super-Eddington accretion, modeled as the slim
      disk */
   int include_slim_disk;
 
-  /* Whether to use GRMHD fits for the spindown rate due to jets */
-  int include_GRMHD_spindown;
-
-  /* Whether to include the expected suppression of the accretion rate due to
-   * ADIOS winds in the thick disk regime */
-  int include_ADIOS_suppression;
-
-  /* The inner radius in the accretion rate - R relation, if ADIOS suppression
-   * is included */
-  float ADIOS_R_in;
-
-  /* The slope of the accretion rate - R relation, if ADIOS suppression
-   * is included */
-  float ADIOS_s;
+  /* Whether or not to use jets from the thin disc regime (at moderate
+   * Eddington ratios. */
+  int use_jets_in_thin_disc;
 
   /*! Whether to fix the radiative efficiency to some value [1] or not [0]. */
   int fix_radiative_efficiency;
@@ -351,10 +362,6 @@ struct black_holes_props {
   /*! Jet velocity if the constant velocity model is used */
   float v_jet;
 
-  /*! Parameters of the scaling between AGN jet velocity and BH mass */
-  float v_jet_BH_mass_scaling_reference_mass;
-  float v_jet_BH_mass_scaling_slope;
-
   /*! Sets the launching velocity of the jet to v_jet_cs_ratio times the
       sound speed of the hot gas in the halo, assuming it is at virial
       temperature. This is used if the launching model is BH_mass or
@@ -369,30 +376,70 @@ struct black_holes_props {
       sound_speed launching models are used. */
   float v_jet_xi;
 
+  /*! The reference BH mass to use in the case that we employ a BH mass scaling
+   * for the jet velocity. */
+  float v_jet_BH_mass_scaling_reference_mass;
+
+  /*! The power law slope to use in the case that we employ a BH mass scaling
+   * for the jet velocity. */
+  float v_jet_BH_mass_scaling_slope;
+
   /*! The minimal jet velocity to use in the variable-velocity models */
   float v_jet_min;
 
+  /*! The maximal jet velocity to use in the variable-velocity models */
+  float v_jet_max;
+
+  /*! The minimum sound speed of hot gas to count when calculating the
+   *  smoothed sound speed of gas in the kernel */
+  float sound_speed_hot_gas_min;
+
   /*! The effective (half-)opening angle of the jet. */
   float opening_angle;
 
-  /*! The slope of the dependence of jet efficiency on aspect ratio of the
-      subgrid accretion disk, H/R. Default value is 1, and another reasonable
-      value is 0 (same jet efficiency for all disks). */
-  float jet_h_r_slope;
-
   /*! The coupling efficiency for jet feedback. */
   float eps_f_jet;
 
-  /*! Whether to fix the jet efficiency to some value [1] or not [0]. If yes,
-      the jets will be pointed along the z-axis. */
+  /*! Whether to fix the jet efficiency to some value [1] or not [0]. */
   int fix_jet_efficiency;
 
   /*! The jet efficiency to use if fix_jet_efficiency is 1. */
   float jet_efficiency;
 
+  /* Whether to fix the jet directions to be along the z-axis. */
+  int fix_jet_direction;
+
+  /*! The accretion efficiency (suppression of accretion rate) to use in
+   *  the thick disc regime (at low Eddington ratios). */
+  float accretion_efficiency_thick;
+
+  /*! The accretion efficiency (suppression of accretion rate) to use in
+   *  the slim disc regime (at super-Eddington ratios). */
+  float accretion_efficiency_slim;
+
+  /*! Exponent to use for scaling of accretion efficiency with transition
+   *  radius in the thick disc. */
+  float ADIOS_s;
+
+  /*! The radius of the thick inner ADIOS disc at the critical transition
+   *  Eddington ratio, in gravitational units */
+  float ADIOS_R_in;
+
+  /* Whether or not we want to use wind feedback in the ADAF/ADIOS regime
+     (at low Eddington ratios). */
+  int use_ADIOS_winds;
+
+  /* The factor by which we multiply the slim disc wind efficiency - 0 meaning
+     no winds, and 1 meaning full winds. */
+  float slim_disc_wind_factor;
+
   /*! The jet launching scheme to use: minimum distance,
       maximum distance, closest to spin axis or minimum density. */
   enum AGN_jet_feedback_models jet_feedback_model;
+
+  /*! The accretion efficiency mode to use: constant or variable
+   * (Eddington-ratio dependent) . */
+  enum accretion_efficiency_modes accretion_efficiency_mode;
 };
 
 /**
@@ -530,15 +577,92 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
       params, "SPINJETAGN:AGN_use_deterministic_feedback", 1);
   bp->epsilon_f =
       parser_get_param_float(params, "SPINJETAGN:coupling_efficiency");
-  bp->AGN_delta_T_desired =
-      parser_get_param_float(params, "SPINJETAGN:AGN_delta_T_K");
-  /* Check that it makes sense. */
-  if (bp->AGN_delta_T_desired <= 0.f)
-    error("The AGN heating temperature delta T must be > 0 K, not %.5e K.",
-          bp->AGN_delta_T_desired);
 
-  bp->AGN_delta_T_desired /=
-      units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+  /* Common conversion factors ----------------------------- */
+
+  /* Calculate temperature to internal energy conversion factor (all internal
+   * units) */
+  const double k_B = phys_const->const_boltzmann_k;
+  const double m_p = phys_const->const_proton_mass;
+  const double mu = hydro_props->mu_ionised;
+  bp->temp_to_u_factor = k_B / (mu * hydro_gamma_minus_one * m_p);
+
+  /* ---- Black hole time-step properties ------------------ */
+
+  char temp2[PARSER_MAX_LINE_SIZE];
+  parser_get_param_string(params, "SPINJETAGN:AGN_delta_T_model", temp2);
+  if (strcmp(temp2, "Constant") == 0) {
+    bp->AGN_heating_temperature_model = AGN_heating_temperature_constant;
+
+    bp->AGN_delta_T_desired =
+        parser_get_param_float(params, "SPINJETAGN:AGN_delta_T_K");
+    /* Check that it makes sense. */
+
+    if (bp->AGN_delta_T_desired <= 0.f)
+      error("The AGN heating temperature delta T must be > 0 K, not %.5e K.",
+            bp->AGN_delta_T_desired);
+
+    bp->AGN_delta_T_desired /=
+        units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+
+  } else if (strcmp(temp2, "Local") == 0) {
+    bp->AGN_heating_temperature_model = AGN_heating_temperature_local;
+
+    bp->delta_T_xi = parser_get_param_float(params, "SPINJETAGN:delta_T_xi");
+
+    bp->delta_T_min =
+        parser_get_param_float(params, "SPINJETAGN:delta_T_min_K");
+
+    /* Check that minimum temperature makes sense */
+    if (bp->delta_T_min <= 0.f)
+      error(
+          "The minimum AGN heating temperature delta T must be > 0 K, "
+          "not %.5e K.",
+          bp->delta_T_min);
+
+    /* Convert to internal units */
+    bp->delta_T_min /= units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+
+    bp->delta_T_max =
+        parser_get_param_float(params, "SPINJETAGN:delta_T_max_K");
+
+    /* Check that minimum temperature makes sense */
+    if (bp->delta_T_max <= 0.f)
+      error(
+          "The maximum AGN heating temperature delta T must be > 0 K, "
+          "not %.5e K.",
+          bp->delta_T_max);
+
+    /* Convert to internal units */
+    bp->delta_T_max /= units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+
+    float temp_hot_gas_min =
+        parser_get_param_float(params, "SPINJETAGN:temperature_hot_gas_min_K");
+
+    temp_hot_gas_min /= units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+
+    bp->sound_speed_hot_gas_min =
+        sqrtf(hydro_gamma * hydro_gamma_minus_one * temp_hot_gas_min *
+              bp->temp_to_u_factor);
+
+    /* Define constants used to parametrise the Dalla Vecchia & Schaye (2012)
+       condition - Eqn 18. */
+    bp->normalisation_Dalla_Vecchia = 1.8e6;
+    bp->normalisation_Dalla_Vecchia /=
+        units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+    bp->ref_ngb_mass_Dalla_Vecchia = 1e6 * 60. * phys_const->const_solar_mass;
+
+    /* This is nH = 0.1 cm^-3, convert to physical density */
+    bp->ref_density_Dalla_Vecchia =
+        0.1 * mu * m_p /
+        units_cgs_conversion_factor(us, UNIT_CONV_NUMBER_DENSITY);
+
+  } else {
+    error(
+        "The AGN heating temperature model must be Constant or SoundSpeed,"
+        " not %s",
+        temp2);
+  }
 
   bp->num_ngbs_to_heat =
       parser_get_param_float(params, "SPINJETAGN:AGN_num_ngb_to_heat");
@@ -606,34 +730,23 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
   bp->major_merger_threshold =
       parser_get_param_float(params, "SPINJETAGN:threshold_major_merger");
 
-  char temp2[PARSER_MAX_LINE_SIZE];
-  parser_get_param_string(params, "SPINJETAGN:merger_threshold_type", temp2);
-  if (strcmp(temp2, "CircularVelocity") == 0)
+  char temp3[PARSER_MAX_LINE_SIZE];
+  parser_get_param_string(params, "SPINJETAGN:merger_threshold_type", temp3);
+  if (strcmp(temp3, "CircularVelocity") == 0)
     bp->merger_threshold_type = BH_mergers_circular_velocity;
-  else if (strcmp(temp2, "EscapeVelocity") == 0)
+  else if (strcmp(temp3, "EscapeVelocity") == 0)
     bp->merger_threshold_type = BH_mergers_escape_velocity;
-  else if (strcmp(temp2, "DynamicalEscapeVelocity") == 0)
+  else if (strcmp(temp3, "DynamicalEscapeVelocity") == 0)
     bp->merger_threshold_type = BH_mergers_dynamical_escape_velocity;
   else
     error(
         "The BH merger model must be either CircularVelocity, EscapeVelocity, "
         "or DynamicalEscapeVelocity, not %s",
-        temp2);
+        temp3);
 
   bp->max_merging_distance_ratio =
       parser_get_param_float(params, "SPINJETAGN:merger_max_distance_ratio");
 
-  /* Common conversion factors ----------------------------- */
-
-  /* Calculate temperature to internal energy conversion factor (all internal
-   * units) */
-  const double k_B = phys_const->const_boltzmann_k;
-  const double m_p = phys_const->const_proton_mass;
-  const double mu = hydro_props->mu_ionised;
-  bp->temp_to_u_factor = k_B / (mu * hydro_gamma_minus_one * m_p);
-
-  /* ---- Black hole time-step properties ------------------ */
-
   const double yr_in_cgs = 365.25 * 24. * 3600.;
   bp->time_step_min =
       parser_get_param_float(params, "SPINJETAGN:minimum_timestep_yr") *
@@ -673,9 +786,10 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
         bp->seed_spin);
   }
 
-  /* Calculate the critical transition accretion rate between the thick and
+  /* The critical transition accretion rate between the thick and
      thin disk regimes. */
-  bp->mdot_crit_ADAF = 0.2 * bp->alpha_acc_2;
+  bp->mdot_crit_ADAF =
+      parser_get_param_float(params, "SPINJETAGN:mdot_crit_ADAF");
 
   /* Calculate the gas-to-total pressure ratio as based on simulations
      (see Yuan & Narayan 2014) */
@@ -719,14 +833,14 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
   bp->xi_TD = 2. * (1. + 7. * bp->alpha_acc_2) / (4. + bp->alpha_acc_2) /
               bp->alpha_acc_2;
 
-  char temp3[PARSER_MAX_LINE_SIZE];
-  parser_get_param_string(params, "SPINJETAGN:TD_region", temp3);
-  if (strcmp(temp3, "B") == 0)
+  char temp4[PARSER_MAX_LINE_SIZE];
+  parser_get_param_string(params, "SPINJETAGN:TD_region", temp4);
+  if (strcmp(temp4, "B") == 0)
     bp->TD_region = TD_region_B;
-  else if (strcmp(temp3, "C") == 0)
+  else if (strcmp(temp4, "C") == 0)
     bp->TD_region = TD_region_C;
   else
-    error("The choice of thin disc region must be B or C, not %s", temp3);
+    error("The choice of thin disc region must be B or C, not %s", temp4);
 
   /* ---- Jet feedback - related parameters ---------- */
   bp->include_jets = parser_get_param_int(params, "SPINJETAGN:include_jets");
@@ -754,17 +868,6 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
         "least one of the two feedback modes must be turned on.");
   }
 
-  bp->turn_off_secondary_feedback =
-      parser_get_param_int(params, "SPINJETAGN:turn_off_secondary_feedback");
-
-  if ((bp->turn_off_secondary_feedback != 0) &&
-      (bp->turn_off_secondary_feedback != 1)) {
-    error(
-        "The turn_off_secondary_feedback parameter must be either 0 or 1, "
-        "not %d",
-        bp->turn_off_secondary_feedback);
-  }
-
   bp->include_slim_disk =
       parser_get_param_int(params, "SPINJETAGN:include_slim_disk");
 
@@ -775,53 +878,14 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
         bp->include_slim_disk);
   }
 
-  bp->include_GRMHD_spindown =
-      parser_get_param_int(params, "SPINJETAGN:include_GRMHD_spindown");
-
-  if ((bp->include_GRMHD_spindown != 0) && (bp->include_GRMHD_spindown != 1)) {
-    error(
-        "The include_GRMHD_spindown parameter must be either 0 or 1, "
-        "not %d",
-        bp->include_GRMHD_spindown);
-  }
-
-  bp->include_ADIOS_suppression =
-      parser_get_param_int(params, "SPINJETAGN:include_ADIOS_suppression");
+  bp->use_jets_in_thin_disc =
+      parser_get_param_int(params, "SPINJETAGN:use_jets_in_thin_disc");
 
-  if ((bp->include_ADIOS_suppression != 0) &&
-      (bp->include_ADIOS_suppression != 1)) {
+  if ((bp->use_jets_in_thin_disc != 0) && (bp->use_jets_in_thin_disc != 1)) {
     error(
-        "The include_ADIOS_suppression parameter must be either 0 or 1, "
+        "The use_jets_in_thin_disc parameter must be either 0 or 1, "
         "not %d",
-        bp->include_ADIOS_suppression);
-  }
-
-  bp->ADIOS_R_in = parser_get_param_float(params, "SPINJETAGN:ADIOS_R_in");
-
-  if (bp->ADIOS_R_in <= 1.) {
-    error(
-        "The ADIOS_R_in parameter must be > 1, "
-        "not %f",
-        bp->ADIOS_R_in);
-  }
-
-  bp->ADIOS_s = parser_get_param_float(params, "SPINJETAGN:ADIOS_s");
-
-  if ((bp->ADIOS_s < 0.) || (bp->ADIOS_s > 1.)) {
-    error(
-        "The ADIOS_s parameter must be between 0 and 1, "
-        "not %f",
-        bp->ADIOS_s);
-  }
-
-  bp->TD_SD_eps_r_threshold =
-      parser_get_param_float(params, "SPINJETAGN:TD_SD_eps_r_threshold");
-
-  if ((bp->TD_SD_eps_r_threshold <= 0.) || (bp->TD_SD_eps_r_threshold >= 1.)) {
-    error(
-        "The TD_SD_eps_r_threshold parameter governing the transition between"
-        "thin and slim disk must be between 0. and 1., not %f",
-        bp->TD_SD_eps_r_threshold);
+        bp->use_jets_in_thin_disc);
   }
 
   bp->N_jet = parser_get_param_float(params, "SPINJETAGN:N_jet");
@@ -830,9 +894,9 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
     error("The N_jet parameter must be divisible by two, not %d", bp->N_jet);
   }
 
-  char temp4[PARSER_MAX_LINE_SIZE];
-  parser_get_param_string(params, "SPINJETAGN:AGN_jet_velocity_model", temp4);
-  if (strcmp(temp4, "Constant") == 0) {
+  char temp5[PARSER_MAX_LINE_SIZE];
+  parser_get_param_string(params, "SPINJETAGN:AGN_jet_velocity_model", temp5);
+  if (strcmp(temp5, "Constant") == 0) {
     bp->AGN_jet_velocity_model = AGN_jet_velocity_constant;
 
     bp->v_jet = parser_get_param_float(params, "SPINJETAGN:v_jet_km_p_s");
@@ -843,7 +907,7 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
     if (bp->v_jet <= 0.)
       error("The v_jet parameter must be > 0., not %f", bp->v_jet);
 
-  } else if (strcmp(temp4, "BlackHoleMass") == 0) {
+  } else if (strcmp(temp5, "BlackHoleMass") == 0) {
     bp->AGN_jet_velocity_model = AGN_jet_velocity_BH_mass;
 
     bp->v_jet_BH_mass_scaling_reference_mass = parser_get_param_float(
@@ -852,14 +916,15 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
     bp->v_jet_BH_mass_scaling_slope = parser_get_param_float(
         params, "SPINJETAGN:v_jet_BH_mass_scaling_slope");
 
-    bp->v_jet_cs_ratio =
-        parser_get_param_float(params, "SPINJETAGN:v_jet_cs_ratio");
-
     bp->v_jet_min =
         parser_get_param_float(params, "SPINJETAGN:v_jet_min_km_p_s");
     bp->v_jet_min *= (1e5 / (us->UnitLength_in_cgs / us->UnitTime_in_cgs));
 
-  } else if (strcmp(temp4, "MassLoading") == 0) {
+    bp->v_jet_max =
+        parser_get_param_float(params, "SPINJETAGN:v_jet_max_km_p_s");
+    bp->v_jet_max *= (1e5 / (us->UnitLength_in_cgs / us->UnitTime_in_cgs));
+
+  } else if (strcmp(temp5, "MassLoading") == 0) {
     bp->AGN_jet_velocity_model = AGN_jet_velocity_mass_loading;
 
     bp->v_jet_mass_loading =
@@ -869,7 +934,11 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
         parser_get_param_float(params, "SPINJETAGN:v_jet_min_km_p_s");
     bp->v_jet_min *= (1e5 / (us->UnitLength_in_cgs / us->UnitTime_in_cgs));
 
-  } else if (strcmp(temp4, "Local") == 0) {
+    bp->v_jet_max =
+        parser_get_param_float(params, "SPINJETAGN:v_jet_max_km_p_s");
+    bp->v_jet_max *= (1e5 / (us->UnitLength_in_cgs / us->UnitTime_in_cgs));
+
+  } else if (strcmp(temp5, "Local") == 0) {
     bp->AGN_jet_velocity_model = AGN_jet_velocity_local;
 
     bp->v_jet_xi = parser_get_param_float(params, "SPINJETAGN:v_jet_xi");
@@ -878,16 +947,20 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
         parser_get_param_float(params, "SPINJETAGN:v_jet_min_km_p_s");
     bp->v_jet_min *= (1e5 / (us->UnitLength_in_cgs / us->UnitTime_in_cgs));
 
-  } else if (strcmp(temp4, "SoundSpeed") == 0) {
-    bp->AGN_jet_velocity_model = AGN_jet_velocity_sound_speed;
+    bp->v_jet_max =
+        parser_get_param_float(params, "SPINJETAGN:v_jet_max_km_p_s");
+    bp->v_jet_max *= (1e5 / (us->UnitLength_in_cgs / us->UnitTime_in_cgs));
 
-    bp->v_jet_xi = parser_get_param_float(params, "SPINJETAGN:v_jet_xi");
+    float temp_hot_gas_min =
+        parser_get_param_float(params, "SPINJETAGN:temperature_hot_gas_min_K");
 
-    bp->v_jet_min =
-        parser_get_param_float(params, "SPINJETAGN:v_jet_min_km_p_s");
-    bp->v_jet_min *= (1e5 / (us->UnitLength_in_cgs / us->UnitTime_in_cgs));
+    temp_hot_gas_min /= units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+
+    bp->sound_speed_hot_gas_min =
+        sqrtf(hydro_gamma * hydro_gamma_minus_one * temp_hot_gas_min *
+              bp->temp_to_u_factor);
 
-  } else if (strcmp(temp4, "HaloMass") == 0) {
+  } else if (strcmp(temp5, "HaloMass") == 0) {
     error(
         "The scaling of jet velocities with halo mass is currently not "
         "supported.");
@@ -895,16 +968,13 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
     error(
         "The AGN jet velocity model must be Constant, MassLoading, "
         "BlackHoleMass, Local, SoundSpeed or HaloMass, not %s",
-        temp4);
+        temp5);
   }
 
   bp->opening_angle =
       parser_get_param_float(params, "SPINJETAGN:opening_angle_in_degrees");
   bp->opening_angle = bp->opening_angle * M_PI / 180.;
 
-  bp->jet_h_r_slope =
-      parser_get_param_float(params, "SPINJETAGN:jet_h_r_slope");
-
   bp->eps_f_jet = parser_get_param_float(params, "SPINJETAGN:eps_f_jet");
 
   if ((bp->eps_f_jet <= 0.) || (bp->eps_f_jet > 1.)) {
@@ -928,10 +998,18 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
       parser_get_param_float(params, "SPINJETAGN:jet_efficiency");
 
   if (bp->jet_efficiency <= 0.) {
+    error("The constant jet efficiency parameter must be >0., not %f",
+          bp->jet_efficiency);
+  }
+
+  bp->fix_jet_direction =
+      parser_get_param_int(params, "SPINJETAGN:fix_jet_direction");
+
+  if ((bp->fix_jet_direction != 0) && (bp->fix_jet_direction != 1)) {
     error(
-        "The jet_efficiency corresponding to the jet efficiency "
-        "must be larger than 0., not %f",
-        bp->jet_efficiency);
+        "The fix_jet_direction parameter must be either 0 or 1, "
+        "not %d",
+        bp->fix_jet_direction);
   }
 
   bp->fix_radiative_efficiency =
@@ -955,21 +1033,89 @@ INLINE static void black_holes_props_init(struct black_holes_props *bp,
         bp->radiative_efficiency);
   }
 
-  char temp5[60];
-  parser_get_param_string(params, "SPINJETAGN:AGN_jet_feedback_model", temp5);
-  if (strcmp(temp5, "MinimumDistance") == 0)
+  bp->use_ADIOS_winds =
+      parser_get_param_int(params, "SPINJETAGN:use_ADIOS_winds");
+
+  if ((bp->use_ADIOS_winds != 0) && (bp->use_ADIOS_winds != 1)) {
+    error(
+        "The use_ADIOS_winds parameter must be either 0 or 1, "
+        "not %d",
+        bp->use_ADIOS_winds);
+  }
+
+  bp->slim_disc_wind_factor =
+      parser_get_param_float(params, "SPINJETAGN:slim_disc_wind_factor");
+
+  if ((bp->slim_disc_wind_factor < 0) || (bp->slim_disc_wind_factor > 1)) {
+    error(
+        "The slim_disc_wind_factor parameter must be between 0 and 1, "
+        "(inclusive), not %f",
+        bp->slim_disc_wind_factor);
+  }
+
+  char temp6[60];
+  parser_get_param_string(params, "SPINJETAGN:AGN_jet_feedback_model", temp6);
+  if (strcmp(temp6, "MinimumDistance") == 0)
     bp->jet_feedback_model = AGN_jet_minimum_distance_model;
-  else if (strcmp(temp5, "MaximumDistance") == 0)
+  else if (strcmp(temp6, "MaximumDistance") == 0)
     bp->jet_feedback_model = AGN_jet_maximum_distance_model;
-  else if (strcmp(temp5, "SpinAxis") == 0)
+  else if (strcmp(temp6, "SpinAxis") == 0)
     bp->jet_feedback_model = AGN_jet_spin_axis_model;
-  else if (strcmp(temp5, "MinimumDensity") == 0)
+  else if (strcmp(temp6, "MinimumDensity") == 0)
     bp->jet_feedback_model = AGN_jet_minimum_density_model;
   else
     error(
         "The AGN feedback model must be MinimumDistance, MaximumDistance, "
         "SpinAxis or MinimumDensity, not %s",
-        temp5);
+        temp6);
+
+  bp->accretion_efficiency_slim =
+      parser_get_param_float(params, "SPINJETAGN:accretion_efficiency_slim");
+
+  if ((bp->accretion_efficiency_slim < 0) ||
+      (bp->accretion_efficiency_slim > 1)) {
+    error(
+        "The accretion_efficiency_slim parameter must be between 0 and 1, "
+        "(inclusive), not %f",
+        bp->accretion_efficiency_slim);
+  }
+
+  char temp7[60];
+  parser_get_param_string(params, "SPINJETAGN:accretion_efficiency_mode",
+                          temp7);
+  if (strcmp(temp7, "Constant") == 0) {
+    bp->accretion_efficiency_mode = BH_accretion_efficiency_constant;
+    bp->accretion_efficiency_thick =
+        parser_get_param_float(params, "SPINJETAGN:accretion_efficiency_thick");
+
+    if ((bp->accretion_efficiency_thick < 0) ||
+        (bp->accretion_efficiency_thick > 1)) {
+      error(
+          "The accretion_efficiency_thick parameter must be between 0 and 1, "
+          "(inclusive), not %f",
+          bp->accretion_efficiency_thick);
+    }
+  } else if (strcmp(temp7, "Variable") == 0) {
+    bp->accretion_efficiency_mode = BH_accretion_efficiency_variable;
+    bp->ADIOS_s = parser_get_param_float(params, "SPINJETAGN:ADIOS_s");
+
+    if ((bp->ADIOS_s < 0.f) || (bp->ADIOS_s > 1.f)) {
+      error(
+          "The ADIOS_s parameter must be between 0 and 1, "
+          "(inclusive), not %f",
+          bp->ADIOS_s);
+    }
+
+    bp->ADIOS_R_in = parser_get_param_float(params, "SPINJETAGN:ADIOS_R_in");
+
+    if (bp->ADIOS_R_in <= 1.f) {
+      error("The ADIOS_R_in parameter must be larger than 1, not %f",
+            bp->ADIOS_s);
+    }
+  } else {
+    error("The accretion efficiency model must be Constant or Variable, not %s",
+          temp7);
+  }
 }
 
 /**
diff --git a/src/black_holes/SPIN_JET/black_holes_spin.h b/src/black_holes/SPIN_JET/black_holes_spin.h
index d7207ef770f31a826946713aed7ae39483fd71e4..72139e17f292fd5690f4a3ee813f4f95e37f51ac 100644
--- a/src/black_holes/SPIN_JET/black_holes_spin.h
+++ b/src/black_holes/SPIN_JET/black_holes_spin.h
@@ -26,17 +26,45 @@
 /* Local includes */
 #include "black_holes_properties.h"
 #include "black_holes_struct.h"
+#include "hydro_properties.h"
 #include "inline.h"
 #include "physical_constants.h"
 
+/**
+ * @brief Compute the gravitational radius of a black hole.
+ *
+ * @param a Black hole mass.
+ * @param constants Physical constants (in internal units).
+ */
+__attribute__((always_inline)) INLINE static float
+black_hole_gravitational_radius(float mass,
+                                const struct phys_const* constants) {
+
+  const float r_G =
+      mass * constants->const_newton_G /
+      (constants->const_speed_light_c * constants->const_speed_light_c);
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (r_G <= 0.f) {
+    error(
+        "Something went wrong with calculation of R_G of black holes. "
+        " R_G is %f instead of R_G > 0.",
+        r_G);
+  }
+#endif
+
+  return r_G;
+}
+
 /**
  * @brief Compute the radius of the horizon of a BH particle in gravitational
  * units.
  *
  * @param a Black hole spin, -1 < a < 1.
  */
-__attribute__((always_inline)) INLINE static float r_hor(float a) {
-  return 1. + sqrtf((1. - a) * (1. + a));
+__attribute__((always_inline)) INLINE static float black_hole_horizon_radius(
+    float a) {
+  return 1.f + sqrtf((1.f - a) * (1.f + a));
 }
 
 /**
@@ -48,32 +76,33 @@ __attribute__((always_inline)) INLINE static float r_hor(float a) {
  *
  * @param a Black hole spin, -1 < a < 1.
  */
-__attribute__((always_inline)) INLINE static float r_isco(float a) {
-  const float Z1 = 1. + (cbrtf((1. + fabsf(a)) * (1. - a * a)) +
-                         cbrtf((1. - fabsf(a)) * (1. - a * a)));
-  const float Z2 = sqrtf(3. * a * a + Z1 * Z1);
+__attribute__((always_inline)) INLINE static float black_hole_isco_radius(
+    float a) {
+  const float Z1 = 1.f + (cbrtf((1.f + fabsf(a)) * (1.f - a * a)) +
+                          cbrtf((1.f - fabsf(a)) * (1.f - a * a)));
+  const float Z2 = sqrtf(3.f * a * a + Z1 * Z1);
 
   const float R_ISCO =
-      3. + Z2 - a / fabsf(a) * sqrtf((3. - Z1) * (3. + Z1 + 2. * Z2));
+      3. + Z2 - a / fabsf(a) * sqrtf((3.f - Z1) * (3.f + Z1 + 2.f * Z2));
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (Z1 > 3.) {
+  if (Z1 > 3.f) {
     error(
         "Something went wrong with calculation of Z1 factor for r_isco of"
         " black holes. Z1 is %f instead of Z1 > 3.",
         Z1);
   }
 
-  if ((3. + Z1 + 2. * Z2) < 0.) {
+  if ((3.f + Z1 + 2.f * Z2) < 0.f) {
     error(
         "Something went wrong with calculation of (3. + Z1 + 2. * Z2 ) "
         "factor for r_isco of black holes. (3. + Z1 + 2. * Z2 ) is %f instead "
         "of"
         " (3. + Z1 + 2. * Z2 ) > 0.",
-        3. + Z1 + 2. * Z2);
+        3.f + Z1 + 2.f * Z2);
   }
 
-  if (R_ISCO < 1.) {
+  if (R_ISCO < 1.f) {
     error(
         "Something went wrong with calculation of R_ISCO of black holes. "
         "R_ISCO is %f instead >= 1.",
@@ -91,15 +120,16 @@ __attribute__((always_inline)) INLINE static float r_isco(float a) {
  * @param a Black hole spin magnitude, 0 < a < 1.
  * @param constants Physical constants (in internal units).
  */
-__attribute__((always_inline)) INLINE static float j_BH(
-    struct bpart* bp, const struct phys_const* constants) {
+__attribute__((always_inline)) INLINE static float
+black_hole_angular_momentum_magnitude(struct bpart* bp,
+                                      const struct phys_const* constants) {
 
   const float J_BH =
       fabs(bp->subgrid_mass * bp->subgrid_mass * bp->spin *
            constants->const_newton_G / constants->const_speed_light_c);
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (J_BH <= 0.) {
+  if (J_BH <= 0.f) {
     error(
         "Something went wrong with calculation of j_BH of black holes. "
         " J_BH is %f instead of J_BH > 0.",
@@ -110,31 +140,6 @@ __attribute__((always_inline)) INLINE static float j_BH(
   return J_BH;
 }
 
-/**
- * @brief Compute the gravitational radius of a black hole.
- *
- * @param a Black hole mass.
- * @param constants Physical constants (in internal units).
- */
-__attribute__((always_inline)) INLINE static float R_gravitational(
-    float mass, const struct phys_const* constants) {
-
-  const float r_G =
-      mass * constants->const_newton_G /
-      (constants->const_speed_light_c * constants->const_speed_light_c);
-
-#ifdef SWIFT_DEBUG_CHECKS
-  if (r_G <= 0.) {
-    error(
-        "Something went wrong with calculation of R_G of black holes. "
-        " R_G is %f instead of R_G > 0.",
-        r_G);
-  }
-#endif
-
-  return r_G;
-}
-
 /**
  * @brief Compute the warp radius of a black hole particle.
  *
@@ -158,27 +163,29 @@ __attribute__((always_inline)) INLINE static float R_gravitational(
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float r_warp(
+__attribute__((always_inline)) INLINE static float black_hole_warp_radius(
     struct bpart* bp, const struct phys_const* constants,
     const struct black_holes_props* props) {
 
-  /* Define placeholder variable for the result */
-  float Rw = -1.;
+  /* Define placeholder value for the result. We will assign the final result
+     to this variable. */
+  float Rw = -1.f;
 
   /* Gravitational radius */
-  const float R_G = R_gravitational(bp->subgrid_mass, constants);
+  const float R_G =
+      black_hole_gravitational_radius(bp->subgrid_mass, constants);
 
   /* Start branching depending on which accretion mode the BH is in */
   if (bp->accretion_mode == BH_thick_disc) {
 
     /* Eqn. 22 from Lubow et al. (2002) with H/R=h_0_ADAF (thick disk) */
-    const float base = 15.36 * fabsf(bp->spin) / props->h_0_ADAF_2;
-    Rw = R_G * powf(base, 0.4);
+    const float base = 15.36f * fabsf(bp->spin) / props->h_0_ADAF_2;
+    Rw = R_G * powf(base, 0.4f);
   } else if (bp->accretion_mode == BH_slim_disc) {
 
     /* Eqn. 22 from Lubow et al. (2002) with H/R=1/gamma_SD (slim disk) */
-    const float base = 15.36 * fabsf(bp->spin) * props->gamma_SD;
-    Rw = R_G * powf(base, 0.4);
+    const float base = 15.36f * fabsf(bp->spin) * props->gamma_SD;
+    Rw = R_G * powf(base, 0.4f);
   } else if (bp->accretion_mode == BH_thin_disc) {
 
     /* Start branching depending on which region of the thin disk we wish to
@@ -189,20 +196,20 @@ __attribute__((always_inline)) INLINE static float r_warp(
       /* Calculate different factors in eqn. 11 (Griffin et al. 2019) for warp
           radius of region b in Shakura & Sunyaev (1973) */
       float mass_factor =
-          powf(bp->subgrid_mass / (1e8 * constants->const_solar_mass), 0.2);
-      float edd_factor = powf(bp->eddington_fraction, 0.4);
+          powf(bp->subgrid_mass / (1e8f * constants->const_solar_mass), 0.2f);
+      float edd_factor = powf(bp->eddington_fraction, 0.4f);
 
       /* Gather the factors and finalize calculation */
       const float base = mass_factor * fabsf(bp->spin) /
                          (props->xi_TD * props->alpha_factor_08 * edd_factor);
-      const float rw = 3410. * 2. * R_G * powf(base, 0.625);
+      const float rw = 3410.f * 2.f * R_G * powf(base, 0.625f);
 
       /* Self-gravity radius in region b: eqn. 16 in Griffin et al. */
-      mass_factor =
-          powf(bp->subgrid_mass / (1e8 * constants->const_solar_mass), -0.961);
-      edd_factor = powf(bp->eddington_fraction, -0.353);
+      mass_factor = powf(
+          bp->subgrid_mass / (1e8f * constants->const_solar_mass), -0.961f);
+      edd_factor = powf(bp->eddington_fraction, -0.353f);
 
-      const float rs = 4790. * 2. * R_G * mass_factor *
+      const float rs = 4790.f * 2.f * R_G * mass_factor *
                        props->alpha_factor_0549 * edd_factor;
 
       /* Take the minimum */
@@ -213,20 +220,20 @@ __attribute__((always_inline)) INLINE static float r_warp(
 
       /* Calculate different factors in eqn. A8 (Fiacconi et al. 2018) */
       float mass_factor =
-          powf(bp->subgrid_mass / (1e6 * constants->const_solar_mass), 0.2);
-      float edd_factor = powf(bp->eddington_fraction, 0.3);
+          powf(bp->subgrid_mass / (1e6f * constants->const_solar_mass), 0.2f);
+      float edd_factor = powf(bp->eddington_fraction, 0.3f);
 
       /* Gather the factors and finalize calculation */
       const float base = mass_factor * fabsf(bp->spin) /
                          (props->xi_TD * props->alpha_factor_02 * edd_factor);
-      const float rw = 1553. * 2. * R_G * powf(base, 0.5714);
+      const float rw = 1553.f * 2.f * R_G * powf(base, 0.5714f);
 
       /* Repeat the same for self-gravity radius - eqn. A6 in F2018 */
-      mass_factor =
-          powf(bp->subgrid_mass / (1e6 * constants->const_solar_mass), -1.1556);
-      edd_factor = powf(bp->eddington_fraction, -0.48889);
+      mass_factor = powf(
+          bp->subgrid_mass / (1e6f * constants->const_solar_mass), -1.1556f);
+      edd_factor = powf(bp->eddington_fraction, -0.48889f);
 
-      const float rs = 1.2 * 100000. * 2. * R_G * mass_factor *
+      const float rs = 1.2f * 100000.f * 2.f * R_G * mass_factor *
                        props->alpha_factor_06222 * edd_factor;
 
       /* Take the minimum */
@@ -235,7 +242,7 @@ __attribute__((always_inline)) INLINE static float r_warp(
   }
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (Rw < 0.) {
+  if (Rw < 0.f) {
     error(
         "Something went wrong with calculation of Rw of black holes. "
         " Rw is %f instead of Rw >= 0.",
@@ -271,15 +278,17 @@ __attribute__((always_inline)) INLINE static float r_warp(
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float m_warp(
+__attribute__((always_inline)) INLINE static double black_hole_warp_mass(
     struct bpart* bp, const struct phys_const* constants,
     const struct black_holes_props* props) {
 
-  /* Define placeholder variable for the result */
-  float Mw = -1.;
+  /* Define placeholder value for the result. We will assign the final result
+     to this variable. */
+  double Mw = -1.;
 
   /* Gravitational radius */
-  const float R_G = R_gravitational(bp->subgrid_mass, constants);
+  const float R_G =
+      black_hole_gravitational_radius(bp->subgrid_mass, constants);
 
   /* Start branching depending on which accretion mode the BH is in */
   if ((bp->accretion_mode == BH_thick_disc) ||
@@ -298,7 +307,7 @@ __attribute__((always_inline)) INLINE static float m_warp(
     Mw = 2. * bp->accretion_rate /
          (3. * props->alpha_acc * v_0 *
           sqrtf(bp->subgrid_mass * constants->const_newton_G)) *
-         powf(r_warp(bp, constants, props), 1.5);
+         powf(black_hole_warp_radius(bp, constants, props), 1.5);
   } else {
 
     /* Start branching depending on which region of the thin disk we wish to
@@ -311,7 +320,7 @@ __attribute__((always_inline)) INLINE static float m_warp(
           powf(bp->subgrid_mass / (1e8 * constants->const_solar_mass), 2.2);
       const float edd_factor = powf(bp->eddington_fraction, 0.6);
       const float R_factor =
-          powf(r_warp(bp, constants, props) / (2. * R_G), 1.4);
+          powf(black_hole_warp_radius(bp, constants, props) / (2. * R_G), 1.4);
 
       /* Gather factors and finalize calculation */
       Mw = constants->const_solar_mass * 1.35 * mass_factor *
@@ -324,7 +333,7 @@ __attribute__((always_inline)) INLINE static float m_warp(
           powf(bp->subgrid_mass / (1e6 * constants->const_solar_mass), 2.2);
       const float edd_factor = powf(bp->eddington_fraction, 0.7);
       const float R_factor =
-          powf(r_warp(bp, constants, props) / (2. * R_G), 1.25);
+          powf(black_hole_warp_radius(bp, constants, props) / (2. * R_G), 1.25);
 
       Mw = constants->const_solar_mass * 0.01 * mass_factor *
            props->alpha_factor_08_inv_10 * edd_factor * R_factor;
@@ -363,12 +372,14 @@ __attribute__((always_inline)) INLINE static float m_warp(
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float j_warp(
-    struct bpart* bp, const struct phys_const* constants,
-    const struct black_holes_props* props) {
+__attribute__((always_inline)) INLINE static double
+black_hole_warp_angular_momentum(struct bpart* bp,
+                                 const struct phys_const* constants,
+                                 const struct black_holes_props* props) {
 
-  /* Define placeholder variable for the result */
-  float Jw = -1.;
+  /* Define placeholder value for the result. We will assign the final result
+     to this variable. */
+  double Jw = -1.;
 
   /* Start branching depending on which accretion mode the BH is in */
   if ((bp->accretion_mode == BH_thick_disc) ||
@@ -388,8 +399,9 @@ __attribute__((always_inline)) INLINE static float j_warp(
     }
 
     /* Gather factors for the final result  */
+    const double r_warp = black_hole_warp_radius(bp, constants, props);
     Jw = 2. * bp->accretion_rate * omega_0 / (2. * props->alpha_acc * v_0) *
-         r_warp(bp, constants, props) * r_warp(bp, constants, props);
+         r_warp * r_warp;
   } else {
 
     /* Start branching depending on which region of the thin disk we wish to
@@ -401,14 +413,14 @@ __attribute__((always_inline)) INLINE static float j_warp(
        For region b, c=-3/5 (see Griffin et al. 2019), and for region c, c=-3/4
        (see Fiacconi et al. 2018). */
     if (props->TD_region == TD_region_B) {
-      Jw = 0.737 * m_warp(bp, constants, props) *
+      Jw = 0.737 * black_hole_warp_mass(bp, constants, props) *
            sqrtf(bp->subgrid_mass * constants->const_newton_G *
-                 r_warp(bp, constants, props));
+                 black_hole_warp_radius(bp, constants, props));
     }
     if (props->TD_region == TD_region_C) {
-      Jw = 0.714 * m_warp(bp, constants, props) *
+      Jw = 0.714 * black_hole_warp_mass(bp, constants, props) *
            sqrtf(bp->subgrid_mass * constants->const_newton_G *
-                 r_warp(bp, constants, props));
+                 black_hole_warp_radius(bp, constants, props));
     }
   }
 
@@ -432,46 +444,64 @@ __attribute__((always_inline)) INLINE static float j_warp(
  *
  * @param a Black hole spin, -1 < a < 1.
  */
-__attribute__((always_inline)) INLINE static float eps_NT(float a) {
+__attribute__((always_inline)) INLINE static float eps_Novikov_Thorne(float a) {
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (r_isco(a) <= 0.6667) {
+  if (black_hole_isco_radius(a) <= 0.6667f) {
     error(
-        "Something went wrong with calculation of eps_NT of black holes. "
-        " r_isco is %f instead of r_isco > 1.",
-        r_isco(a));
+        "Something went wrong with calculation of eps_Novikov_Thorn of. "
+        "black holes. r_isco is %f instead of r_isco > 1.",
+        black_hole_isco_radius(a));
   }
 #endif
 
-  return 1. - sqrtf(1. - 2. / 3. / r_isco(a));
+  return 1. - sqrtf(1. - 2.f / (3.f * black_hole_isco_radius(a)));
 }
 
 /**
  * @brief Compute the spin- and accretion rate-dependant radiative efficiency
  * of a BH particle in the super-Eddington (slim disk) regime.
  *
- * This is eqn. 3 in Madau et al. (2014), which is based on numerical GR
+ * This is eqn. 3-6 in Madau et al. (2014), which is based on numerical GR
  * results by Sadowski (2009).
  *
  * @param a Black hole spin, -1 < a < 1.
  * @param m_dot Accretion rate normalized to the Eddington rate.
  */
-__attribute__((always_inline)) INLINE static float eps_SD(float a, float mdot) {
-  const float B = powf(4.627 - 4.445 * a, -0.5524);
-  const float C = powf(827.3 - 718.1 * a, -0.706);
-  const float A = powf(0.9663 - 0.9292 * a, -0.5693);
+__attribute__((always_inline)) INLINE static float eps_slim_disc(float a,
+                                                                 float mdot) {
+  const float B = powf(4.627f - 4.445f * a, -0.5524f);
+  const float C = powf(827.3f - 718.1f * a, -0.706f);
+  const float A = powf(0.9663f - 0.9292f * a, -0.5693f);
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (mdot <= 0.) {
+  if (mdot <= 0.f) {
     error(
-        "The calculation of eps_SD was called even though mdot is %f. "
+        "The calculation of eps_slim_disc was called even though mdot is %f. "
         " This function should not have been called if the accretion rate is "
         " not > 0.",
         mdot);
   }
 #endif
 
-  return 0.1 / mdot * (0.985 / (B + 1.6 / mdot) + 0.015 / (C + 1.6 / mdot)) * A;
+  /* Since we use a definition of the Eddington ratio (mdot) that includes
+     the varying (Novikov-Thorne) radiative efficiency, we need to rescale this
+     back to a constant one, as the paper provides a formula assuming a
+     constant radiative efficiency. They use a value of 1/16, so we redefine
+     the Eddington ratio using the ratio of efficiencies. */
+  const float constant_rad_efficiency = 1.f / 16.f;
+
+  mdot = mdot * constant_rad_efficiency / eps_Novikov_Thorne(a);
+
+  /* Return radiative efficiency as given by Eqn 3 from Madau et al. (2014).
+     Note that the equation provided in the paper is for L / L_Edd, rather than
+     for L / (f_Edd * M_Edd * c^2). We thus need to multiply their Eqn 3 by
+     L_Edd / (f_Edd * M_Edd * c^2) = eps_rad_constant / mdot. Here we have used
+     M_Edd = L_Edd / (eps_rad_constant * c^2). Also note that mdot = f_Edd in
+     our notation. */
+
+  return (constant_rad_efficiency / mdot) *
+         (0.985f / (B + mdot) + 0.015f / (C + mdot)) * A;
 }
 
 /**
@@ -479,24 +509,35 @@ __attribute__((always_inline)) INLINE static float eps_SD(float a, float mdot) {
  *
  * The possible modes are the thick disk, thin disk and slim disk, in
  * order of increasing accretion rate. The transition from thick to thin disk
- * is at 0.4*alpha^2, based on current theory (Yuan & Narayan 2014). The
- * transition from thin to slim disk occurs when the slim disk efficiency
- * becomes sufficiently weak compared to the thin disk one. We parametrize
- * this transition as occuring at eps_SD = props->TD_SD_eps_r_threshold *
- * eps_TD, with props->TD_SD_eps_r_threshold < 1 of order 0.5
+ * is currently governed by a free parameter, props->mdot_crit_ADAF (of order
+ * 0.01. The transition between the thin and slim disc is assumed to take place
+ * at mdot = 1, i.e. for super-Eddington accretion. Note that this assumption
+ * only works if we define mdot by using the spin-dependent radiative
+ * efficiency, which we do.
  *
  * @param bp Pointer to the b-particle data.
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static void decide_mode(
-    struct bpart* bp, const struct black_holes_props* props) {
-  if (bp->eddington_fraction < props->mdot_crit_ADAF) {
+__attribute__((always_inline)) INLINE static void
+black_hole_select_accretion_mode(struct bpart* bp,
+                                 const struct black_holes_props* props) {
+
+  /* For deciding the accretion mode, we want to use the Eddington fraction
+   * calculated using the raw, unsuppressed accretion rate. This means that
+   * if the disc is currently thick, its current Eddington fraction, which is
+   * already suppressed, needs to be unsuppressed (increased) to retrieve the
+   * raw Bondi-based Eddington ratio. */
+  float eddington_fraction_Bondi = bp->eddington_fraction;
+  eddington_fraction_Bondi *= 1.f / bp->accretion_efficiency;
+
+  if (eddington_fraction_Bondi < props->mdot_crit_ADAF) {
     bp->accretion_mode = BH_thick_disc;
   } else {
-    if ((eps_SD(bp->spin, bp->eddington_fraction) <
-         props->TD_SD_eps_r_threshold * eps_NT(bp->spin)) &&
-        (props->include_slim_disk)) {
+
+    /* The disc is assumed to be slim (super-Eddington) if the Eddington
+     * fraction is above 1. */
+    if ((eddington_fraction_Bondi > 1.f) && (props->include_slim_disk)) {
       bp->accretion_mode = BH_slim_disc;
     } else {
       bp->accretion_mode = BH_thin_disc;
@@ -504,144 +545,183 @@ __attribute__((always_inline)) INLINE static void decide_mode(
   }
 
   /* If we do not include radiative feedback, then we force the disk to be in
-     the thick disk mode */
+     the thick disk mode always. */
   if (props->turn_off_radiative_feedback) {
     bp->accretion_mode = BH_thick_disc;
   }
 
   /* similar for if we do not include jets - we force the disk to be thin */
-  if (props->include_jets == 0) {
+  if (!props->include_jets) {
     bp->accretion_mode = BH_thin_disc;
   }
 }
 
 /**
- * @brief Compute the aspect ratio of the subgrid accretion disk.
+ * @brief Compute the accretion efficiency of a BH particle.
  *
  * The result depends on bp->accretion_mode (thick disk, thin disk or
- * slim disk). For the thick disk and slim disk, the aspect ratio is
- * a constant, H/R = h_0.
- *
- * For the thin disk, the result depends on props->TD_region (B - region b from
- * Shakura & Sunyaev 1973, C - region c from Shakura & Sunyaev 1973). In region
- * b, we take H/R as eqn. 8 in Griffin et al. (2019), and in region c H/r is
- * taken directly as eqn. 2.19 from Shakura & Sunyaev (1973).
+ * slim disk). We assume no accretion efficiency (100%) in the thin disk,
+ * and allow for options for a non-zero accretion efficiency in the thick
+ * and slim disc. For both we allow the option of constant values, and for the
+ * thick disc we allow an option for a scaling with Eddington ratio that is
+ * motivated by simulations.
  *
  * @param bp Pointer to the b-particle data.
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float aspect_ratio(
-    struct bpart* bp, const struct phys_const* constants,
-    const struct black_holes_props* props) {
-
-  /* Define placeholder variable for the result */
-  float h_0 = -1.;
-
-  /* Start branching depending on which accretion mode the BH is in */
-  if ((bp->accretion_mode == BH_thick_disc) ||
-      (bp->accretion_mode == BH_slim_disc)) {
-    if (bp->accretion_mode == BH_thick_disc) {
-      h_0 = props->h_0_ADAF;
-    } else {
-      h_0 = 0.5 * props->gamma_SD_inv;
-    }
-  } else {
-
-    /* Start branching depending on which region of the thin disk we wish to
-       base the model upon (TD_region=B: region b from Shakura & Sunyaev 1973,
-       or TD_region=C: region c). */
-    if (props->TD_region == TD_region_B) {
+__attribute__((always_inline)) INLINE static float
+black_hole_accretion_efficiency(struct bpart* bp,
+                                const struct black_holes_props* props,
+                                const struct phys_const* constants,
+                                const struct cosmology* cosmo) {
+
+  if (bp->accretion_mode == BH_thick_disc ||
+      bp->accretion_mode == BH_slim_disc) {
+
+    if (props->accretion_efficiency_mode == BH_accretion_efficiency_constant) {
+      if (bp->accretion_mode == BH_thick_disc) {
+        return props->accretion_efficiency_thick;
+      } else if (bp->accretion_mode == BH_slim_disc) {
+        return props->accretion_efficiency_slim;
+      } else {
 
-      /* Compute factors for eqn. 8 in Griffin et al. (2019). */
-      const float mass_factor =
-          powf(bp->subgrid_mass / (1e8 * constants->const_solar_mass), -0.1);
-      const float edd_factor = powf(bp->eddington_fraction, 0.2);
-      const float R_G = R_gravitational(bp->subgrid_mass, constants);
-      const float R_factor =
-          powf(r_warp(bp, constants, props) / (2. * R_G), 0.05);
+#ifdef SWIFT_DEBUG_CHECKS
+        error(
+            "This branch of the function accretion_efficiency() should not"
+            " have been reached!");
+#endif
 
-      /* Gather factors and finalize calculation. */
-      h_0 = 1.25 * 0.001 * mass_factor * props->alpha_factor_01 * edd_factor *
-            R_factor;
-    }
-    if (props->TD_region == TD_region_C) {
+        return 1.f;
+      }
+    } else if (props->accretion_efficiency_mode ==
+               BH_accretion_efficiency_variable) {
+
+      if (bp->accretion_mode == BH_thick_disc) {
+
+        /* Compute the transition radius between an outer thin disc and an
+         * inner thick disc. This is assumed to happen at 10 R_G at the
+         * critical value of the Eddington ratio between the two regimes.
+         * The transition radius then increases as 1 / f_Edd^2. Note that
+         * we also need to use the raw (unsuppressed) Eddington ratio here,
+         * hence the multiplication by accretion efficiencies. Note that the
+         * units of the transition radius here are in R_G. */
+        float R_tr = props->ADIOS_R_in * props->mdot_crit_ADAF *
+                     props->mdot_crit_ADAF * bp->accretion_efficiency *
+                     bp->accretion_efficiency /
+                     (bp->eddington_fraction * bp->eddington_fraction);
+
+        /* We need to also compute the Bondi radius (in units of R_G), which
+         * can be expressed in terms of the ratio between speed of light and
+         * sound speed. */
+        const double c = constants->const_speed_light_c;
+        float gas_c_phys2 = bp->sound_speed_gas * cosmo->a_factor_sound_speed;
+        gas_c_phys2 = gas_c_phys2 * gas_c_phys2;
+        float R_B = c * c / gas_c_phys2;
+
+        /* Limit the transition radius to no larger than R_B and no smaller
+         * than 10 R_G. */
+        R_tr = fminf(R_B, R_tr);
+        R_tr = fmaxf(10.f, R_tr);
+
+        /* Implement the actual scaling of accretion efficiency with transition
+         * radius as found by GRMHD simulations. */
+        float suppr_factor = powf(10.f / R_tr, props->ADIOS_s);
+        return suppr_factor;
+      } else if (bp->accretion_mode == BH_slim_disc) {
+        return props->accretion_efficiency_slim;
+      } else {
 
-      /* Compute factors for eqn. 2.19 in Shakura & Sunyaev (1973). */
-      const float mass_factor =
-          powf(bp->subgrid_mass / (1e8 * constants->const_solar_mass), -0.1);
-      const float edd_factor = powf(bp->eddington_fraction, 0.15);
-      const float R_G = R_gravitational(bp->subgrid_mass, constants);
-      const float R_factor =
-          powf(r_warp(bp, constants, props) / (2. * R_G), 0.125);
+#ifdef SWIFT_DEBUG_CHECKS
+        error(
+            "This branch of the function accretion_efficiency() should not"
+            " have been reached!");
+#endif
 
-      /* Gather factors and finalize calculation. */
-      h_0 = 1.15 * 0.001 * mass_factor * props->alpha_factor_01 * edd_factor *
-            R_factor;
-    }
-  }
+        return 1.f;
+      }
+    } else {
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (h_0 <= 0.) {
-    error(
-        "Something went wrong with calculation of h_0 of black holes. "
-        " h_0 is %f instead of h_0 > 0.",
-        h_0);
-  }
+      error(
+          "This branch of the function accretion_efficiency() should not"
+          " have been reached!");
 #endif
 
-  return h_0;
+      return 1.f;
+    }
+  } else {
+    return 1.f;
+  }
 }
 
 /**
  * @brief Compute the jet efficiency of a BH particle.
  *
  * The result depends on bp->accretion_mode (thick disk, thin disk or
- * slim disk), through the varying H/R aspect ratios.
+ * slim disk).
  *
  * The equation implemented is eqn. 9 from Tchekhovskoy et al. (2010), with the
- * dimensionless magnetic flux phi taken as eqn. 9 from Narayan et al. (2021).
- *
- * The dependence on the aspect ratio comes from results in Tchekhovskoy et al.
- * (2014) and the dependence in classical Blandford & Znajek (1979) jet theory.
+ * dimensionless magnetic flux phi taken as eqn. 9 from Narayan et al. (2022),
+ * and an additional modification from Ricarte et al. (2023).
  *
  * @param bp Pointer to the b-particle data.
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float jet_efficiency(
+__attribute__((always_inline)) INLINE static float black_hole_jet_efficiency(
     struct bpart* bp, const struct black_holes_props* props) {
 
-  float jet_eff = -1.;
+  /* Define placeholder value for the result. We will assign the final result
+     to this variable. */
+  float jet_eff = -1.f;
   if (props->fix_jet_efficiency) {
     jet_eff = props->jet_efficiency;
   } else {
-    const float kappa = 0.05;
+
+    /* Numerical prefactor that appears in the jet power formula, related to
+       the geometry of the magnetic field. */
+    const float kappa = 0.05f;
+
+    /* Definition of angular velocity at the BH event horizon */
     const float horizon_ang_vel =
-        bp->spin / (2. * (1. + sqrtf(1. - bp->spin * bp->spin)));
-    const float phi = -20.2 * bp->spin * bp->spin * bp->spin -
-                      14.9 * bp->spin * bp->spin + 34. * bp->spin + 52.6;
-    jet_eff = kappa * 0.25 * M_1_PI * phi * phi *
-              powf(bp->aspect_ratio * 3.333, props->jet_h_r_slope) *
-              horizon_ang_vel * horizon_ang_vel *
-              (1. + 1.38 * horizon_ang_vel * horizon_ang_vel -
-               9.2 * horizon_ang_vel * horizon_ang_vel * horizon_ang_vel *
+        bp->spin / (2.f * (1.f + sqrtf(1.f - bp->spin * bp->spin)));
+
+    /* Dimensionless magnetic flux as a function of BH spin, using Eqn. (15)
+       from Narayan et al. (2022). */
+    float phi = -20.2f * bp->spin * bp->spin * bp->spin -
+                14.9f * bp->spin * bp->spin + 34.f * bp->spin + 52.6f;
+
+    float Eddington_ratio = bp->eddington_fraction;
+
+    /* Suppress the magnetic flux if we are in the thin or slim disc,
+     * according to results from Ricarte et al. (2023). */
+    if ((bp->accretion_mode == BH_slim_disc) ||
+        (props->use_jets_in_thin_disc && bp->accretion_mode == BH_thin_disc)) {
+      phi *= powf(Eddington_ratio / 1.88f, 1.29f) /
+             (1.f + powf(Eddington_ratio / 1.88f, 1.29f));
+    }
+
+    /* Full jet efficiency formula as in Tchekhovskoy et al. (2010). */
+    jet_eff = kappa * 0.25f * M_1_PI * phi * phi * horizon_ang_vel *
+              horizon_ang_vel *
+              (1.f + 1.38f * horizon_ang_vel * horizon_ang_vel -
+               9.2f * horizon_ang_vel * horizon_ang_vel * horizon_ang_vel *
                    horizon_ang_vel);
   }
 
   /* Turn off jet feedback if we want to do that */
-  if (props->include_jets == 0) {
-    jet_eff = 0.;
+  if (!props->include_jets) {
+    jet_eff = 0.f;
   }
 
   /* Turn off jets in thin disk mode if we want to do that */
-  if ((props->turn_off_secondary_feedback) &&
-      (bp->accretion_mode == BH_thin_disc)) {
-    jet_eff = 0.;
+  if ((bp->accretion_mode == BH_thin_disc) && (!props->use_jets_in_thin_disc)) {
+    jet_eff = 0.f;
   }
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (jet_eff < 0.) {
+  if (jet_eff < 0.f) {
     error(
         "Something went wrong with calculation of jet efficiency of black "
         "holes. jet_eff is %f instead of jet_eff >= 0.",
@@ -668,14 +748,16 @@ __attribute__((always_inline)) INLINE static float jet_efficiency(
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float rad_efficiency(
-    struct bpart* bp, const struct black_holes_props* props) {
+__attribute__((always_inline)) INLINE static float
+black_hole_radiative_efficiency(struct bpart* bp,
+                                const struct black_holes_props* props) {
 
   /* Calculate Novikov-Thorne efficiency, which will be needed twice. */
-  const float eps_TD = eps_NT(bp->spin);
+  const float eps_TD = eps_Novikov_Thorne(bp->spin);
 
-  /* Define placeholder variable for the result */
-  float rad_eff = -1;
+  /* Define placeholder value for the result. We will assign the final result
+     to this variable. */
+  float rad_eff = -1.f;
 
   if (props->fix_radiative_efficiency) {
     rad_eff = props->radiative_efficiency;
@@ -689,11 +771,11 @@ __attribute__((always_inline)) INLINE static float rad_efficiency(
     } else if (bp->accretion_mode == BH_slim_disc) {
 
       /* Assign Madau 2014 efficiency to the slim disk. */
-      rad_eff = eps_SD(bp->spin, bp->eddington_fraction);
+      rad_eff = eps_slim_disc(bp->spin, bp->eddington_fraction);
     } else {
 
 #ifdef SWIFT_DEBUG_CHECKS
-      if (props->beta_acc > 1.) {
+      if (props->beta_acc > 1.f) {
         error(
             "Something went wrong with calculation of radiative efficiency of "
             " black holes. beta_acc is %f instead of beta_acc < 1.",
@@ -701,30 +783,37 @@ __attribute__((always_inline)) INLINE static float rad_efficiency(
       }
 #endif
 
-      /* Assign Mahadevan 1997 efficiency to the thick disk. */
-      if (bp->eddington_fraction < props->edd_crit_thick) {
-        rad_eff = 4.8 * eps_TD / r_isco(bp->spin) * (1. - props->beta_acc) *
-                  props->delta_ADAF;
+      /* Assign Mahadevan 1997 efficiency to the thick disk. We implement these
+         using Eqns. (29) and (30) from Griffin et al. (2019). */
+      if (bp->eddington_fraction < props->mdot_crit_ADAF) {
+        rad_eff = 4.8f * eps_TD / black_hole_isco_radius(bp->spin) *
+                  (1.f - props->beta_acc) * props->delta_ADAF;
       } else {
-        rad_eff = 2.4 * eps_TD / r_isco(bp->spin) * props->beta_acc *
-                  bp->eddington_fraction * props->alpha_acc_2_inv;
+        rad_eff = 2.4f * eps_TD / black_hole_isco_radius(bp->spin) *
+                  props->beta_acc * bp->eddington_fraction *
+                  props->alpha_acc_2_inv;
+      }
+
+      /* Add contribution of truncated thin disc from larger radii */
+      if (props->accretion_efficiency_mode ==
+          BH_accretion_efficiency_variable) {
+        float R_tr = props->ADIOS_R_in * props->mdot_crit_ADAF *
+                     props->mdot_crit_ADAF * bp->accretion_efficiency *
+                     bp->accretion_efficiency /
+                     (bp->eddington_fraction * bp->eddington_fraction);
+        R_tr = fmaxf(10.f, R_tr);
+        rad_eff += 1.f - sqrtf(1. - 2.f / (3.f * R_tr));
       }
     }
   }
 
   /* Turn off radiative feedback if we want to do that */
   if (props->turn_off_radiative_feedback) {
-    rad_eff = 0.;
-  }
-
-  /* Turn off radiation in the thick disk mode if we want to do that */
-  if ((props->turn_off_secondary_feedback) &&
-      (bp->accretion_mode == BH_thick_disc)) {
-    rad_eff = 0.;
+    rad_eff = 0.f;
   }
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (rad_eff < 0.) {
+  if (rad_eff < 0.f) {
     error(
         "Something went wrong with calculation of radiative efficiency of "
         " black holes. rad_eff is %f instead of rad_eff >= 0.",
@@ -735,6 +824,67 @@ __attribute__((always_inline)) INLINE static float rad_efficiency(
   return rad_eff;
 }
 
+/**
+ * @brief Compute the wind efficiency of a BH particle.
+ *
+ * The result depends on bp->accretion_mode (thick disk, thin disk or
+ * slim disk), with no wind assumed for the thin disc (effectively, the
+ * radiation launches its own wind, while in the thick/slim disc, it is gas
+ * pressure/MHD effects that launch the wind. In all cases, the wind is dumped
+ * as thermal energy, alongside radiation.
+ *
+ * For the thick disk, we take the results from Sadowski et al. (2013)
+ * (2013MNRAS.436.3856S), which is applicable to MAD discs. For the slim disc,
+ * we constructed a fitting function by using the total MHD efficiency from
+ * Ricarte et al. (2023) (2023ApJ...954L..22R), which includes both winds and
+ * jets, and subtracting from that the jet efficiency used by our model.
+ *
+ * @param bp Pointer to the b-particle data.
+ * @param constants Physical constants (in internal units).
+ * @param props Properties of the black hole scheme.
+ */
+__attribute__((always_inline)) INLINE static float black_hole_wind_efficiency(
+    struct bpart* bp, const struct black_holes_props* props) {
+
+  /* (Dimensionless) magnetic flux on the BH horizon, as given by the
+     Narayan et al. (2022) fitting function for MAD discs. */
+  float phi = -20.2f * bp->spin * bp->spin * bp->spin -
+              14.9f * bp->spin * bp->spin + 34.f * bp->spin + 52.6f;
+
+  if (bp->accretion_mode == BH_slim_disc) {
+
+    /* We need to suppress the magnetic flux by an Eddington-ratio-dependent
+       factor (Equation 3 from Ricarte et al. 2023). */
+    float Eddington_ratio = bp->eddington_fraction;
+    phi *= powf(Eddington_ratio / 1.88f, 1.29f) /
+           (1.f + powf(Eddington_ratio / 1.88f, 1.29f));
+    float phi_factor = (1.f + (phi / 50.f) * (phi / 50.f));
+
+    float horizon_ang_vel =
+        bp->spin / (2.f * (1.f + sqrtf(1.f - bp->spin * bp->spin)));
+    float spin_factor =
+        1.f - 8.f * horizon_ang_vel * horizon_ang_vel + 1.f * horizon_ang_vel;
+    if (bp->spin > 0.f) {
+      spin_factor = max(0.4f, spin_factor);
+    } else {
+      spin_factor = max(0.f, spin_factor);
+    }
+
+    /* Final result for slim disc wind efficiency. (Not published
+       yet anywhere) */
+    return props->slim_disc_wind_factor * 0.0635f * phi_factor * spin_factor;
+  } else if (bp->accretion_mode == BH_thick_disc && props->use_ADIOS_winds) {
+
+    /* Equation (29) from Sadowski et al. (2013). */
+    float horizon_ang_vel =
+        bp->spin / (2.f * (1.f + sqrtf(1.f - bp->spin * bp->spin)));
+    return 0.005f * (1.f + 3.f * phi * phi / 2500.f * horizon_ang_vel *
+                               horizon_ang_vel / 0.04f);
+  } else {
+    return 0.f;
+  }
+}
+
 /**
  * @brief Compute the spec. ang. mom. at the inner radius of a BH particle.
  *
@@ -755,31 +905,34 @@ __attribute__((always_inline)) INLINE static float l_acc(
     struct bpart* bp, const struct phys_const* constants,
     const struct black_holes_props* props) {
 
-  /* Define placeholder variable for the result */
-  float L = -1.;
+  /* Define placeholder value for the result. We will assign the final result
+     to this variable. */
+  float L = -1.f;
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (r_isco(bp->spin) <= 0.6667) {
+  if (black_hole_isco_radius(bp->spin) <= 0.6667f) {
     error(
         "Something went wrong with calculation of l_acc of black holes. "
         " r_isco is %f instead of r_isco > 1.",
-        r_isco(bp->spin));
+        black_hole_isco_radius(bp->spin));
   }
 #endif
 
   /* Spec. ang. mom. at ISCO */
-  const float L_ISCO = 0.385 * (1. + 2. * sqrtf(3. * r_isco(bp->spin) - 2.));
+  const float L_ISCO =
+      0.385f *
+      (1.f + 2.f * sqrtf(3.f * black_hole_isco_radius(bp->spin) - 2.f));
 
   /* Branch depending on which accretion mode the BH is in */
   if ((bp->accretion_mode == BH_thick_disc) ||
       (bp->accretion_mode == BH_slim_disc)) {
-    L = 0.45 * L_ISCO;
+    L = 0.45f * L_ISCO;
   } else {
     L = L_ISCO;
   }
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (L <= 0.) {
+  if (L <= 0.f) {
     error(
         "Something went wrong with calculation of l_acc of black holes. "
         " l_acc is %f instead of l_acc > 0.",
@@ -791,51 +944,168 @@ __attribute__((always_inline)) INLINE static float l_acc(
 }
 
 /**
- * @brief Compute the evolution of the spin of a BH particle.
+ * @brief Compute the evolution of the spin of a BH particle. This
+ * spinup/spindown rate is equal to da / dln(M_BH)_0, or
+ * da / (d(M_BH,0)/M_BH ), where the subscript '0' means that it is
+ * the mass increment before losses due to jets, radiation or winds
+ * (i.e. without the effect of efficiencies).
  *
  * The result depends on bp->accretion_mode (thick disk, thin disk or
  * slim disk), due to differing spec. ang. momenta as well as jet and
  * radiative efficiencies.
  *
- * This equation corresponds to eqn. 2 in Benson & Babul (2009), including
- * a jet spindown term.
+ * For the thick disc, we use the jet spindown formula from Narayan et al.
+ * (2022). For the slim and thin disc, we use the formula from Ricarte et al.
+ * (2023).
  *
  * @param bp Pointer to the b-particle data.
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float da_dln_mbh_0(
+__attribute__((always_inline)) INLINE static float black_hole_spinup_rate(
     struct bpart* bp, const struct phys_const* constants,
     const struct black_holes_props* props) {
   const float a = bp->spin;
 
-  if ((a == 0.) || (a < -0.9981) || (a > 0.9981)) {
+  if ((a == 0.f) || (a < -0.9981f) || (a > 0.9981f)) {
     error(
-        "The da_dln_mbh_0 function was called and spin is %f. Spin should "
+        "The spinup function was called and spin is %f. Spin should "
         " not be a = 0, a < -0.998 or a > 0.998.",
         a);
   }
 
-  float spinup_rate = 0.;
-
-  if (props->include_GRMHD_spindown) {
-    if (bp->accretion_mode == BH_thin_disc) {
-      spinup_rate = l_acc(bp, constants, props) -
-                    2. * a * (1. - rad_efficiency(bp, props));
-    } else {
-      spinup_rate = 0.45 - 12.53 * a - 7.8 * a * a + 9.44 * a * a * a +
-                    5.71 * a * a * a * a - 4.03 * a * a * a * a * a;
+  if (bp->accretion_mode == BH_thin_disc && !props->use_jets_in_thin_disc) {
+
+    /* If we are in the thin disc and use no jets, we use the simple spinup /
+     * spindown formula, e.g. from Benson & Babul (2009). This accounts for
+     * accretion only. */
+    return l_acc(bp, constants, props) -
+           2.f * a * (1.f - bp->radiative_efficiency);
+
+  } else if (bp->accretion_mode == BH_thick_disc) {
+
+    /* Fiting function from Narayan et al. (2022) */
+    return 0.45f - 12.53f * a - 7.8f * a * a + 9.44f * a * a * a +
+           5.71f * a * a * a * a - 4.03f * a * a * a * a * a;
+
+  } else if (bp->accretion_mode == BH_slim_disc ||
+             (bp->accretion_mode == BH_thin_disc &&
+              props->use_jets_in_thin_disc)) {
+
+    /* Fitting function from Ricarte et al. (2023). */
+    float Eddington_ratio =
+        bp->eddington_fraction * eps_Novikov_Thorne(bp->spin) / 0.1f;
+    float xi = Eddington_ratio * 0.017f;
+    float s_min = 0.86f - 1.94f * bp->spin;
+    float L_ISCO =
+        0.385f *
+        (1.f + 2.f * sqrtf(3.f * black_hole_isco_radius(bp->spin) - 2.f));
+    float s_thin = L_ISCO - 2.f * a * (1.f - eps_Novikov_Thorne(bp->spin));
+    float s_HD = (s_thin + s_min * xi) / (1.f + xi);
+
+    float horizon_ang_vel =
+        fabsf(bp->spin) / (2.f * (1.f + sqrtf(1.f - bp->spin * bp->spin)));
+    float k_EM = 0.23f;
+    if (bp->spin > 0.f) {
+      k_EM = min(0.1f + 0.5f * bp->spin, 0.35f);
     }
+
+    float s_EM = -1.f * bp->spin / fabsf(bp->spin) * bp->jet_efficiency *
+                 (1.f / (k_EM * horizon_ang_vel) - 2.f * bp->spin);
+    return s_HD + s_EM;
   } else {
-    spinup_rate =
-        l_acc(bp, constants, props) -
-        2. * a * (1. - rad_efficiency(bp, props)) -
-        sqrtf(1. - a * a) / a *
-            (a * a + (1. + sqrtf(1. - a * a)) * (1. + sqrtf(1. - a * a))) *
-            jet_efficiency(bp, props);
+
+#ifdef SWIFT_DEBUG_CHECKS
+    error(
+        "We shouldn't have reached this branch of the "
+        "black_hole_spinup_rate() function!");
+#endif
+
+    return 0;
+  }
+}
+
+/**
+ * @brief Compute the heating temperature used for AGN feedback.
+ *
+ * @param bp The #bpart doing feedback.
+ * @param props Properties of the BH scheme.
+ * @param cosmo The current cosmological model.
+ * @param constants The physical constants (in internal units).
+ */
+__attribute__((always_inline)) INLINE static float black_hole_feedback_delta_T(
+    const struct bpart* bp, const struct black_holes_props* props,
+    const struct cosmology* cosmo, const struct phys_const* constants) {
+
+  float delta_T = -1.f;
+  if (props->AGN_heating_temperature_model ==
+      AGN_heating_temperature_constant) {
+    delta_T = props->AGN_delta_T_desired;
+
+  } else if (props->AGN_heating_temperature_model ==
+             AGN_heating_temperature_local) {
+
+    /* Calculate feedback power */
+    const float feedback_power =
+        bp->radiative_efficiency * props->epsilon_f * bp->accretion_rate *
+        constants->const_speed_light_c * constants->const_speed_light_c;
+
+    /* Get the sound speed of the hot gas in the kernel. Make sure the actual
+     * value that is used is at least the value specified in the parameter
+     * file. */
+    float sound_speed_hot_gas =
+        bp->sound_speed_gas_hot * cosmo->a_factor_sound_speed;
+    sound_speed_hot_gas =
+        max(sound_speed_hot_gas, props->sound_speed_hot_gas_min);
+
+    /* Take the maximum of the sound speed of the hot gas and the gas velocity
+     * dispersion. Calculate the replenishment time-scale by assuming that it
+     * will replenish under the influence of whichever of those two values is
+     * larger. */
+    const float gas_dispersion = bp->velocity_dispersion_gas * cosmo->a_inv;
+    const double replenishment_time_scale =
+        bp->h * cosmo->a / max(sound_speed_hot_gas, gas_dispersion);
+
+    /* Calculate heating temperature from the power, smoothing length (proper,
+       not comoving), neighbour sound speed and neighbour mass. Apply floor. */
+    const float delta_T_repl =
+        (2.f * 0.6f * constants->const_proton_mass * feedback_power *
+         replenishment_time_scale) /
+        (3.f * constants->const_boltzmann_k * bp->ngb_mass);
+
+    /* Calculate heating temperature from the crossing condition, i.e. set the
+     * temperature such that a new particle pair will be heated roughly when
+     * the previous one crosses (exits) the BH kernel on account of its sound-
+     * crossing time-scale. This also depends on power, smoothing length and
+     * neighbour mass (per particle, not total). */
+    const float delta_T_cross =
+        (0.6 * constants->const_proton_mass) / (constants->const_boltzmann_k) *
+        powf(2.f * bp->h * cosmo->a * feedback_power /
+                 (sqrtf(15.f) * bp->ngb_mass / ((double)bp->num_ngbs)),
+             0.6667f);
+
+    /* Calculate minimum temperature from Dalla Vecchia & Schaye (2012) to
+       prevent numerical overcooling. This is in Kelvin. */
+    const float delta_T_min_Dalla_Vecchia =
+        props->normalisation_Dalla_Vecchia *
+        cbrt(bp->ngb_mass / props->ref_ngb_mass_Dalla_Vecchia) *
+        pow(bp->rho_gas * cosmo->a3_inv / props->ref_density_Dalla_Vecchia,
+            2.f / 3.f);
+
+    /* Apply the crossing and replenishment floors */
+    delta_T = fmaxf(delta_T_cross, delta_T_repl);
+
+    /* Apply the Dalla Vecchia floor, and multiply by scaling factor */
+    delta_T = props->delta_T_xi * fmaxf(delta_T, delta_T_min_Dalla_Vecchia);
+
+    /* Apply an additional, constant floor */
+    delta_T = fmaxf(delta_T, props->delta_T_min);
+
+    /* Apply a ceiling */
+    delta_T = fminf(delta_T, props->delta_T_max);
   }
 
-  return spinup_rate;
+  return delta_T;
 }
 
 /**
@@ -853,26 +1123,13 @@ __attribute__((always_inline)) INLINE static float black_hole_feedback_dv_jet(
   float v_jet = -1.;
   if (props->AGN_jet_velocity_model == AGN_jet_velocity_BH_mass) {
 
-    /* Assign the halo mass according to an empirical relation given in the
-       parameter file */
-    const float halo_mass =
-        powf(bp->subgrid_mass / props->v_jet_BH_mass_scaling_reference_mass,
+    v_jet =
+        powf((bp->subgrid_mass / props->v_jet_BH_mass_scaling_reference_mass),
              props->v_jet_BH_mass_scaling_slope);
 
-    /* Get the critical density and virial overdensity at this redshift */
-    const float critical_density = cosmo->critical_density;
-    const float overdensity = cosmo->overdensity_BN98;
-
-    /* Gather the previous factors and compute the virial radius, virial
-       velocity and finally the sound speed in the hot gas */
-    const float virial_radius =
-        cbrtf(3. * halo_mass / (4. * M_PI * overdensity * critical_density));
-    const float virial_velocity =
-        sqrtf(halo_mass * constants->const_newton_G / virial_radius);
-    const float sound_speed = sqrtf(5. / 3. * 0.5) * virial_velocity;
-
-    /* Return the jet velocity as some factor times the sound speed */
-    v_jet = fmaxf(props->v_jet_min, props->v_jet_cs_ratio * sound_speed);
+    /* Apply floor and ceiling values */
+    v_jet = props->v_jet_max * fminf(v_jet, 1.f);
+    v_jet = fmaxf(v_jet, props->v_jet_min);
 
   } else if (props->AGN_jet_velocity_model == AGN_jet_velocity_constant) {
     v_jet = props->v_jet;
@@ -883,33 +1140,56 @@ __attribute__((always_inline)) INLINE static float black_hole_feedback_dv_jet(
        apply a floor value*/
     v_jet = sqrtf(2.f * bp->jet_efficiency / props->v_jet_mass_loading) *
             constants->const_speed_light_c;
+
+    /* Apply floor and ceiling values */
     v_jet = fmaxf(props->v_jet_min, v_jet);
+    v_jet = fminf(props->v_jet_max, v_jet);
 
   } else if (props->AGN_jet_velocity_model == AGN_jet_velocity_local) {
 
     /* Calculate jet power */
-    const float jet_power = bp->jet_efficiency * bp->accretion_rate *
-                            constants->const_speed_light_c *
-                            constants->const_speed_light_c;
-
-    /* Calculate jet velocity from the power, smoothing length (proper, not
-       comoving) and neighbour mass. Then apply floor. */
-    v_jet = props->v_jet_xi * cbrt(jet_power * bp->h * cosmo->a /
-                                   (bp->ngb_mass / ((double)bp->num_ngbs)));
-    v_jet = fmaxf(v_jet, props->v_jet_min);
-
-  } else if (props->AGN_jet_velocity_model == AGN_jet_velocity_sound_speed) {
-
-    /* Calculate jet power */
-    const float jet_power = bp->jet_efficiency * bp->accretion_rate *
-                            constants->const_speed_light_c *
-                            constants->const_speed_light_c;
-
-    /* Calculate jet velocity from the power, smoothing length (proper, not
-       comoving), neighbour sound speed and neighbour mass. Apply floor. */
-    v_jet = props->v_jet_xi * sqrtf(jet_power * bp->h * cosmo->a /
-                                    (bp->ngb_mass * bp->sound_speed_gas));
+    const double jet_power = bp->jet_efficiency * bp->accretion_rate *
+                             constants->const_speed_light_c *
+                             constants->const_speed_light_c;
+
+    /* Get the sound speed of the hot gas in the kernel. Make sure the actual
+     * value that is used is at least the value specified in the parameter
+     * file. */
+    float sound_speed_hot_gas =
+        bp->sound_speed_gas_hot * cosmo->a_factor_sound_speed;
+    sound_speed_hot_gas =
+        max(sound_speed_hot_gas, props->sound_speed_hot_gas_min);
+
+    /* Take the maximum of the sound speed of the hot gas and the gas velocity
+     * dispersion. Calculate the replenishment time-scale by assuming that it
+     * will replenish under the influence of whichever of those two values is
+     * larger. */
+    const float gas_dispersion = bp->velocity_dispersion_gas * cosmo->a_inv;
+    const double replenishment_time_scale =
+        bp->h * cosmo->a / max(sound_speed_hot_gas, gas_dispersion);
+
+    /* Calculate jet velocity from the replenishment condition, taking the
+     * power, smoothing length (proper, not comoving), neighbour sound speed
+     * and (total) neighbour mass. */
+    const float v_jet_repl =
+        sqrtf(jet_power * replenishment_time_scale / (2.f * bp->ngb_mass));
+
+    /* Calculate jet velocity from the crossing condition, i.e. set the
+     * velocity such that a new particle pair will be launched roughly when
+     * the previous one crosses (exits) the BH kernel. This also depends on
+     * power, smoothing length and neighbour mass (per particle, not total). */
+    const float v_jet_cross =
+        cbrtf(bp->h * cosmo->a * jet_power /
+              (4.f * bp->ngb_mass / ((double)bp->num_ngbs)));
+
+    /* Find whichever of these two is the minimum, and multiply it by an
+     * arbitrary scaling factor (whose fiducial value is 1, i.e. no
+     * rescaling. */
+    v_jet = props->v_jet_xi * fmaxf(v_jet_repl, v_jet_cross);
+
+    /* Apply floor and ceiling values */
     v_jet = fmaxf(v_jet, props->v_jet_min);
+    v_jet = fminf(v_jet, props->v_jet_max);
 
   } else {
     error(
@@ -917,7 +1197,7 @@ __attribute__((always_inline)) INLINE static float black_hole_feedback_dv_jet(
         "supported.");
   }
 
-  if (v_jet <= 0.) {
+  if (v_jet <= 0.f) {
     error(
         "The black_hole_feedback_dv_jet returned a value less than 0. which "
         " is v_jet = %f.",
@@ -945,7 +1225,7 @@ __attribute__((always_inline)) INLINE static float black_hole_feedback_dv_jet(
  * @param cos_gamma cosine of the angle between the second spin and the initial
  * total angular momentu
  */
-__attribute__((always_inline)) INLINE static float l_variable(
+__attribute__((always_inline)) INLINE static float black_hole_l_variable(
     const float a1, const float a2, const float q, const float eta,
     const float cos_alpha, const float cos_beta, const float cos_gamma) {
 
@@ -971,38 +1251,6 @@ __attribute__((always_inline)) INLINE static float l_variable(
   return term1 + term2 + term3 + term4 + term5;
 }
 
-/**
- * @brief Auxilliary function used for the calculation of final spin of
- * a BH merger.
- *
- * This implements the fitting formula for the final spin from Barausse &
- * Rezolla (2009), ApJ, 704, Equation 6. It is used in the merger_spin_evolve()
- * function.
- *
- * @param a1 spin of the first (more massive) black hole
- * @param a2 spin of the less massive black hole
- * @param q mass ratio of the two black holes, 0 < q < 1
- * @param cos_alpha cosine of the angle between the two spins
- * @param cos_beta cosine of the angle between the first spin and the initial
- * total angular momentum
- * @param cos_gamma cosine of the angle between the second spin and the initial
- * total angular momentu
- */
-__attribute__((always_inline)) INLINE static float final_spin(
-    const float a1, const float a2, const float q, const float cos_alpha,
-    const float cos_beta, const float cos_gamma, const float l) {
-
-  /* Gather the terms of Eqn. 6 */
-  const float term1 = a1 * a1;
-  const float term2 = a2 * a2 * q * q * q * q;
-  const float term3 = 2.f * a1 * a2 * q * q * cos_alpha;
-  const float term4 = 2.f * (a1 * cos_beta + a2 * q * q * cos_gamma) * l * q;
-  const float term5 = l * l * q * q;
-
-  /* Calculate the final spin */
-  return sqrtf(term1 + term2 + term3 + term4 + term5) / ((1.f + q) * (1.f + q));
-}
-
 /**
  * @brief Auxilliary function used for the calculation of mass lost to GWs.
  *
@@ -1035,14 +1283,14 @@ __attribute__((always_inline)) INLINE static float mass_fraction_lost_to_GWs(
  * @param constants Physical constants (in internal units).
  * @param props Properties of the black hole scheme.
  */
-__attribute__((always_inline)) INLINE static float merger_spin_evolve(
-    struct bpart* bpi, const struct bpart* bpj,
-    const struct phys_const* constants) {
+__attribute__((always_inline)) INLINE static float
+black_hole_merger_spin_evolve(struct bpart* bpi, const struct bpart* bpj,
+                              const struct phys_const* constants) {
 
   /* Check if something is wrong with the masses. This is important and could
      possibly happen as a result of jet spindown and mass loss at any time,
      so we want to know about it. */
-  if ((bpj->subgrid_mass <= 0.) || (bpi->subgrid_mass <= 0.)) {
+  if ((bpj->subgrid_mass <= 0.f) || (bpi->subgrid_mass <= 0.f)) {
     error(
         "Something went wrong with calculation of spin of a black hole "
         " merger remnant. The black hole masses are %f and %f, instead of  > "
@@ -1059,7 +1307,6 @@ __attribute__((always_inline)) INLINE static float merger_spin_evolve(
   const float mass_ratio = m2 / m1;
   const float sym_mass_ratio =
       mass_ratio / ((mass_ratio + 1.f) * (mass_ratio + 1.f));
-  const float reduced_mass = m1 * m2 / (m1 + m2);
 
   /* The absolute values of the spins are also needed */
   const float spin1 = fabsf(bpi->spin);
@@ -1067,7 +1314,7 @@ __attribute__((always_inline)) INLINE static float merger_spin_evolve(
 
   /* Check if the BHs have been spun down to 0. This is again an important
      potential break point, we want to know about it. */
-  if ((spin1 == 0.) || (spin2 == 0.)) {
+  if ((spin1 == 0.f) || (spin2 == 0.f)) {
     error(
         "Something went wrong with calculation of spin of a black hole "
         " merger remnant. The black hole spins are %f and %f, instead of  > 0.",
@@ -1086,19 +1333,52 @@ __attribute__((always_inline)) INLINE static float merger_spin_evolve(
      two BHs, which is used in the fits. Start by defining the coordinates in
      the frame of one of the BHs (it doesn't matter which one, the total
      angular momentum is the same). */
-  const float relative_coordinates[3] = {
-      bpj->x[0] - bpi->x[0], bpj->x[1] - bpi->x[1], bpj->x[2] - bpi->x[2]};
-  const float relative_velocities[3] = {
-      bpj->v[0] - bpi->v[0], bpj->v[1] - bpi->v[1], bpj->v[2] - bpi->v[2]};
+  const float centre_of_mass[3] = {
+      (m1 * bpi->x[0] + m2 * bpj->x[0]) / (m1 + m2),
+      (m1 * bpi->x[1] + m2 * bpj->x[1]) / (m1 + m2),
+      (m1 * bpi->x[2] + m2 * bpj->x[2]) / (m1 + m2)};
+  const float centre_of_mass_vel[3] = {
+      (m1 * bpi->v[0] + m2 * bpj->v[0]) / (m1 + m2),
+      (m1 * bpi->v[1] + m2 * bpj->v[1]) / (m1 + m2),
+      (m1 * bpi->v[2] + m2 * bpj->v[2]) / (m1 + m2)};
+
+  /* Coordinates of each of the BHs in the frame of the centre of mass. */
+  const float relative_coordinates_1[3] = {bpi->x[0] - centre_of_mass[0],
+                                           bpi->x[1] - centre_of_mass[1],
+                                           bpi->x[2] - centre_of_mass[2]};
+  const float relative_coordinates_2[3] = {bpj->x[0] - centre_of_mass[0],
+                                           bpj->x[1] - centre_of_mass[1],
+                                           bpj->x[2] - centre_of_mass[2]};
+
+  /* The velocities of each BH in the centre of mass frame. */
+  const float relative_velocities_1[3] = {bpi->v[0] - centre_of_mass_vel[0],
+                                          bpi->v[1] - centre_of_mass_vel[1],
+                                          bpi->v[2] - centre_of_mass_vel[2]};
+  const float relative_velocities_2[3] = {bpj->v[0] - centre_of_mass_vel[0],
+                                          bpj->v[1] - centre_of_mass_vel[1],
+                                          bpj->v[2] - centre_of_mass_vel[2]};
+
+  /* The angular momentum of each BH in the centre of mass frame. */
+  const float angular_momentum_1[3] = {
+      m1 * (relative_coordinates_1[1] * relative_velocities_1[2] -
+            relative_coordinates_1[2] * relative_velocities_1[1]),
+      m1 * (relative_coordinates_1[2] * relative_velocities_1[0] -
+            relative_coordinates_1[0] * relative_velocities_1[2]),
+      m1 * (relative_coordinates_1[0] * relative_velocities_1[1] -
+            relative_coordinates_1[1] * relative_velocities_1[0])};
+  const float angular_momentum_2[3] = {
+      m2 * (relative_coordinates_2[1] * relative_velocities_2[2] -
+            relative_coordinates_2[2] * relative_velocities_2[1]),
+      m2 * (relative_coordinates_2[2] * relative_velocities_2[0] -
+            relative_coordinates_2[0] * relative_velocities_2[2]),
+      m2 * (relative_coordinates_2[0] * relative_velocities_2[1] -
+            relative_coordinates_2[1] * relative_velocities_2[0])};
 
   /* Calculate the orbital angular momentum itself. */
   const float orbital_angular_momentum[3] = {
-      reduced_mass * (relative_coordinates[1] * relative_velocities[2] -
-                      relative_coordinates[2] * relative_velocities[1]),
-      reduced_mass * (relative_coordinates[2] * relative_velocities[0] -
-                      relative_coordinates[0] * relative_velocities[2]),
-      reduced_mass * (relative_coordinates[0] * relative_velocities[1] -
-                      relative_coordinates[1] * relative_velocities[0])};
+      angular_momentum_1[0] + angular_momentum_2[0],
+      angular_momentum_1[1] + angular_momentum_2[1],
+      angular_momentum_1[2] + angular_momentum_2[2]};
 
   /* Calculate the magnitude of the orbital angular momentum. */
   const float orbital_angular_momentum_magnitude =
@@ -1121,12 +1401,19 @@ __attribute__((always_inline)) INLINE static float merger_spin_evolve(
      system, i.e. including the orbital angular momentum and the spins. This
      is needed since the final spin is assumed to be along the direction of
      this total angular momentum. Hence here we compute the direction. */
+  const float j_BH_1 =
+      fabs(bpi->subgrid_mass * bpi->subgrid_mass * bpi->spin *
+           constants->const_newton_G / constants->const_speed_light_c);
+  const float j_BH_2 =
+      fabs(bpj->subgrid_mass * bpj->subgrid_mass * bpj->spin *
+           constants->const_newton_G / constants->const_speed_light_c);
+
   float total_angular_momentum_direction[3] = {
-      m1 * spin1 * spin_vec1[0] + m2 * spin2 * spin_vec2[0] +
+      j_BH_1 * spin_vec1[0] + j_BH_2 * spin_vec2[0] +
           orbital_angular_momentum[0],
-      m1 * spin1 * spin_vec1[1] + m2 * spin2 * spin_vec2[1] +
+      j_BH_1 * spin_vec1[1] + j_BH_2 * spin_vec2[1] +
           orbital_angular_momentum[1],
-      m1 * spin1 * spin_vec1[2] + m2 * spin2 * spin_vec2[2] +
+      j_BH_1 * spin_vec1[2] + j_BH_2 * spin_vec2[2] +
           orbital_angular_momentum[2]};
 
   /* The above is actually the total angular momentum, so we need to normalize
@@ -1165,11 +1452,28 @@ __attribute__((always_inline)) INLINE static float merger_spin_evolve(
 
   /* Get the variable l used in the fit, see Eqn. 10 in Barausse & Rezolla
      (2009). */
-  const float l = l_variable(spin1, spin2, mass_ratio, sym_mass_ratio,
-                             cos_alpha, cos_beta, cos_gamma);
+  const float l = black_hole_l_variable(
+      spin1, spin2, mass_ratio, sym_mass_ratio, cos_alpha, cos_beta, cos_gamma);
+
+  const float l_vector[3] = {l * orbital_angular_momentum_direction[0],
+                             l * orbital_angular_momentum_direction[1],
+                             l * orbital_angular_momentum_direction[2]};
+
+  /* Final spin vector, constructed from the two spins and the auxilliary l
+     vector. */
+  const float spin_vector[3] = {
+      (spin_vec1[0] +
+       spin_vec2[0] * mass_ratio * mass_ratio * l_vector[0] * mass_ratio) /
+          ((1.f + mass_ratio) * (1.f + mass_ratio)),
+      (spin_vec1[1] +
+       spin_vec2[1] * mass_ratio * mass_ratio * l_vector[1] * mass_ratio) /
+          ((1.f + mass_ratio) * (1.f + mass_ratio)),
+      (spin_vec1[2] +
+       spin_vec2[2] * mass_ratio * mass_ratio * l_vector[2] * mass_ratio) /
+          ((1.f + mass_ratio) * (1.f + mass_ratio))};
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (l < 0.) {
+  if (l < 0.f) {
     error(
         "Something went wrong with calculation of spin of a black hole "
         " merger remnant. The l factor is %f, instead of  >= 0.",
@@ -1177,13 +1481,13 @@ __attribute__((always_inline)) INLINE static float merger_spin_evolve(
   }
 #endif
 
-  /* Calculate the magnitude of final spin from Barausse & Rezolla (2009),
-     Eqn. 6. */
+  /* Get magnitude of the final spin simply as the magnitude of the vector. */
   const float final_spin_magnitude =
-      final_spin(spin1, spin2, mass_ratio, cos_alpha, cos_beta, cos_gamma, l);
+      sqrtf(spin_vector[0] * spin_vector[0] + spin_vector[1] * spin_vector[1] +
+            spin_vector[2] * spin_vector[2]);
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (final_spin_magnitude <= 0.) {
+  if (final_spin_magnitude <= 0.f) {
     error(
         "Something went wrong with calculation of spin of a black hole "
         " merger remnant. The final spin magnitude is %f, instead of > 0.",
@@ -1193,15 +1497,15 @@ __attribute__((always_inline)) INLINE static float merger_spin_evolve(
 
   /* Assign the final spin value to the BH, but also make sure we don't go
      above 0.998 nor below 0.001. */
-  bpi->spin = min(final_spin_magnitude, 0.998);
-  if (fabsf(bpi->spin) < 0.01) {
-    bpi->spin = 0.01;
+  bpi->spin = min(final_spin_magnitude, 0.998f);
+  if (fabsf(bpi->spin) < 0.01f) {
+    bpi->spin = 0.01f;
   }
 
   /* Assign the directions of the spin to the BH. */
-  bpi->angular_momentum_direction[0] = total_angular_momentum_direction[0];
-  bpi->angular_momentum_direction[1] = total_angular_momentum_direction[1];
-  bpi->angular_momentum_direction[2] = total_angular_momentum_direction[2];
+  bpi->angular_momentum_direction[0] = spin_vector[0] / final_spin_magnitude;
+  bpi->angular_momentum_direction[1] = spin_vector[1] / final_spin_magnitude;
+  bpi->angular_momentum_direction[2] = spin_vector[2] / final_spin_magnitude;
 
   /* Finally we also want to calculate the fraction of total mass-energy
      lost during the merger to gravitational waves. We use Eqn. 16 and 18
diff --git a/src/black_holes_properties.h b/src/black_holes_properties.h
index f0f7ae660ebbb56cb7949e517e9f334a137e53c3..c54db15a9317fcd13ac4efa6e6da50344258317f 100644
--- a/src/black_holes_properties.h
+++ b/src/black_holes_properties.h
@@ -33,4 +33,11 @@
 #error "Invalid choice of black hole model"
 #endif
 
+/*! Define a flag to be used to output jet tracer data (or not) */
+#if defined(BLACK_HOLES_SPIN_JET)
+#define with_jets 1
+#else
+#define with_jets 0
+#endif
+
 #endif /* SWIFT_BLACK_HOLES_PROPERTIES_H */
diff --git a/src/cell.c b/src/cell.c
index 232dc14d76689e956bdad2db7d9c48f30165cb7a..26bff361c5d092ed4f14f2c2f7b831f6fef61302 100644
--- a/src/cell.c
+++ b/src/cell.c
@@ -293,6 +293,39 @@ int cell_link_bparts(struct cell *c, struct bpart *bparts) {
   return c->black_holes.count;
 }
 
+/**
+ * @brief Link the cells recursively to the given #sink array.
+ *
+ * @param c The #cell.
+ * @param sinks The #sink array.
+ *
+ * @return The number of particles linked.
+ */
+int cell_link_sinks(struct cell *c, struct sink *sinks) {
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->nodeID == engine_rank)
+    error("Linking foreign particles in a local cell!");
+
+  if (c->sinks.parts != NULL)
+    error("Linking sparts into a cell that was already linked");
+#endif
+
+  c->sinks.parts = sinks;
+  c->sinks.parts_rebuild = sinks;
+
+  /* Fill the progeny recursively, depth-first. */
+  if (c->split) {
+    int offset = 0;
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL)
+        offset += cell_link_sinks(c->progeny[k], &sinks[offset]);
+    }
+  }
+
+  /* Return the total number of linked particles. */
+  return c->sinks.count;
+}
+
 /**
  * @brief Recurse down foreign cells until reaching one with hydro
  * tasks; then trigger the linking of the #part array from that
@@ -413,6 +446,7 @@ void cell_unlink_foreign_particles(struct cell *c) {
   c->hydro.parts = NULL;
   c->stars.parts = NULL;
   c->black_holes.parts = NULL;
+  c->sinks.parts = NULL;
 
   if (c->split) {
     for (int k = 0; k < 8; k++) {
@@ -588,6 +622,7 @@ void cell_clean_links(struct cell *c, void *data) {
   c->stars.prepare2 = NULL;
   c->stars.feedback = NULL;
   c->sinks.swallow = NULL;
+  c->sinks.density = NULL;
   c->sinks.do_sink_swallow = NULL;
   c->sinks.do_gas_swallow = NULL;
   c->black_holes.density = NULL;
@@ -626,6 +661,18 @@ void cell_check_part_drift_point(struct cell *c, void *data) {
         c->hydro.parts[i].time_bin != time_bin_inhibited)
       error("part in an incorrect time-zone! p->ti_drift=%lld ti_drift=%lld",
             c->hydro.parts[i].ti_drift, ti_drift);
+
+  for (int i = 0; i < c->hydro.count; ++i) {
+    const struct part *p = &c->hydro.parts[i];
+    if (p->depth_h == c->depth) {
+      if (!(p->h >= c->h_min_allowed && p->h < c->h_max_allowed) && c->split) {
+        error(
+            "depth_h set incorrectly! c->depth=%d p->depth_h=%d h=%e h_min=%e "
+            "h_max=%e",
+            c->depth, p->depth_h, p->h, c->h_min_allowed, c->h_max_allowed);
+      }
+    }
+  }
 #else
   error("Calling debugging code without debugging flag activated.");
 #endif
@@ -700,6 +747,20 @@ void cell_check_sink_drift_point(struct cell *c, void *data) {
           "sink-part in an incorrect time-zone! sink->ti_drift=%lld "
           "ti_drift=%lld",
           c->sinks.parts[i].ti_drift, ti_drift);
+
+  for (int i = 0; i < c->sinks.count; ++i) {
+    const struct sink *sp = &c->sinks.parts[i];
+    if (sp->depth_h == c->depth) {
+      if (!(sp->h >= c->h_min_allowed && sp->h < c->h_max_allowed) &&
+          c->split) {
+        error(
+            "depth_h set incorrectly! c->depth=%d sp->depth_h=%d h=%e h_min=%e "
+            "h_max=%e",
+            c->depth, sp->depth_h, sp->h, c->h_min_allowed, c->h_max_allowed);
+      }
+    }
+  }
+
 #else
   error("Calling debugging code without debugging flag activated.");
 #endif
@@ -734,8 +795,69 @@ void cell_check_spart_drift_point(struct cell *c, void *data) {
   for (int i = 0; i < c->stars.count; ++i)
     if (c->stars.parts[i].ti_drift != ti_drift &&
         c->stars.parts[i].time_bin != time_bin_inhibited)
-      error("g-part in an incorrect time-zone! gp->ti_drift=%lld ti_drift=%lld",
+      error("s-part in an incorrect time-zone! sp->ti_drift=%lld ti_drift=%lld",
             c->stars.parts[i].ti_drift, ti_drift);
+
+  for (int i = 0; i < c->stars.count; ++i) {
+    const struct spart *p = &c->stars.parts[i];
+    if (p->depth_h == c->depth) {
+      if (!(p->h >= c->h_min_allowed && p->h < c->h_max_allowed) && c->split) {
+        error(
+            "depth_h set incorrectly! c->depth=%d p->depth_h=%d h=%e h_min=%e "
+            "h_max=%e",
+            c->depth, p->depth_h, p->h, c->h_min_allowed, c->h_max_allowed);
+      }
+    }
+  }
+#else
+  error("Calling debugging code without debugging flag activated.");
+#endif
+}
+
+/**
+ * @brief Checks that the #bpart in a cell are at the
+ * current point in time
+ *
+ * Calls error() if the cell is not at the current time.
+ *
+ * @param c Cell to act upon
+ * @param data The current time on the integer time-line
+ */
+void cell_check_bpart_drift_point(struct cell *c, void *data) {
+#ifdef SWIFT_DEBUG_CHECKS
+
+  const integertime_t ti_drift = *(integertime_t *)data;
+
+  /* Only check local cells */
+  if (c->nodeID != engine_rank) return;
+
+  /* Only check cells with content */
+  if (c->black_holes.count == 0) return;
+
+  if (c->black_holes.ti_old_part != ti_drift)
+    error(
+        "Cell in an incorrect time-zone! c->black_holes.ti_old_part=%lld "
+        "ti_drift=%lld",
+        c->black_holes.ti_old_part, ti_drift);
+
+  for (int i = 0; i < c->black_holes.count; ++i)
+    if (c->black_holes.parts[i].ti_drift != ti_drift &&
+        c->black_holes.parts[i].time_bin != time_bin_inhibited)
+      error("s-part in an incorrect time-zone! sp->ti_drift=%lld ti_drift=%lld",
+            c->black_holes.parts[i].ti_drift, ti_drift);
+
+  for (int i = 0; i < c->black_holes.count; ++i) {
+    const struct bpart *bp = &c->black_holes.parts[i];
+    if (bp->depth_h == c->depth) {
+      if (!(bp->h >= c->h_min_allowed && bp->h < c->h_max_allowed) &&
+          c->split) {
+        error(
+            "depth_h set incorrectly! c->depth=%d p->depth_h=%d h=%e h_min=%e "
+            "h_max=%e",
+            c->depth, bp->depth_h, bp->h, c->h_min_allowed, c->h_max_allowed);
+      }
+    }
+  }
 #else
   error("Calling debugging code without debugging flag activated.");
 #endif
@@ -1034,6 +1156,9 @@ void cell_clean(struct cell *c) {
   /* Stars */
   cell_free_stars_sorts(c);
 
+  /* Grid */
+  cell_free_grid(c);
+
   /* Recurse */
   for (int k = 0; k < 8; k++)
     if (c->progeny[k]) cell_clean(c->progeny[k]);
diff --git a/src/cell.h b/src/cell.h
index 7e9de839e6b87cb8a7f73f07e706b5985cf8b527..8756d874adfe77c51355061d68be4f070bab6147 100644
--- a/src/cell.h
+++ b/src/cell.h
@@ -35,6 +35,7 @@
 #include "align.h"
 #include "cell_black_holes.h"
 #include "cell_grav.h"
+#include "cell_grid.h"
 #include "cell_hydro.h"
 #include "cell_rt.h"
 #include "cell_sinks.h"
@@ -201,7 +202,7 @@ struct pcell {
     int count;
 
     /*! Maximal cut off radius. */
-    float r_cut_max;
+    float h_max;
 
     /*! Minimal integer end-of-timestep in this cell for sinks tasks */
     integertime_t ti_end_min;
@@ -211,6 +212,12 @@ struct pcell {
 
   } sinks;
 
+  /*! Grid variables */
+  struct {
+    /*! self complete flag */
+    enum grid_completeness self_completeness;
+  } grid;
+
   /*! RT variables */
   struct {
 
@@ -273,6 +280,15 @@ struct pcell_step {
     float dx_max_part;
   } black_holes;
 
+  struct {
+
+    /*! Minimal integer end-of-timestep in this cell (sinks) */
+    integertime_t ti_end_min;
+
+    /*! Maximal distance any #part has travelled since last rebuild */
+    float dx_max_part;
+  } sinks;
+
   struct {
 
     /*! Minimal integer end-of-timestep in this cell (rt) */
@@ -285,34 +301,32 @@ struct pcell_step {
 };
 
 /**
- * @brief Cell information to propagate the new counts of star particles.
+ * @brief Cell information to propagate the new counts of star particles (star
+ * variables).
  */
-struct pcell_sf {
+struct pcell_sf_stars {
 
-  /*! Stars variables */
-  struct {
+  /* Distance by which the stars pointer has moved since the last rebuild */
+  ptrdiff_t delta_from_rebuild;
 
-    /* Distance by which the stars pointer has moved since the last rebuild */
-    ptrdiff_t delta_from_rebuild;
-
-    /* Number of particles in the cell */
-    int count;
-
-    /*! Maximum part movement in this cell since last construction. */
-    float dx_max_part;
-
-  } stars;
+  /* Number of particles in the cell */
+  int count;
 
-  /*! Grav. variables */
-  struct {
+  /*! Maximum part movement in this cell since last construction. */
+  float dx_max_part;
+};
 
-    /* Distance by which the gpart pointer has moved since the last rebuild */
-    ptrdiff_t delta_from_rebuild;
+/**
+ * @brief Cell information to propagate the new counts of star particles (grav
+ * variables).
+ */
+struct pcell_sf_grav {
 
-    /* Number of particles in the cell */
-    int count;
+  /* Distance by which the gpart pointer has moved since the last rebuild */
+  ptrdiff_t delta_from_rebuild;
 
-  } grav;
+  /* Number of particles in the cell */
+  int count;
 };
 
 /**
@@ -395,6 +409,9 @@ struct cell {
   /*! Sink particles variables */
   struct cell_sinks sinks;
 
+  /*! The grid variables */
+  struct cell_grid grid;
+
   /*! Radiative transfer variables */
   struct cell_rt rt;
 
@@ -460,6 +477,14 @@ struct cell {
   /*! Minimum dimension, i.e. smallest edge of this cell (min(width)). */
   float dmin;
 
+  /*! When walking the tree and running loops at different level, this is
+   * the minimal h that can be processed at this level */
+  float h_min_allowed;
+
+  /*! When walking the tree and running loops at different level, this is
+   * the maximal h that can be processed at this level */
+  float h_max_allowed;
+
   /*! ID of the previous owner, e.g. runner. */
   short int owner;
 
@@ -469,9 +494,6 @@ struct cell {
   /*! ID of the node this cell lives on. */
   int nodeID;
 
-  /*! Number of tasks that are associated with this cell. */
-  short int nr_tasks;
-
   /*! The depth of this cell in the tree. */
   char depth;
 
@@ -488,6 +510,9 @@ struct cell {
 
 #ifdef SWIFT_DEBUG_CHECKS
 
+  /*! Number of tasks that are associated with this cell. */
+  short int nr_tasks;
+
   /*! The list of tasks that have been executed on this cell */
   char tasks_executed[task_type_count];
 
@@ -535,19 +560,26 @@ void cell_unpack_bpart_swallow(struct cell *c,
                                const struct black_holes_bpart_data *data);
 int cell_pack_tags(const struct cell *c, int *tags);
 int cell_unpack_tags(const int *tags, struct cell *c);
+int cell_pack_grid_extra(const struct cell *c,
+                         enum grid_construction_level *info);
+int cell_unpack_grid_extra(const enum grid_construction_level *info,
+                           struct cell *c, struct cell *construction_level);
 int cell_pack_end_step(const struct cell *c, struct pcell_step *pcell);
 int cell_unpack_end_step(struct cell *c, const struct pcell_step *pcell);
 void cell_pack_timebin(const struct cell *const c, timebin_t *const t);
 void cell_unpack_timebin(struct cell *const c, timebin_t *const t);
 int cell_pack_multipoles(struct cell *c, struct gravity_tensors *m);
 int cell_unpack_multipoles(struct cell *c, struct gravity_tensors *m);
-int cell_pack_sf_counts(struct cell *c, struct pcell_sf *pcell);
-int cell_unpack_sf_counts(struct cell *c, struct pcell_sf *pcell);
+int cell_pack_sf_counts(struct cell *c, struct pcell_sf_stars *pcell);
+int cell_unpack_sf_counts(struct cell *c, struct pcell_sf_stars *pcell);
+int cell_pack_grav_counts(struct cell *c, struct pcell_sf_grav *pcell);
+int cell_unpack_grav_counts(struct cell *c, struct pcell_sf_grav *pcell);
 int cell_get_tree_size(struct cell *c);
 int cell_link_parts(struct cell *c, struct part *parts);
 int cell_link_gparts(struct cell *c, struct gpart *gparts);
 int cell_link_sparts(struct cell *c, struct spart *sparts);
 int cell_link_bparts(struct cell *c, struct bpart *bparts);
+int cell_link_sinks(struct cell *c, struct sink *sinks);
 int cell_link_foreign_parts(struct cell *c, struct part *parts);
 int cell_link_foreign_gparts(struct cell *c, struct gpart *gparts);
 void cell_unlink_foreign_particles(struct cell *c);
@@ -563,6 +595,7 @@ void cell_clean(struct cell *c);
 void cell_check_part_drift_point(struct cell *c, void *data);
 void cell_check_gpart_drift_point(struct cell *c, void *data);
 void cell_check_spart_drift_point(struct cell *c, void *data);
+void cell_check_bpart_drift_point(struct cell *c, void *data);
 void cell_check_sink_drift_point(struct cell *c, void *data);
 void cell_check_multipole_drift_point(struct cell *c, void *data);
 void cell_reset_task_counters(struct cell *c);
@@ -632,6 +665,13 @@ void cell_activate_limiter(struct cell *c, struct scheduler *s);
 void cell_clear_drift_flags(struct cell *c, void *data);
 void cell_clear_limiter_flags(struct cell *c, void *data);
 void cell_set_super_mapper(void *map_data, int num_elements, void *extra_data);
+void cell_grid_update_self_completeness(struct cell *c, int force);
+void cell_set_grid_completeness_mapper(void *map_data, int num_elements,
+                                       void *extra_data);
+void cell_set_grid_construction_level_mapper(void *map_data, int num_elements,
+                                             void *extra_data);
+void cell_grid_set_self_completeness_mapper(void *map_data, int num_elements,
+                                            void *extra_data);
 void cell_check_spart_pos(const struct cell *c,
                           const struct spart *global_sparts);
 void cell_check_sort_flags(const struct cell *c);
@@ -665,7 +705,8 @@ struct sink *cell_convert_part_to_sink(struct engine *e, struct cell *c,
                                        struct part *p, struct xpart *xp);
 void cell_reorder_extra_parts(struct cell *c, const ptrdiff_t parts_offset);
 void cell_reorder_extra_gparts(struct cell *c, struct part *parts,
-                               struct spart *sparts, struct sink *sinks);
+                               struct spart *sparts, struct sink *sinks,
+                               struct bpart *bparts);
 void cell_reorder_extra_sparts(struct cell *c, const ptrdiff_t sparts_offset);
 void cell_reorder_extra_sinks(struct cell *c, const ptrdiff_t sinks_offset);
 int cell_can_use_pair_mm(const struct cell *ci, const struct cell *cj,
@@ -768,6 +809,36 @@ cell_can_recurse_in_pair_hydro_task(const struct cell *c) {
                        c->hydro.dx_max_part_old) < 0.5f * c->dmin);
 }
 
+/**
+ * @brief Can a sub-pair hydro task recurse to a lower level based
+ * on the status of the particles in the cell.
+ *
+ * @param c The #cell.
+ */
+__attribute__((always_inline)) INLINE static int
+cell_can_recurse_in_subpair_hydro_task(const struct cell *c) {
+
+  /* If so, is the cut-off radius plus the max distance the parts have moved */
+  /* smaller than the sub-cell sizes ? */
+  return ((kernel_gamma * c->hydro.h_max_active + c->hydro.dx_max_part_old) <
+          0.5f * c->dmin);
+}
+
+/**
+ * @brief Can a sub-pair hydro task recurse to a lower level based
+ * on the status of the particles in the cell.
+ *
+ * @param c The #cell.
+ */
+__attribute__((always_inline)) INLINE static int
+cell_can_recurse_in_subpair2_hydro_task(const struct cell *c) {
+
+  /* If so, is the cut-off radius plus the max distance the parts have moved */
+  /* smaller than the sub-cell sizes ? */
+  return ((kernel_gamma * c->hydro.h_max + c->hydro.dx_max_part) <
+          0.5f * c->dmin);
+}
+
 /**
  * @brief Can a sub-self hydro task recurse to a lower level based
  * on the status of the particles in the cell.
@@ -781,6 +852,32 @@ cell_can_recurse_in_self_hydro_task(const struct cell *c) {
   return c->split && (kernel_gamma * c->hydro.h_max_old < 0.5f * c->dmin);
 }
 
+/**
+ * @brief Can a sub-self hydro task recurse to a lower level based
+ * on the status of the particles in the cell.
+ *
+ * @param c The #cell.
+ */
+__attribute__((always_inline)) INLINE static int
+cell_can_recurse_in_subself_hydro_task(const struct cell *c) {
+
+  /* Is the cell not smaller than the smoothing length? */
+  return (kernel_gamma * c->hydro.h_max_active < 0.5f * c->dmin);
+}
+
+/**
+ * @brief Can a hydro task recurse to a lower level based
+ * on the status of the particles in the cell.
+ *
+ * @param c The #cell.
+ */
+__attribute__((always_inline)) INLINE static int
+cell_can_recurse_in_subself2_hydro_task(const struct cell *c) {
+
+  /* Is the cell split and not smaller than the smoothing length? */
+  return c->split && (kernel_gamma * c->hydro.h_max < 0.5f * c->dmin);
+}
+
 /**
  * @brief Can a sub-pair star task recurse to a lower level based
  * on the status of the particles in the cell.
@@ -789,18 +886,29 @@ cell_can_recurse_in_self_hydro_task(const struct cell *c) {
  * @param cj The #cell with hydro parts.
  */
 __attribute__((always_inline)) INLINE static int
-cell_can_recurse_in_pair_stars_task(const struct cell *ci,
-                                    const struct cell *cj) {
+cell_can_recurse_in_pair_stars_task(const struct cell *c) {
 
   /* Is the cell split ? */
   /* If so, is the cut-off radius plus the max distance the parts have moved */
   /* smaller than the sub-cell sizes ? */
   /* Note: We use the _old values as these might have been updated by a drift */
-  return ci->split && cj->split &&
-         ((kernel_gamma * ci->stars.h_max_old + ci->stars.dx_max_part_old) <
-          0.5f * ci->dmin) &&
-         ((kernel_gamma * cj->hydro.h_max_old + cj->hydro.dx_max_part_old) <
-          0.5f * cj->dmin);
+  return c->split && ((kernel_gamma * c->stars.h_max_old +
+                       c->stars.dx_max_part_old) < 0.5f * c->dmin);
+}
+
+/**
+ * @brief Can a sub-pair stars task recurse to a lower level based
+ * on the status of the particles in the cell.
+ *
+ * @param c The #cell.
+ */
+__attribute__((always_inline)) INLINE static int
+cell_can_recurse_in_subpair_stars_task(const struct cell *c) {
+
+  /* If so, is the cut-off radius plus the max distance the parts have moved */
+  /* smaller than the sub-cell sizes ? */
+  return ((kernel_gamma * c->stars.h_max_active + c->stars.dx_max_part_old) <
+          0.5f * c->dmin);
 }
 
 /**
@@ -817,6 +925,19 @@ cell_can_recurse_in_self_stars_task(const struct cell *c) {
          (kernel_gamma * c->hydro.h_max_old < 0.5f * c->dmin);
 }
 
+/**
+ * @brief Can a sub-self stars task recurse to a lower level based
+ * on the status of the particles in the cell.
+ *
+ * @param c The #cell.
+ */
+__attribute__((always_inline)) INLINE static int
+cell_can_recurse_in_subself_stars_task(const struct cell *c) {
+
+  /* Is the cell not smaller than the smoothing length? */
+  return (kernel_gamma * c->stars.h_max_active < 0.5f * c->dmin);
+}
+
 /**
  * @brief Can a sub-pair sink task recurse to a lower level based
  * on the status of the particles in the cell.
@@ -833,7 +954,7 @@ cell_can_recurse_in_pair_sinks_task(const struct cell *ci,
   /* smaller than the sub-cell sizes ? */
   /* Note: We use the _old values as these might have been updated by a drift */
   return ci->split && cj->split &&
-         ((ci->sinks.r_cut_max_old + ci->sinks.dx_max_part_old) <
+         ((kernel_gamma * ci->sinks.h_max_old + ci->sinks.dx_max_part_old) <
           0.5f * ci->dmin) &&
          ((kernel_gamma * cj->hydro.h_max_old + cj->hydro.dx_max_part_old) <
           0.5f * cj->dmin);
@@ -886,7 +1007,7 @@ __attribute__((always_inline)) INLINE static int
 cell_can_recurse_in_self_sinks_task(const struct cell *c) {
 
   /* Is the cell split and not smaller than the smoothing length? */
-  return c->split && (c->sinks.r_cut_max_old < 0.5f * c->dmin) &&
+  return c->split && (kernel_gamma * c->sinks.h_max_old < 0.5f * c->dmin) &&
          (kernel_gamma * c->hydro.h_max_old < 0.5f * c->dmin);
 }
 
@@ -992,6 +1113,45 @@ cell_need_rebuild_for_hydro_pair(const struct cell *ci, const struct cell *cj) {
   return 0;
 }
 
+/**
+ * @brief Have gas particles in a pair of cells moved too much, invalidating the
+ * completeness criterion for ci?
+ *
+ * This function returns true the grid completeness criterion from the
+ * perspective of ci (the #cell for which the grid will be constructed) is no
+ * longer valid.
+ *
+ * NOTE: This function assumes that the self_completeness flags are up to date.
+ * NOTE: This whether a cell needs a rebuild for a grid construction pair is an
+ * asymmetric property, so it should always be checked both ways!
+ *
+ * @param ci The first #cell. This is the cell for which the grid will be
+ * constructed.
+ * @param cj The second #cell. This is the neighbouring cell whose particles are
+ * used as ghost particles.
+ * @return Whether completeness of ci is invalidated by the pair (ci, cj).
+ */
+__attribute__((always_inline, nonnull)) INLINE static int
+cell_grid_pair_invalidates_completeness(struct cell *ci, struct cell *cj) {
+
+  /* Check completeness criteria */
+  /* NOTE: Both completeness flags should already be updated at this point */
+  const int ci_self_complete = ci->grid.self_completeness == grid_complete;
+  const int cj_self_complete = cj->grid.self_completeness == grid_complete;
+  if (!ci_self_complete) return 1;
+#ifdef SHADOWSWIFT_RELAXED_COMPLETENESS
+  /* If if ci is self-complete and cj is not, but its maximal search radius is
+   * sufficiently small, we still consider the pair (ci, cj) complete.
+   * NOTE: ci->dmin == cj->dmin */
+  if (!cj_self_complete && kernel_gamma * ci->hydro.h_max > 0.5 * cj->dmin)
+    return 1;
+#else
+  if (!ci_self_complete || !cj_self_complete) return 1;
+#endif
+
+  return 0;
+}
+
 /**
  * @brief Have star particles in a pair of cells moved too much and require a
  * rebuild?
@@ -1026,7 +1186,7 @@ cell_need_rebuild_for_sinks_pair(const struct cell *ci, const struct cell *cj) {
   /* Is the cut-off radius plus the max distance the parts in both cells have */
   /* moved larger than the cell size ? */
   /* Note ci->dmin == cj->dmin */
-  if (max(ci->sinks.r_cut_max, kernel_gamma * cj->hydro.h_max) +
+  if (max(kernel_gamma * ci->sinks.h_max, kernel_gamma * cj->hydro.h_max) +
           ci->sinks.dx_max_part + cj->hydro.dx_max_part >
       cj->dmin) {
     return 1;
@@ -1313,6 +1473,32 @@ cell_get_stars_sorts(const struct cell *c, const int sid) {
   return &c->stars.sort[j * (c->stars.count + 1)];
 }
 
+/**
+ * @brief Free grid memory for cell.
+ *
+ * @param c The #cell.
+ */
+__attribute__((always_inline)) INLINE static void cell_free_grid(
+    struct cell *c) {
+
+#ifndef MOVING_MESH
+  /* Nothing to do as we have no tessellations */
+#else
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->grid.construction_level != c && c->grid.voronoi != NULL)
+    error("Grid allocated, but not on grid construction level!");
+#endif
+  if (c->grid.construction_level == c) {
+    if (c->grid.voronoi != NULL) {
+      voronoi_destroy(c->grid.voronoi);
+      c->grid.voronoi = NULL;
+    }
+  }
+#endif
+}
+
+void cell_free_grid_rec(struct cell *c);
+
 /**
  * @brief Set the given flag for the given cell.
  */
@@ -1456,4 +1642,145 @@ __attribute__((always_inline)) INLINE void cell_assign_cell_index(
 #endif
 }
 
+/**
+ * @brief Set the depth_h field of a #part.
+ *
+ * @param p The #part.
+ * @param leaf_cell The leaf cell where the particle is located.
+ */
+__attribute__((always_inline)) static INLINE void cell_set_part_h_depth(
+    struct part *p, const struct cell *leaf_cell) {
+
+  const float h = p->h;
+  const struct cell *c = leaf_cell;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (leaf_cell->split) error("Running on an unsplit cell!");
+#endif
+
+  /* Case where h is much smaller than the leaf cell itself */
+  if (h < c->h_min_allowed) {
+    p->depth_h = c->depth;
+    return;
+  }
+
+  /* Climb the tree to find the correct level */
+  while (c != NULL) {
+    if (h >= c->h_min_allowed && h < c->h_max_allowed) {
+      p->depth_h = c->depth;
+      return;
+    }
+    c = c->parent;
+  }
+
+#ifdef SWIFT_DEBUG_CHECKS
+  error("Could not find an appropriate depth!");
+#endif
+}
+
+/**
+ * @brief Set the depth_h field of a #sink.
+ *
+ * @param sp The #sink.
+ * @param leaf_cell The leaf cell where the particle is located.
+ */
+__attribute__((always_inline)) static INLINE void cell_set_sink_h_depth(
+    struct sink *sp, const struct cell *leaf_cell) {
+
+  const float h = sp->h;
+  const struct cell *c = leaf_cell;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (leaf_cell->split) error("Running on an unsplit cell!");
+#endif
+
+  /* Case where h is much smaller than the leaf cell itself */
+  if (h < c->h_min_allowed) {
+    sp->depth_h = c->depth;
+    return;
+  }
+
+  /* Climb the tree to find the correct level */
+  while (c != NULL) {
+    if (h >= c->h_min_allowed && h < c->h_max_allowed) {
+      sp->depth_h = c->depth;
+      return;
+    }
+    c = c->parent;
+  }
+#ifdef SWIFT_DEBUG_CHECKS
+  error("Could not find an appropriate depth!");
+#endif
+}
+
+/**
+ * @brief Set the depth_h field of a #spart.
+ *
+ * @param sp The #spart.
+ * @param leaf_cell The leaf cell where the particle is located.
+ */
+__attribute__((always_inline)) static INLINE void cell_set_spart_h_depth(
+    struct spart *sp, const struct cell *leaf_cell) {
+
+  const float h = sp->h;
+  const struct cell *c = leaf_cell;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (leaf_cell->split) error("Running on an unsplit cell!");
+#endif
+
+  /* Case where h is much smaller than the leaf cell itself */
+  if (h < c->h_min_allowed) {
+    sp->depth_h = c->depth;
+    return;
+  }
+
+  /* Climb the tree to find the correct level */
+  while (c != NULL) {
+    if (h >= c->h_min_allowed && h < c->h_max_allowed) {
+      sp->depth_h = c->depth;
+      return;
+    }
+    c = c->parent;
+  }
+#ifdef SWIFT_DEBUG_CHECKS
+  error("Could not find an appropriate depth!");
+#endif
+}
+
+/**
+ * @brief Set the depth_h field of a #bpart.
+ *
+ * @param bp The #bpart.
+ * @param leaf_cell The leaf cell where the particle is located.
+ */
+__attribute__((always_inline)) static INLINE void cell_set_bpart_h_depth(
+    struct bpart *bp, const struct cell *leaf_cell) {
+
+  const float h = bp->h;
+  const struct cell *c = leaf_cell;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (leaf_cell->split) error("Running on an unsplit cell!");
+#endif
+
+  /* Case where h is much smaller than the leaf cell itself */
+  if (h < c->h_min_allowed) {
+    bp->depth_h = c->depth;
+    return;
+  }
+
+  /* Climb the tree to find the correct level */
+  while (c != NULL) {
+    if (h >= c->h_min_allowed && h < c->h_max_allowed) {
+      bp->depth_h = c->depth;
+      return;
+    }
+    c = c->parent;
+  }
+#ifdef SWIFT_DEBUG_CHECKS
+  error("Could not find an appropriate depth!");
+#endif
+}
+
 #endif /* SWIFT_CELL_H */
diff --git a/src/cell_black_holes.h b/src/cell_black_holes.h
index fb8a779b4b7c33b8db6d59b44f6480307e2fb36c..01ed209c9d7d988613f13f6efaa80e647ad1668f 100644
--- a/src/cell_black_holes.h
+++ b/src/cell_black_holes.h
@@ -55,9 +55,9 @@ struct cell_black_holes {
     struct task *density_ghost;
 
     /*! The ghost tasks related to BH swallowing */
-    struct task *swallow_ghost_0;
     struct task *swallow_ghost_1;
     struct task *swallow_ghost_2;
+    struct task *swallow_ghost_3;
 
     /*! Linked list of the tasks computing this cell's BH density. */
     struct link *density;
diff --git a/src/cell_convert_part.c b/src/cell_convert_part.c
index 78f430a07c3c91150fc87482edb4235108f46363..f7f54beb6f23123dc7202c0a21208b22034f5689 100644
--- a/src/cell_convert_part.c
+++ b/src/cell_convert_part.c
@@ -26,6 +26,7 @@
 #include "cell.h"
 
 /* Local headers. */
+#include "active.h"
 #include "engine.h"
 #include "hydro.h"
 #include "sink_properties.h"
@@ -220,6 +221,7 @@ struct spart *cell_add_spart(struct engine *e, struct cell *const c) {
 
     /* Update the spart->gpart links (shift by 1) */
     for (size_t i = 0; i < n_copy; ++i) {
+
 #ifdef SWIFT_DEBUG_CHECKS
       if (c->stars.parts[i + 1].gpart == NULL) {
         error("Incorrectly linked spart!");
@@ -264,6 +266,9 @@ struct spart *cell_add_spart(struct engine *e, struct cell *const c) {
   sp->ti_drift = e->ti_current;
 #endif
 
+  /* Give the new particle the correct depth */
+  cell_set_spart_h_depth(sp, c);
+
   /* Register that we used one of the free slots. */
   const size_t one = 1;
   atomic_sub(&e->s->nr_extra_sparts, one);
@@ -288,7 +293,7 @@ struct spart *cell_add_spart(struct engine *e, struct cell *const c) {
 struct sink *cell_add_sink(struct engine *e, struct cell *const c) {
   /* Perform some basic consitency checks */
   if (c->nodeID != engine_rank) error("Adding sink on a foreign node");
-  if (c->grav.ti_old_part != e->ti_current) error("Undrifted cell!");
+  if (c->sinks.ti_old_part != e->ti_current) error("Undrifted cell!");
   if (c->split) error("Addition of sink performed above the leaf level");
 
   /* Progeny number at each level */
@@ -353,6 +358,11 @@ struct sink *cell_add_sink(struct engine *e, struct cell *const c) {
 
     /* Update the sink->gpart links (shift by 1) */
     for (size_t i = 0; i < n_copy; ++i) {
+
+      // TODO: Matthieu figure out whether this is strictly needed
+      /* Skip inhibited (swallowed) sink particles */
+      if (sink_is_inhibited(&c->sinks.parts[i + 1], e)) continue;
+
 #ifdef SWIFT_DEBUG_CHECKS
       if (c->sinks.parts[i + 1].gpart == NULL) {
         error("Incorrectly linked sink!");
@@ -397,6 +407,9 @@ struct sink *cell_add_sink(struct engine *e, struct cell *const c) {
   sp->ti_drift = e->ti_current;
 #endif
 
+  /* Give the new particle the correct depth */
+  cell_set_sink_h_depth(sp, c);
+
   /* Register that we used one of the free slots. */
   const size_t one = 1;
   atomic_sub(&e->s->nr_extra_sinks, one);
@@ -489,6 +502,10 @@ struct gpart *cell_add_gpart(struct engine *e, struct cell *c) {
     /* Update the gpart->spart links (shift by 1) */
     struct gpart *gparts = c->grav.parts;
     for (size_t i = 0; i < n_copy; ++i) {
+
+      /* Skip inhibited particles */
+      if (gpart_is_inhibited(&c->grav.parts[i + 1], e)) continue;
+
       if (gparts[i + 1].type == swift_type_gas) {
         s->parts[-gparts[i + 1].id_or_neg_offset].gpart++;
       } else if (gparts[i + 1].type == swift_type_stars) {
@@ -572,7 +589,7 @@ void cell_remove_part(const struct engine *e, struct cell *c, struct part *p,
   /* Mark the gpart as inhibited and stand-alone */
   if (p->gpart) {
     p->gpart->time_bin = time_bin_inhibited;
-    p->gpart->id_or_neg_offset = p->id;
+    p->gpart->id_or_neg_offset = 1;
     p->gpart->type = swift_type_dark_matter;
   }
 
@@ -645,7 +662,7 @@ void cell_remove_spart(const struct engine *e, struct cell *c,
   sp->time_bin = time_bin_inhibited;
   if (sp->gpart) {
     sp->gpart->time_bin = time_bin_inhibited;
-    sp->gpart->id_or_neg_offset = sp->id;
+    sp->gpart->id_or_neg_offset = 1;
     sp->gpart->type = swift_type_dark_matter;
   }
 
@@ -684,7 +701,7 @@ void cell_remove_bpart(const struct engine *e, struct cell *c,
   bp->time_bin = time_bin_inhibited;
   if (bp->gpart) {
     bp->gpart->time_bin = time_bin_inhibited;
-    bp->gpart->id_or_neg_offset = bp->id;
+    bp->gpart->id_or_neg_offset = 1;
     bp->gpart->type = swift_type_dark_matter;
   }
 
@@ -722,7 +739,7 @@ void cell_remove_sink(const struct engine *e, struct cell *c,
   sink->time_bin = time_bin_inhibited;
   if (sink->gpart) {
     sink->gpart->time_bin = time_bin_inhibited;
-    sink->gpart->id_or_neg_offset = sink->id;
+    sink->gpart->id_or_neg_offset = 1;
     sink->gpart->type = swift_type_dark_matter;
   }
 
@@ -905,6 +922,9 @@ struct spart *cell_convert_part_to_spart(struct engine *e, struct cell *c,
   /* Set a smoothing length */
   sp->h = p->h;
 
+  /* Give the new particle the correct depth */
+  cell_set_spart_h_depth(sp, c);
+
   /* Here comes the Sun! */
   return sp;
 }
@@ -983,6 +1003,9 @@ struct spart *cell_spawn_new_spart_from_part(struct engine *e, struct cell *c,
   /* Set a smoothing length */
   sp->h = p->h;
 
+  /* Give the new particle the correct depth */
+  cell_set_spart_h_depth(sp, c);
+
   /* Here comes the Sun! */
   return sp;
 }
@@ -1051,10 +1074,9 @@ struct sink *cell_convert_part_to_sink(struct engine *e, struct cell *c,
 #ifdef SWIFT_DEBUG_CHECKS
   sp->ti_kick = gp->ti_kick;
   gp->ti_drift = sp->ti_drift;
-#endif
 
-  /* Set a smoothing length */
-  sp->r_cut = e->sink_properties->cut_off_radius;
+  message("A new sink (%lld) is born !", sp->id);
+#endif
 
   /* Here comes the Sink! */
   return sp;
@@ -1130,7 +1152,10 @@ struct spart *cell_spawn_new_spart_from_sink(struct engine *e, struct cell *c,
 #endif
 
   /* Set a smoothing length */
-  sp->h = s->r_cut;
+  sp->h = s->h;
+
+  /* Give the new particle the correct depth */
+  cell_set_spart_h_depth(sp, c);
 
   /* Here comes the Sun! */
   return sp;
diff --git a/src/cell_drift.c b/src/cell_drift.c
index f4a6287581ed44252ae9d04c19e51e8fe170948a..ea427d140afee617bfa582010b70168496552d90 100644
--- a/src/cell_drift.c
+++ b/src/cell_drift.c
@@ -27,6 +27,7 @@
 
 /* Local headers. */
 #include "active.h"
+#include "adaptive_softening.h"
 #include "drift.h"
 #include "feedback.h"
 #include "gravity.h"
@@ -135,6 +136,22 @@ void cell_set_ti_old_bpart(struct cell *c, const integertime_t ti) {
   }
 }
 
+/**
+ * @brief Recursively set the sinks' ti_old_part to the current time.
+ *
+ * @param c The cell to update.
+ * @param ti The current integer time.
+ */
+void cell_set_ti_old_sink(struct cell *c, const integertime_t ti) {
+
+  c->sinks.ti_old_part = ti;
+  if (c->split) {
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL) cell_set_ti_old_sink(c->progeny[k], ti);
+    }
+  }
+}
+
 /**
  * @brief Recursively drifts the #part in a cell hierarchy.
  *
@@ -315,6 +332,9 @@ void cell_drift_part(struct cell *c, const struct engine *e, int force,
       p->h = min(p->h, hydro_h_max);
       p->h = max(p->h, hydro_h_min);
 
+      /* Set the appropriate depth level for this particle */
+      cell_set_part_h_depth(p, c);
+
       /* Compute (square of) motion since last cell construction */
       const float dx2 = xp->x_diff[0] * xp->x_diff[0] +
                         xp->x_diff[1] * xp->x_diff[1] +
@@ -340,6 +360,7 @@ void cell_drift_part(struct cell *c, const struct engine *e, int force,
       /* Get ready for a density calculation */
       if (part_is_active(p, e)) {
         hydro_init_part(p, &e->s->hs);
+        adaptive_softening_init_part(p);
         mhd_init_part(p);
         black_holes_init_potential(&p->black_holes_data);
         chemistry_init_part(p, e->chemistry);
@@ -347,7 +368,7 @@ void cell_drift_part(struct cell *c, const struct engine *e, int force,
         tracers_after_init(p, xp, e->internal_units, e->physical_constants,
                            with_cosmology, e->cosmology, e->hydro_properties,
                            e->cooling_func, e->time);
-        sink_init_part(p);
+        sink_init_part(p, e->sink_properties);
         rt_init_part(p);
 
         /* Update the maximal active smoothing length in the cell */
@@ -567,6 +588,7 @@ void cell_drift_spart(struct cell *c, const struct engine *e, int force,
   const int periodic = e->s->periodic;
   const double dim[3] = {e->s->dim[0], e->s->dim[1], e->s->dim[2]};
   const int with_cosmology = (e->policy & engine_policy_cosmology);
+  const int with_rt = (e->policy & engine_policy_rt);
   const float stars_h_max = e->hydro_properties->h_max;
   const float stars_h_min = e->hydro_properties->h_min;
   const integertime_t ti_old_spart = c->stars.ti_old_part;
@@ -707,6 +729,9 @@ void cell_drift_spart(struct cell *c, const struct engine *e, int force,
       sp->h = min(sp->h, stars_h_max);
       sp->h = max(sp->h, stars_h_min);
 
+      /* Set the appropriate depth level for this particle */
+      cell_set_spart_h_depth(sp, c);
+
       /* Compute (square of) motion since last cell construction */
       const float dx2 = sp->x_diff[0] * sp->x_diff[0] +
                         sp->x_diff[1] * sp->x_diff[1] +
@@ -729,7 +754,8 @@ void cell_drift_spart(struct cell *c, const struct engine *e, int force,
         rt_init_spart(sp);
 
         /* Update the maximal active smoothing length in the cell */
-        cell_h_max_active = max(cell_h_max_active, sp->h);
+        if (feedback_is_active(sp, e) || with_rt)
+          cell_h_max_active = max(cell_h_max_active, sp->h);
       }
     }
 
@@ -911,6 +937,9 @@ void cell_drift_bpart(struct cell *c, const struct engine *e, int force,
       bp->h = min(bp->h, black_holes_h_max);
       bp->h = max(bp->h, black_holes_h_min);
 
+      /* Set the appropriate depth level for this particle */
+      cell_set_bpart_h_depth(bp, c);
+
       /* Compute (square of) motion since last cell construction */
       const float dx2 = bp->x_diff[0] * bp->x_diff[0] +
                         bp->x_diff[1] * bp->x_diff[1] +
@@ -968,13 +997,15 @@ void cell_drift_sink(struct cell *c, const struct engine *e, int force) {
   const int periodic = e->s->periodic;
   const double dim[3] = {e->s->dim[0], e->s->dim[1], e->s->dim[2]};
   const int with_cosmology = (e->policy & engine_policy_cosmology);
+  const float sinks_h_max = e->hydro_properties->h_max;
+  const float sinks_h_min = e->hydro_properties->h_min;
   const integertime_t ti_old_sink = c->sinks.ti_old_part;
   const integertime_t ti_current = e->ti_current;
   struct sink *const sinks = c->sinks.parts;
 
   float dx_max = 0.f, dx2_max = 0.f;
-  float cell_r_max = 0.f;
-  float cell_r_max_active = 0.f;
+  float cell_h_max = 0.f;
+  float cell_h_max_active = 0.f;
 
   /* Drift irrespective of cell flags? */
   force = (force || cell_get_flag(c, cell_flag_do_sink_drift));
@@ -994,7 +1025,7 @@ void cell_drift_sink(struct cell *c, const struct engine *e, int force) {
     cell_clear_flag(c, cell_flag_do_sink_drift | cell_flag_do_sink_sub_drift);
 
     /* Update the time of the last drift */
-    c->sinks.ti_old_part = ti_current;
+    cell_set_ti_old_sink(c, ti_current);
 
     return;
   }
@@ -1014,14 +1045,14 @@ void cell_drift_sink(struct cell *c, const struct engine *e, int force) {
 
         /* Update */
         dx_max = max(dx_max, cp->sinks.dx_max_part);
-        cell_r_max = max(cell_r_max, cp->sinks.r_cut_max);
-        cell_r_max_active = max(cell_r_max_active, cp->sinks.r_cut_max_active);
+        cell_h_max = max(cell_h_max, cp->sinks.h_max);
+        cell_h_max_active = max(cell_h_max_active, cp->sinks.h_max_active);
       }
     }
 
     /* Store the values */
-    c->sinks.r_cut_max = cell_r_max;
-    c->sinks.r_cut_max_active = cell_r_max_active;
+    c->sinks.h_max = cell_h_max;
+    c->sinks.h_max_active = cell_h_max_active;
     c->sinks.dx_max_part = dx_max;
 
     /* Update the time of the last drift */
@@ -1091,7 +1122,12 @@ void cell_drift_sink(struct cell *c, const struct engine *e, int force) {
         }
       }
 
-      /* sp->h does not need to be limited. */
+      /* Limit h to within the allowed range */
+      sink->h = min(sink->h, sinks_h_max);
+      sink->h = max(sink->h, sinks_h_min);
+
+      /* Set the appropriate depth level for this particle */
+      cell_set_sink_h_depth(sink, c);
 
       /* Compute (square of) motion since last cell construction */
       const float dx2 = sink->x_diff[0] * sink->x_diff[0] +
@@ -1100,7 +1136,7 @@ void cell_drift_sink(struct cell *c, const struct engine *e, int force) {
       dx2_max = max(dx2_max, dx2);
 
       /* Maximal smoothing length */
-      cell_r_max = max(cell_r_max, sink->r_cut);
+      cell_h_max = max(cell_h_max, sink->h);
 
       /* Mark the particle has not being swallowed */
       sink_mark_sink_as_not_swallowed(&sink->merger_data);
@@ -1109,7 +1145,7 @@ void cell_drift_sink(struct cell *c, const struct engine *e, int force) {
       if (sink_is_active(sink, e)) {
         sink_init_sink(sink);
 
-        cell_r_max_active = max(cell_r_max_active, sink->r_cut);
+        cell_h_max_active = max(cell_h_max_active, sink->h);
       }
     }
 
@@ -1117,8 +1153,8 @@ void cell_drift_sink(struct cell *c, const struct engine *e, int force) {
     dx_max = sqrtf(dx2_max);
 
     /* Store the values */
-    c->sinks.r_cut_max = cell_r_max;
-    c->sinks.r_cut_max_active = cell_r_max_active;
+    c->sinks.h_max = cell_h_max;
+    c->sinks.h_max_active = cell_h_max_active;
     c->sinks.dx_max_part = dx_max;
 
     /* Update the time of the last drift */
diff --git a/src/cell_grid.c b/src/cell_grid.c
new file mode 100644
index 0000000000000000000000000000000000000000..374d3b6844f083c52a4ef4e70edf08c77744dab6
--- /dev/null
+++ b/src/cell_grid.c
@@ -0,0 +1,442 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *                             Yolan Uyttenhove (Yolan.Uyttenhove@UGent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+/* Config parameters. */
+#include <config.h>
+
+/* Corresponding header */
+#include "cell_grid.h"
+
+/* Local headers */
+#include "engine.h"
+#include "error.h"
+#include "space_getsid.h"
+
+/**
+ * @brief Recursively free grid memory for cell.
+ *
+ * @param c The #cell.
+ */
+void cell_free_grid_rec(struct cell *c) {
+
+#ifndef MOVING_MESH
+  /* Nothing to do as we have no tessellations */
+#else
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->grid.construction_level != c && c->grid.voronoi != NULL)
+    error("Grid allocated, but not on grid construction level!");
+#endif
+  if (c->grid.construction_level == NULL) {
+    for (int k = 0; k < 8; k++)
+      if (c->progeny[k] != NULL) cell_free_grid_rec(c->progeny[k]);
+
+  } else if (c->grid.construction_level == c) {
+    cell_free_grid(c);
+  } else if (c->grid.construction_level != c) {
+    error("Somehow ended up below grid construction level!");
+  }
+#endif
+}
+
+/**
+ * @brief Updates the grid.self_completeness flag on this cell and its
+ * sub-cells.
+ *
+ * This cell satisfies the local completeness criterion for the Voronoi grid.
+ *
+ * A cell is defined as locally complete if, when we would split that cell in
+ * thirds along each dimension (i.e. in 27 smaller cells), every small cube
+ * would contain at least one particle.
+ *
+ * If a given cell and its direct neighbours on the same level in the AMR tree
+ * are self-complete, the Voronoi grid of that cell can completely be
+ * constructed using only particles of this cell and its direct neighbours,
+ * i.e. by doing a normal SWIFT neighbour loop.
+ *
+ * @param c The #cell to be checked
+ * @param force Whether to forcefully recompute the completeness if it was not
+ * invalidated.
+ * */
+void cell_grid_update_self_completeness(struct cell *c, int force) {
+  if (c == NULL) return;
+  if (!force && (c->grid.self_completeness != grid_invalidated_completeness))
+    return;
+
+  if (c->split) {
+    int all_complete = 1;
+
+    /* recurse */
+    for (int i = 0; all_complete && i < 8; i++) {
+      if (c->progeny[i] != NULL) {
+        cell_grid_update_self_completeness(c->progeny[i], force);
+        /* As long as all progeny is complete, this cell can safely be split for
+         * the grid construction (when not considering neighbouring cells) */
+        all_complete &=
+            (c->progeny[i]->grid.self_completeness == grid_complete);
+      }
+    }
+
+    /* If all sub-cells are complete, this cell is also complete. */
+    if (all_complete) {
+      c->grid.self_completeness = grid_complete;
+      /* We set complete to true for now */
+      c->grid.complete = 1;
+      /* We are done here */
+      return;
+    }
+  }
+
+  /* If this cell is not split, or not all subcells are complete, we need to
+   * check if this cell is complete by looping over all the particles. */
+
+  /* criterion = 0b111_111_111_111_111_111_111_111_111*/
+#ifdef HYDRO_DIMENSION_1D
+  const int criterion = (1 << 3) - 1;
+#elif defined(HYDRO_DIMENSION_2D)
+  const int criterion = (1 << 9) - 1;
+#elif defined(HYDRO_DIMENSION_3D)
+  const int criterion = (1 << 27) - 1;
+#else
+#error "Unknown hydro dimension"
+#endif
+  int flags = 0;
+  for (int i = 0; flags != criterion && i < c->hydro.count; i++) {
+    struct part *p = &c->hydro.parts[i];
+    int x_bin = (int)(3. * (p->x[0] - c->loc[0]) / c->width[0]);
+    int y_bin = (int)(3. * (p->x[1] - c->loc[1]) / c->width[1]);
+    int z_bin = (int)(3. * (p->x[2] - c->loc[2]) / c->width[2]);
+    if (x_bin >= 0 && x_bin < 3 && y_bin >= 0 && y_bin < 3 && z_bin >= 0 &&
+        z_bin < 3) {
+      flags |= 1 << (x_bin + 3 * y_bin + 9 * z_bin);
+    }
+  }
+
+  /* Set completeness flags accordingly */
+  if (flags == criterion) {
+    c->grid.self_completeness = grid_complete;
+    c->grid.complete = 1;
+  } else {
+    c->grid.self_completeness = grid_incomplete;
+    c->grid.complete = 0;
+  }
+}
+
+/**
+ * @brief (mapper) Updates the grid.self_completeness flag on this cell and its
+ * sub-cells.
+ *
+ * This function recomputes the self_completeness flag for all cells containing
+ * particles.
+ */
+void cell_grid_set_self_completeness_mapper(void *map_data, int num_elements,
+                                            void *extra_data) {
+  /* Extract the engine pointer. */
+  struct engine *e = (struct engine *)extra_data;
+
+  struct space *s = e->s;
+  const int nodeID = e->nodeID;
+  struct cell *cells = s->cells_top;
+
+  /* Loop through the elements, which are just byte offsets from NULL. */
+  for (int ind = 0; ind < num_elements; ind++) {
+    /* Get the cell index. */
+    const int cid = (size_t)(map_data) + ind;
+
+    /* Get the cell */
+    struct cell *c = &cells[cid];
+
+    /* A top level cell can be empty in 1D and 2D simulations, just skip it */
+    if (c->hydro.count == 0) {
+      continue;
+#ifdef SWIFT_DEBUG_CHECKS
+      if (hydro_dimension == 3)
+        error("Found empty top-level cell while running in 3D!");
+#endif
+    }
+    if (c->nodeID != nodeID) continue;
+
+    /* Set the splittable attribute for the moving mesh */
+    cell_grid_update_self_completeness(c, /*force*/ 1);
+  }
+}
+
+/**
+ * @brief Updates the grid.completeness flag for the given pair of cells and all
+ * recursive pairs of sub-cells.
+ *
+ * If one of the cells of the pair is not self-complete, or requires a rebuild,
+ * we mark the other cell in the pair as incomplete (it cannot construct its
+ * Voronoi grid on that level).
+ *
+ *
+ *
+ * @param ci The first #cell of the pair to be checked.
+ * @param cj The second #cell of the pair to be checked. If NULL, only check
+ * pairs of subcells from ci.
+ * @param sid The sort_id (direction) of the pair.
+ * @param e The #engine.
+ * */
+void cell_grid_set_pair_completeness(struct cell *restrict ci,
+                                     struct cell *restrict cj, int sid,
+                                     const struct engine *e) {
+
+  int ci_local = ci->nodeID == e->nodeID;
+  int cj_local = cj != NULL ? cj->nodeID == e->nodeID : 0;
+  /* Anything to do here? */
+  if (!ci_local && !cj_local) return;
+
+  /* Self or pair? */
+  if (cj == NULL) {
+    /* Self: Here we just need to recurse to hit all the pairs of sub-cells */
+    if (ci->split) {
+      /* recurse */
+      for (int k = 0; k < 7; k++) {
+        if (ci->progeny[k] != NULL) {
+          /* Self: Recurse for pairs of sub-cells of this sub-cell */
+          cell_grid_set_pair_completeness(ci->progeny[k], NULL, 0, e);
+
+          /* Recurse for pairs of sub-cells */
+          for (int l = k + 1; l < 8; l++) {
+            if (ci->progeny[l] != NULL) {
+              /* Get sid for pair */
+              int sid_sub = sub_sid_flag[k][l];
+              /* Pair: Recurse for pairs of sub-cells of this pair of sub-cells
+               */
+              cell_grid_set_pair_completeness(ci->progeny[k], ci->progeny[l],
+                                              sid_sub, e);
+            }
+          }
+        }
+      }
+    }
+  } else {
+    /* pair: Here we need to recurse further AND check whether one of the
+     * neighbouring cells invalidates the completeness of the other. */
+    struct cell_split_pair pairs = cell_split_pairs[sid];
+    if (ci->split && cj->split) {
+      /* recurse */
+      for (int i = 0; i < pairs.count; i++) {
+        struct cell *ci_sub = ci->progeny[pairs.pairs[i].pid];
+        struct cell *cj_sub = cj->progeny[pairs.pairs[i].pjd];
+        if (ci_sub == NULL || cj_sub == NULL) continue;
+        double shift[3];
+        int sid_sub =
+            space_getsid_and_swap_cells(e->s, &ci_sub, &cj_sub, shift);
+#ifdef SWIFT_DEBUG_CHECKS
+        assert(sid_sub == pairs.pairs[i].sid);
+#endif
+        cell_grid_set_pair_completeness(ci_sub, cj_sub, sid_sub, e);
+      }
+    } else if (!ci->split && cj->split) {
+      /* Set the completeness for the sub-cells of cj for this sid to 0 (they
+       * have no neighbouring cell on the same level for this SID) */
+      for (int i = 0; i < pairs.count; i++) {
+        int l = pairs.pairs[i].pjd;
+        if (cj->progeny[l] != NULL) cj->progeny[l]->grid.complete = 0;
+      }
+    } else if (!cj->split && ci->split) {
+      /* Set the completeness for the sub-cells of ci for this sid to 0 (they
+       * have no neighbouring cell on the same level for this SID) */
+      for (int i = 0; i < pairs.count; i++) {
+        int k = pairs.pairs[i].pid;
+        if (ci->progeny[k] != NULL) ci->progeny[k]->grid.complete = 0;
+      }
+    }
+
+    /* Update these cells' completeness flags (i.e. check whether the
+     * neighbouring cell invalidates completeness)
+     * We need to use atomics here, since multiple threads may change this at
+     * the same time. */
+    if (ci_local) {
+      atomic_and(&ci->grid.complete,
+                 !cell_grid_pair_invalidates_completeness(ci, cj));
+    }
+    if (cj_local) {
+      atomic_and(&cj->grid.complete,
+                 !cell_grid_pair_invalidates_completeness(cj, ci));
+    }
+  }
+}
+
+/**
+ * @brief (mapper) Sets the grid.completeness flag for all cells, by looping
+ * aggregating the self_completeness flags of all neighbours of each cell.
+ *
+ * A cell is considered complete if it and all its neighbours are
+ * self_complete. The Voronoi grid may be constructed at any level in the AMR
+ * tree as long as the cell at that level is complete.
+ * */
+void cell_set_grid_completeness_mapper(void *map_data, int num_elements,
+                                       void *extra_data) {
+  /* Extract the engine pointer. */
+  struct engine *e = (struct engine *)extra_data;
+  const int periodic = e->s->periodic;
+
+  struct space *s = e->s;
+  const int nodeID = e->nodeID;
+  const int *cdim = s->cdim;
+  struct cell *cells = s->cells_top;
+
+  /* Loop through the elements, which are just byte offsets from NULL, to set
+   * the neighbour flags. */
+  for (int ind = 0; ind < num_elements; ind++) {
+    /* Get the cell index. */
+    const int cid = (size_t)(map_data) + ind;
+
+    /* Integer indices of the cell in the top-level grid */
+    const int i = cid / (cdim[1] * cdim[2]);
+    const int j = (cid / cdim[2]) % cdim[1];
+    const int k = cid % cdim[2];
+
+    /* Get the cell */
+    struct cell *ci = &cells[cid];
+
+    /* Anything to do here? */
+    if (ci->hydro.count == 0) continue;
+
+    const int ci_local = ci->nodeID == nodeID;
+
+    /* Update completeness for all the pairs of sub cells of this cell */
+    if (ci_local) cell_grid_set_pair_completeness(ci, NULL, 0, e);
+
+    /* Now loop over all the neighbours of this cell to also update the
+     * completeness for pairs with this cell and all pairs of sub-cells */
+    for (int ii = -1; ii < 2; ii++) {
+      int iii = i + ii;
+      if (!periodic && (iii < 0 || iii >= cdim[0])) continue;
+      iii = (iii + cdim[0]) % cdim[0];
+      for (int jj = -1; jj < 2; jj++) {
+        int jjj = j + jj;
+        if (!periodic && (jjj < 0 || jjj >= cdim[1])) continue;
+        jjj = (jjj + cdim[1]) % cdim[1];
+        for (int kk = -1; kk < 2; kk++) {
+          int kkk = k + kk;
+          if (!periodic && (kkk < 0 || kkk >= cdim[2])) continue;
+          kkk = (kkk + cdim[2]) % cdim[2];
+
+          /* Get the neighbouring cell */
+          const int cjd = cell_getid(cdim, iii, jjj, kkk);
+          struct cell *cj = &cells[cjd];
+
+          /* Treat pairs only once. */
+          const int cj_local = cj->nodeID == nodeID;
+          if (cid >= cjd || cj->hydro.count == 0 || (!ci_local && !cj_local))
+            continue;
+
+          /* Update the completeness flag for this pair of cells and all pair of
+           * sub-cells */
+          int sid = (kk + 1) + 3 * ((jj + 1) + 3 * (ii + 1));
+          const int flip = runner_flip[sid];
+          sid = sortlistID[sid];
+          if (flip) {
+            cell_grid_set_pair_completeness(cj, ci, sid, e);
+          } else {
+            cell_grid_set_pair_completeness(ci, cj, sid, e);
+          }
+        }
+      }
+    } /* Now loop over all the neighbours of this cell */
+  } /* Loop through the elements, which are just byte offsets from NULL. */
+}
+
+/**
+ * @brief Select a suitable construction level for the Voronoi grid.
+ *
+ * The Voronoi grid is constructed at the lowest level for which the cell
+ * has more than #space_grid_split_threshold hydro particles *and* is marked
+ * as complete for the Voronoi construction.
+ *
+ * Cells below the construction level store a pointer to its higher level cell
+ * at the construction level.
+ *
+ * @param c The #cell
+ * @param construction_level NULL, if we are yet to encounter the suitable
+ * construction level, or a pointer to the parent-cell of c at the construction
+ * level.
+ */
+void cell_set_grid_construction_level(struct cell *c,
+                                      struct cell *construction_level) {
+
+  /* Above construction level? */
+  if (construction_level == NULL) {
+    /* Check if we can split this cell (i.e. all sub-cells are complete) */
+    int splittable = c->split && c->hydro.count > space_grid_split_threshold;
+    if (!c->grid.complete) {
+      /* Are we on the top level? */
+      if (c->top == c) {
+        warning("Found incomplete top level cell!");
+        splittable = 0;
+      } else {
+        error("Found incomplete cell above construction level!");
+      }
+    }
+    for (int k = 0; splittable && k < 8; k++) {
+      if (c->progeny[k] != NULL) splittable &= c->progeny[k]->grid.complete;
+    }
+
+    if (!splittable) {
+      /* This is the first time we encounter an unsplittable cell, meaning that
+       * it has too few particles to be split further or one of its progenitors
+       * is not complete. I.e. we have arrived at the construction level! */
+      construction_level = c;
+    }
+  }
+
+  /* Set the construction level of this cell */
+  c->grid.construction_level = construction_level;
+
+  /* Recurse. */
+  if (c->split)
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL)
+        cell_set_grid_construction_level(c->progeny[k], construction_level);
+    }
+}
+
+void cell_set_grid_construction_level_mapper(void *map_data, int num_elements,
+                                             void *extra_data) {
+  /* Extract the engine pointer. */
+  struct engine *e = (struct engine *)extra_data;
+
+  struct space *s = e->s;
+  const int nodeID = e->nodeID;
+  struct cell *cells = s->cells_top;
+
+  /* Loop through the elements, which are just byte offsets from NULL, to set
+   * the neighbour flags. */
+  for (int ind = 0; ind < num_elements; ind++) {
+
+    /* Get the cell index. */
+    const int cid = (size_t)(map_data) + ind;
+
+    /* Get the cell */
+    struct cell *ci = &cells[cid];
+
+    /* Anything to do here? */
+    if (ci->hydro.count == 0) continue;
+    const int ci_local = ci->nodeID == nodeID;
+
+    /* This cell's completeness flags are now set all the way down the cell
+     * hierarchy. We can now set the construction level. */
+    if (ci_local) {
+      cell_set_grid_construction_level(ci, NULL);
+    }
+  }
+}
diff --git a/src/cell_grid.h b/src/cell_grid.h
new file mode 100644
index 0000000000000000000000000000000000000000..87e8ec7c9fc8d15af19ec6dc72643374bce02fbc
--- /dev/null
+++ b/src/cell_grid.h
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *                             Yolan Uyttenhove (Yolan.Uyttenhove@UGent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFTSIM_CELL_GRID_H
+#define SWIFTSIM_CELL_GRID_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Includes. */
+#include <stddef.h>
+
+/* Local includes */
+#include "const.h"
+#include "shadowswift/voronoi.h"
+#include "timeline.h"
+
+/*! @brief Enum indicating the completeness for the Voronoi mesh of this cell.
+ *
+ * A cell is considered complete when it and its neighbours on the same level in
+ * the AMR have at least one particle in every 1/27th cube of the cell (obtained
+ * by dividing cells in three along all axes).
+ *
+ * The Voronoi grid can safely be constructed on any level where the cell is
+ * complete. */
+enum grid_completeness {
+  grid_invalidated_completeness = 0,
+  grid_complete,
+  grid_incomplete,
+};
+
+struct cell_grid {
+  /*! Pointer to parent where the grid is constructed. */
+  struct cell *construction_level;
+
+  /*! Pointer to shallowest parent of this cell used in any pair construction
+   * task. Can be above the construction level of this cell. We need to drift at
+   * this level. */
+  struct cell *super;
+
+  /*! Whether this cell is complete (at least one particle in all 27 sub-cells
+   * if this cell is divided in thirds along each axis). */
+  enum grid_completeness self_completeness;
+
+  /*! Whether this cell is itself complete and has directly neighbouring cell
+   * on the same level in all directions which are also complete. */
+  int complete;
+
+#ifdef WITH_MPI
+  /*! Flags indicating whether we should send the faces for the corresponding
+   * SIDs over MPI */
+  int send_flags;
+#endif
+
+  /*! Pointer to the voronoi struct of this cell (if any) */
+  struct voronoi *voronoi;
+
+  /*! Pointer to this cells construction task. */
+  struct task *construction;
+
+  /*! Linked list of this cells outgoing construction synchronization tasks
+   * (i.e. for cells that need this cell for their construction task) */
+  struct link *sync_out;
+
+  /*! Linked list of this cells incoming construction synchronization tasks
+   * (i.e. cells needed for this cell's construction task) */
+  struct link *sync_in;
+
+  /*! Time of last construction */
+  integertime_t ti_old;
+};
+
+struct pcell_faces {
+  size_t counts[27];
+
+  struct voronoi_pair faces[];
+};
+
+/*! @brief Enum used to indicate whether a cell is above, below or on the
+ * construction level. Only used in the packed cell representation */
+enum grid_construction_level {
+  grid_above_construction_level,
+  grid_on_construction_level,
+  grid_below_construction_level
+};
+
+#endif  // SWIFTSIM_CELL_GRID_H
diff --git a/src/cell_hydro.h b/src/cell_hydro.h
index 39db7bc21934e434583551de2a693da46fc7cacc..73d9a5e239b0aa227434846231775fd344ba0988 100644
--- a/src/cell_hydro.h
+++ b/src/cell_hydro.h
@@ -106,6 +106,9 @@ struct cell_hydro {
     /*! Last (integer) time the cell's part were drifted forward in time. */
     integertime_t ti_old_part;
 
+    /*! Spin-lock for the case where we do an extra sort of the cell. */
+    swift_lock_type extra_sort_lock;
+
     /*! Max smoothing length of active particles in this cell. */
     float h_max_active;
 
diff --git a/src/cell_pack.c b/src/cell_pack.c
index cb96b58b40c9b4946b112dd8f01046e2472e981e..58a7496347d946b9dd49e77919c329bcc0268e2e 100644
--- a/src/cell_pack.c
+++ b/src/cell_pack.c
@@ -44,7 +44,7 @@ int cell_pack(struct cell *restrict c, struct pcell *restrict pc,
   pc->hydro.h_max = c->hydro.h_max;
   pc->stars.h_max = c->stars.h_max;
   pc->black_holes.h_max = c->black_holes.h_max;
-  pc->sinks.r_cut_max = c->sinks.r_cut_max;
+  pc->sinks.h_max = c->sinks.h_max;
 
   pc->hydro.ti_end_min = c->hydro.ti_end_min;
   pc->grav.ti_end_min = c->grav.ti_end_min;
@@ -68,6 +68,8 @@ int cell_pack(struct cell *restrict c, struct pcell *restrict pc,
   pc->black_holes.count = c->black_holes.count;
   pc->maxdepth = c->maxdepth;
 
+  pc->grid.self_completeness = c->grid.self_completeness;
+
   /* Copy the Multipole related information */
   if (with_gravity) {
     const struct gravity_tensors *mp = c->grav.multipole;
@@ -140,6 +142,47 @@ int cell_pack_tags(const struct cell *c, int *tags) {
 #endif
 }
 
+/**
+ * @brief Pack the extra grid info of the given cell and all it's sub-cells.
+ *
+ * @param c The #cell.
+ * @param info Pointer to an array of packed extra grid info (construction
+ * levels).
+ *
+ * @return The number of packed tags.
+ */
+int cell_pack_grid_extra(const struct cell *c,
+                         enum grid_construction_level *info) {
+#ifdef WITH_MPI
+
+  /* Start by packing the construction level of the current cell. */
+  if (c->grid.construction_level == NULL) {
+    info[0] = grid_above_construction_level;
+  } else if (c->grid.construction_level == c) {
+    info[0] = grid_on_construction_level;
+  } else {
+    info[0] = grid_below_construction_level;
+  }
+
+  /* Fill in the progeny, depth-first recursion. */
+  int count = 1;
+  for (int k = 0; k < 8; k++)
+    if (c->progeny[k] != NULL)
+      count += cell_pack_grid_extra(c->progeny[k], &info[count]);
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->mpi.pcell_size != count) error("Inconsistent info and pcell count!");
+#endif  // SWIFT_DEBUG_CHECKS
+
+  /* Return the number of packed tags used. */
+  return count;
+
+#else
+  error("SWIFT was not compiled with MPI support.");
+  return 0;
+#endif
+}
+
 void cell_pack_part_swallow(const struct cell *c,
                             struct black_holes_part_data *data) {
 
@@ -203,7 +246,7 @@ int cell_unpack(struct pcell *restrict pc, struct cell *restrict c,
   c->hydro.h_max = pc->hydro.h_max;
   c->stars.h_max = pc->stars.h_max;
   c->black_holes.h_max = pc->black_holes.h_max;
-  c->sinks.r_cut_max = pc->sinks.r_cut_max;
+  c->sinks.h_max = pc->sinks.h_max;
 
   c->hydro.ti_end_min = pc->hydro.ti_end_min;
   c->grav.ti_end_min = pc->grav.ti_end_min;
@@ -227,6 +270,8 @@ int cell_unpack(struct pcell *restrict pc, struct cell *restrict c,
   c->black_holes.count = pc->black_holes.count;
   c->maxdepth = pc->maxdepth;
 
+  c->grid.self_completeness = pc->grid.self_completeness;
+
 #ifdef SWIFT_DEBUG_CHECKS
   c->cellID = pc->cellID;
 #endif
@@ -258,6 +303,7 @@ int cell_unpack(struct pcell *restrict pc, struct cell *restrict c,
       temp->hydro.count = 0;
       temp->grav.count = 0;
       temp->stars.count = 0;
+      temp->sinks.count = 0;
       temp->loc[0] = c->loc[0];
       temp->loc[1] = c->loc[1];
       temp->loc[2] = c->loc[2];
@@ -265,6 +311,8 @@ int cell_unpack(struct pcell *restrict pc, struct cell *restrict c,
       temp->width[1] = c->width[1] / 2;
       temp->width[2] = c->width[2] / 2;
       temp->dmin = c->dmin / 2;
+      temp->h_min_allowed = temp->dmin * 0.5 * (1. / kernel_gamma);
+      temp->h_max_allowed = temp->dmin * (1. / kernel_gamma);
       if (k & 4) temp->loc[0] += temp->width[0];
       if (k & 2) temp->loc[1] += temp->width[1];
       if (k & 1) temp->loc[2] += temp->width[2];
@@ -274,6 +322,7 @@ int cell_unpack(struct pcell *restrict pc, struct cell *restrict c,
       temp->hydro.dx_max_sort = 0.f;
       temp->stars.dx_max_part = 0.f;
       temp->stars.dx_max_sort = 0.f;
+      temp->sinks.dx_max_part = 0.f;
       temp->black_holes.dx_max_part = 0.f;
       temp->nodeID = c->nodeID;
       temp->parent = c;
@@ -329,11 +378,67 @@ int cell_unpack_tags(const int *tags, struct cell *restrict c) {
 #endif
 }
 
+/**
+ * @brief Unpack the extra grid info of a given cell and its sub-cells.
+ *
+ * @param tags An array of extra grid info (construction levels).
+ * @param c The #cell in which to unpack the tags.
+ *
+ * @return The number of tags created.
+ */
+int cell_unpack_grid_extra(const enum grid_construction_level *info,
+                           struct cell *c, struct cell *construction_level) {
+#ifdef WITH_MPI
+
+  /* Unpack the current pcell. */
+  if (info[0] == grid_on_construction_level) {
+    construction_level = c;
+  } else if (info[0] == grid_above_construction_level) {
+#ifdef SWIFT_DEBUG_CHECKS
+    if (construction_level != NULL)
+      error(
+          "Above construction level, but construction level cell pointer is "
+          "not NULL!");
+#endif
+  } else {
+#ifdef SWIFT_DEBUG_CHECKS
+    if (info[0] != grid_below_construction_level)
+      error("Invalid construction level!");
+    if (construction_level == NULL || construction_level == c)
+      error("Invalid construction level cell pointer!");
+#endif
+  }
+
+  c->grid.construction_level = construction_level;
+
+  /* Number of new cells created. */
+  int count = 1;
+
+  /* Fill the progeny recursively, depth-first. */
+  for (int k = 0; k < 8; k++)
+    if (c->progeny[k] != NULL) {
+      count += cell_unpack_grid_extra(&info[count], c->progeny[k],
+                                      construction_level);
+    }
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->mpi.pcell_size != count) error("Inconsistent info and pcell count!");
+#endif  // SWIFT_DEBUG_CHECKS
+
+  /* Return the total number of unpacked tags. */
+  return count;
+
+#else
+  error("SWIFT was not compiled with MPI support.");
+  return 0;
+#endif
+}
+
 /**
  * @brief Pack the cell information about time-step sizes and displacements
  * of a cell hierarchy.
  *
- * @param c The #cells to pack.
+ * @param c The #cell's to pack.
  * @param pcells the packed cell structures to pack into.
  *
  * @return The number of cells that were packed.
@@ -356,6 +461,9 @@ int cell_pack_end_step(const struct cell *c, struct pcell_step *pcells) {
   pcells[0].black_holes.ti_end_min = c->black_holes.ti_end_min;
   pcells[0].black_holes.dx_max_part = c->black_holes.dx_max_part;
 
+  pcells[0].sinks.ti_end_min = c->sinks.ti_end_min;
+  pcells[0].sinks.dx_max_part = c->sinks.dx_max_part;
+
   /* Fill in the progeny, depth-first recursion. */
   int count = 1;
   for (int k = 0; k < 8; k++)
@@ -376,7 +484,7 @@ int cell_pack_end_step(const struct cell *c, struct pcell_step *pcells) {
  * @brief Unpack the cell information about time-step sizes and displacements
  * of a cell hierarchy.
  *
- * @param c The #cells to unpack into.
+ * @param c The #cell's to unpack into.
  * @param pcells the packed cell structures to unpack from.
  *
  * @return The number of cells that were packed.
@@ -400,6 +508,9 @@ int cell_unpack_end_step(struct cell *c, const struct pcell_step *pcells) {
   c->black_holes.ti_end_min = pcells[0].black_holes.ti_end_min;
   c->black_holes.dx_max_part = pcells[0].black_holes.dx_max_part;
 
+  c->sinks.ti_end_min = pcells[0].sinks.ti_end_min;
+  c->sinks.dx_max_part = pcells[0].sinks.dx_max_part;
+
   /* Fill in the progeny, depth-first recursion. */
   int count = 1;
   for (int k = 0; k < 8; k++)
@@ -532,39 +643,108 @@ int cell_unpack_multipoles(struct cell *restrict c,
  *
  * @return The number of packed cells.
  */
-int cell_pack_sf_counts(struct cell *restrict c,
-                        struct pcell_sf *restrict pcells) {
+int cell_pack_sf_counts(struct cell *c, struct pcell_sf_stars *pcells) {
 
 #ifdef WITH_MPI
 
   /* Pack this cell's data. */
-  pcells[0].stars.delta_from_rebuild = c->stars.parts - c->stars.parts_rebuild;
-  pcells[0].stars.count = c->stars.count;
-  pcells[0].stars.dx_max_part = c->stars.dx_max_part;
-
-  /* Pack this cell's data. */
-  pcells[0].grav.delta_from_rebuild = c->grav.parts - c->grav.parts_rebuild;
-  pcells[0].grav.count = c->grav.count;
+  pcells[0].delta_from_rebuild = c->stars.parts - c->stars.parts_rebuild;
+  pcells[0].count = c->stars.count;
+  pcells[0].dx_max_part = c->stars.dx_max_part;
 
 #ifdef SWIFT_DEBUG_CHECKS
   /* Stars */
   if (c->stars.parts_rebuild == NULL)
     error("Star particles array at rebuild is NULL! c->depth=%d", c->depth);
 
-  if (pcells[0].stars.delta_from_rebuild < 0)
+  if (pcells[0].delta_from_rebuild < 0)
     error("Stars part pointer moved in the wrong direction!");
 
-  if (pcells[0].stars.delta_from_rebuild > 0 && c->depth == 0)
+  if (pcells[0].delta_from_rebuild > 0 && c->depth == 0)
     error("Shifting the top-level pointer is not allowed!");
+#endif
 
+  /* Fill in the progeny, depth-first recursion. */
+  int count = 1;
+  for (int k = 0; k < 8; k++)
+    if (c->progeny[k] != NULL) {
+      count += cell_pack_sf_counts(c->progeny[k], &pcells[count]);
+    }
+
+  /* Return the number of packed values. */
+  return count;
+
+#else
+  error("SWIFT was not compiled with MPI support.");
+  return 0;
+#endif
+}
+
+/**
+ * @brief Unpack the counts for star formation of a given cell and its
+ * sub-cells.
+ *
+ * @param c The #cell
+ * @param pcells The multipole information to unpack
+ *
+ * @return The number of cells created.
+ */
+int cell_unpack_sf_counts(struct cell *c, struct pcell_sf_stars *pcells) {
+
+#ifdef WITH_MPI
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->stars.parts_rebuild == NULL)
+    error("Star particles array at rebuild is NULL!");
+#endif
+
+  /* Unpack this cell's data. */
+  c->stars.count = pcells[0].count;
+  c->stars.parts = c->stars.parts_rebuild + pcells[0].delta_from_rebuild;
+  c->stars.dx_max_part = pcells[0].dx_max_part;
+
+  /* Fill in the progeny, depth-first recursion. */
+  int count = 1;
+  for (int k = 0; k < 8; k++)
+    if (c->progeny[k] != NULL) {
+      count += cell_unpack_sf_counts(c->progeny[k], &pcells[count]);
+    }
+
+  /* Return the number of packed values. */
+  return count;
+
+#else
+  error("SWIFT was not compiled with MPI support.");
+  return 0;
+#endif
+}
+
+/**
+ * @brief Pack the counts for star formation of the given cell and all it's
+ * sub-cells.
+ *
+ * @param c The #cell.
+ * @param pcells (output) The multipole information we pack into
+ *
+ * @return The number of packed cells.
+ */
+int cell_pack_grav_counts(struct cell *c, struct pcell_sf_grav *pcells) {
+
+#ifdef WITH_MPI
+
+  /* Pack this cell's data. */
+  pcells[0].delta_from_rebuild = c->grav.parts - c->grav.parts_rebuild;
+  pcells[0].count = c->grav.count;
+
+#ifdef SWIFT_DEBUG_CHECKS
   /* Grav */
   if (c->grav.parts_rebuild == NULL)
     error("Grav. particles array at rebuild is NULL! c->depth=%d", c->depth);
 
-  if (pcells[0].grav.delta_from_rebuild < 0)
+  if (pcells[0].delta_from_rebuild < 0)
     error("Grav part pointer moved in the wrong direction!");
 
-  if (pcells[0].grav.delta_from_rebuild > 0 && c->depth == 0)
+  if (pcells[0].delta_from_rebuild > 0 && c->depth == 0)
     error("Shifting the top-level pointer is not allowed!");
 #endif
 
@@ -572,7 +752,7 @@ int cell_pack_sf_counts(struct cell *restrict c,
   int count = 1;
   for (int k = 0; k < 8; k++)
     if (c->progeny[k] != NULL) {
-      count += cell_pack_sf_counts(c->progeny[k], &pcells[count]);
+      count += cell_pack_grav_counts(c->progeny[k], &pcells[count]);
     }
 
   /* Return the number of packed values. */
@@ -593,31 +773,24 @@ int cell_pack_sf_counts(struct cell *restrict c,
  *
  * @return The number of cells created.
  */
-int cell_unpack_sf_counts(struct cell *restrict c,
-                          struct pcell_sf *restrict pcells) {
+int cell_unpack_grav_counts(struct cell *c, struct pcell_sf_grav *pcells) {
 
 #ifdef WITH_MPI
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (c->stars.parts_rebuild == NULL)
     error("Star particles array at rebuild is NULL!");
-  if (c->grav.parts_rebuild == NULL)
-    error("Grav particles array at rebuild is NULL!");
 #endif
 
   /* Unpack this cell's data. */
-  c->stars.count = pcells[0].stars.count;
-  c->stars.parts = c->stars.parts_rebuild + pcells[0].stars.delta_from_rebuild;
-  c->stars.dx_max_part = pcells[0].stars.dx_max_part;
-
-  c->grav.count = pcells[0].grav.count;
-  c->grav.parts = c->grav.parts_rebuild + pcells[0].grav.delta_from_rebuild;
+  c->grav.count = pcells[0].count;
+  c->grav.parts = c->grav.parts_rebuild + pcells[0].delta_from_rebuild;
 
   /* Fill in the progeny, depth-first recursion. */
   int count = 1;
   for (int k = 0; k < 8; k++)
     if (c->progeny[k] != NULL) {
-      count += cell_unpack_sf_counts(c->progeny[k], &pcells[count]);
+      count += cell_unpack_grav_counts(c->progeny[k], &pcells[count]);
     }
 
   /* Return the number of packed values. */
diff --git a/src/cell_sinks.h b/src/cell_sinks.h
index ac1d9d592030e83df9c8c48b8a0772e2981c201f..2a8bb3c6a19a61a03525a3a5230b3a5333379d0c 100644
--- a/src/cell_sinks.h
+++ b/src/cell_sinks.h
@@ -42,6 +42,9 @@ struct cell_sinks {
     /*! Pointer to the #sink data. */
     struct sink *parts;
 
+    /*! Pointer to the #spart data at rebuild time. */
+    struct sink *parts_rebuild;
+
     /*! Linked list of the tasks computing this cell's sink swallow. */
     struct link *swallow;
 
@@ -57,6 +60,12 @@ struct cell_sinks {
     /*! Implicit tasks marking the entry of the sink block of tasks */
     struct task *sink_in;
 
+    /*! The sink ghost task itself */
+    struct task *density_ghost;
+
+    /*! Linked list of the tasks computing this cell's sink density. */
+    struct link *density;
+
     /*! Implicit tasks marking the end of sink swallow */
     struct task *sink_ghost1;
 
@@ -82,11 +91,12 @@ struct cell_sinks {
     /*! Nr of #sink this cell can hold after addition of new one. */
     int count_total;
 
-    /*! Max cut off radius of active particles in this cell. */
-    float r_cut_max_active;
+    /*! Max smoothing length of active particles in this cell.
+     */
+    float h_max_active;
 
-    /*! Values of r_cut_max before the drifts, used for sub-cell tasks. */
-    float r_cut_max_old;
+    /*! Values of h_max before the drifts, used for sub-cell tasks. */
+    float h_max_old;
 
     /*! Maximum part movement in this cell since last construction. */
     float dx_max_part;
@@ -108,8 +118,8 @@ struct cell_sinks {
   /*! Spin lock for various uses (#sink case). */
   swift_lock_type lock;
 
-  /*! Max cut off radius in this cell. */
-  float r_cut_max;
+  /*! Max smoothing length in this cell. */
+  float h_max;
 
   /*! Number of #sink updated in this cell. */
   int updated;
diff --git a/src/cell_split.c b/src/cell_split.c
index 9e292428aa17859b093df8e7e8a40f083c614fe7..df0fc2572e0b5fa1c33164c07b7b1c7453a3804e 100644
--- a/src/cell_split.c
+++ b/src/cell_split.c
@@ -384,6 +384,7 @@ void cell_split(struct cell *c, const ptrdiff_t parts_offset,
     c->progeny[k]->sinks.count = bucket_count[k];
     c->progeny[k]->sinks.count_total = c->progeny[k]->sinks.count;
     c->progeny[k]->sinks.parts = &c->sinks.parts[bucket_offset[k]];
+    c->progeny[k]->sinks.parts_rebuild = c->progeny[k]->sinks.parts;
   }
 
   /* Finally, do the same song and dance for the gparts. */
@@ -636,7 +637,8 @@ void cell_reorder_extra_sinks(struct cell *c, const ptrdiff_t sinks_offset) {
  * @param sinks The global array of #sink (for re-linking).
  */
 void cell_reorder_extra_gparts(struct cell *c, struct part *parts,
-                               struct spart *sparts, struct sink *sinks) {
+                               struct spart *sparts, struct sink *sinks,
+                               struct bpart *bparts) {
   struct gpart *gparts = c->grav.parts;
   const int count_real = c->grav.count;
 
@@ -668,6 +670,8 @@ void cell_reorder_extra_gparts(struct cell *c, struct part *parts,
         sinks[-gparts[i].id_or_neg_offset].gpart = &gparts[i];
       } else if (gparts[i].type == swift_type_stars) {
         sparts[-gparts[i].id_or_neg_offset].gpart = &gparts[i];
+      } else if (gparts[i].type == swift_type_black_hole) {
+        bparts[-gparts[i].id_or_neg_offset].gpart = &gparts[i];
       }
     }
   }
diff --git a/src/cell_unskip.c b/src/cell_unskip.c
index 406505fd8c4ffd66c7f19b0f3331d0f77b29e5e5..22a73b0d3dbd5ae4221b6b27a2387393fa3bfa41 100644
--- a/src/cell_unskip.c
+++ b/src/cell_unskip.c
@@ -847,7 +847,7 @@ void cell_activate_subcell_hydro_tasks(struct cell *ci, struct cell *cj,
 
     /* Get the orientation of the pair. */
     double shift[3];
-    const int sid = space_getsid(s->space, &ci, &cj, shift);
+    const int sid = space_getsid_and_swap_cells(s->space, &ci, &cj, shift);
 
     /* recurse? */
     if (cell_can_recurse_in_pair_hydro_task(ci) &&
@@ -959,7 +959,7 @@ void cell_activate_subcell_stars_tasks(struct cell *ci, struct cell *cj,
 
     /* Get the orientation of the pair. */
     double shift[3];
-    const int sid = space_getsid(s->space, &ci, &cj, shift);
+    const int sid = space_getsid_and_swap_cells(s->space, &ci, &cj, shift);
 
     const int ci_active = cell_need_activating_stars(ci, e, with_star_formation,
                                                      with_star_formation_sink);
@@ -970,8 +970,8 @@ void cell_activate_subcell_stars_tasks(struct cell *ci, struct cell *cj,
     if (!ci_active && !cj_active) return;
 
     /* recurse? */
-    if (cell_can_recurse_in_pair_stars_task(ci, cj) &&
-        cell_can_recurse_in_pair_stars_task(cj, ci)) {
+    if (cell_can_recurse_in_pair_stars_task(ci) &&
+        cell_can_recurse_in_pair_stars_task(cj)) {
 
       const struct cell_split_pair *csp = &cell_split_pairs[sid];
       for (int k = 0; k < csp->count; k++) {
@@ -1087,14 +1087,16 @@ void cell_activate_subcell_black_holes_tasks(struct cell *ci, struct cell *cj,
 
   /* Otherwise, pair interation */
   else {
-    /* Should we even bother? */
-    if (!cell_is_active_black_holes(ci, e) &&
-        !cell_is_active_black_holes(cj, e))
-      return;
 
     /* Get the orientation of the pair. */
     double shift[3];
-    const int sid = space_getsid(s->space, &ci, &cj, shift);
+    const int sid = space_getsid_and_swap_cells(s->space, &ci, &cj, shift);
+
+    const int ci_active = cell_is_active_black_holes(ci, e);
+    const int cj_active = cell_is_active_black_holes(cj, e);
+
+    /* Should we even bother? */
+    if (!ci_active && !cj_active) return;
 
     /* recurse? */
     if (cell_can_recurse_in_pair_black_holes_task(ci, cj) &&
@@ -1110,19 +1112,24 @@ void cell_activate_subcell_black_holes_tasks(struct cell *ci, struct cell *cj,
     }
 
     /* Otherwise, activate the drifts. */
-    else if (cell_is_active_black_holes(ci, e) ||
-             cell_is_active_black_holes(cj, e)) {
+    else if (ci_active || cj_active) {
+
+      /* Note we need to drift *both* BH cells to deal with BH<->BH swallows
+       * But we only need to drift the gas cell if the *other* cell has an
+       * active BH */
 
       /* Activate the drifts if the cells are local. */
       if (ci->nodeID == engine_rank) cell_activate_drift_bpart(ci, s);
-      if (cj->nodeID == engine_rank) cell_activate_drift_part(cj, s);
-      if (cj->nodeID == engine_rank && with_timestep_sync)
+      if (cj->nodeID == engine_rank && ci_active)
+        cell_activate_drift_part(cj, s);
+      if (cj->nodeID == engine_rank && ci_active && with_timestep_sync)
         cell_activate_sync_part(cj, s);
 
       /* Activate the drifts if the cells are local. */
-      if (ci->nodeID == engine_rank) cell_activate_drift_part(ci, s);
+      if (ci->nodeID == engine_rank && cj_active)
+        cell_activate_drift_part(ci, s);
       if (cj->nodeID == engine_rank) cell_activate_drift_bpart(cj, s);
-      if (ci->nodeID == engine_rank && with_timestep_sync)
+      if (ci->nodeID == engine_rank && cj_active && with_timestep_sync)
         cell_activate_sync_part(ci, s);
     }
   } /* Otherwise, pair interation */
@@ -1144,13 +1151,13 @@ void cell_activate_subcell_sinks_tasks(struct cell *ci, struct cell *cj,
 
   /* Store the current dx_max and h_max values. */
   ci->sinks.dx_max_part_old = ci->sinks.dx_max_part;
-  ci->sinks.r_cut_max_old = ci->sinks.r_cut_max;
+  ci->sinks.h_max_old = ci->sinks.h_max;
   ci->hydro.dx_max_part_old = ci->hydro.dx_max_part;
   ci->hydro.h_max_old = ci->hydro.h_max;
 
   if (cj != NULL) {
     cj->sinks.dx_max_part_old = cj->sinks.dx_max_part;
-    cj->sinks.r_cut_max_old = cj->sinks.r_cut_max;
+    cj->sinks.h_max_old = cj->sinks.h_max;
     cj->hydro.dx_max_part_old = cj->hydro.dx_max_part;
     cj->hydro.h_max_old = cj->hydro.h_max;
   }
@@ -1190,7 +1197,7 @@ void cell_activate_subcell_sinks_tasks(struct cell *ci, struct cell *cj,
   else {
     /* Get the orientation of the pair. */
     double shift[3];
-    const int sid = space_getsid(s->space, &ci, &cj, shift);
+    const int sid = space_getsid_and_swap_cells(s->space, &ci, &cj, shift);
 
     const int ci_active =
         cell_is_active_sinks(ci, e) || cell_is_active_hydro(ci, e);
@@ -1579,7 +1586,7 @@ void cell_activate_subcell_rt_tasks(struct cell *ci, struct cell *cj,
 
     /* Get the orientation of the pair. */
     double shift[3];
-    const int sid = space_getsid(s->space, &ci, &cj, shift);
+    const int sid = space_getsid_and_swap_cells(s->space, &ci, &cj, shift);
 
     /* recurse? */
     if (cell_can_recurse_in_pair_hydro_task(ci) &&
@@ -1625,11 +1632,9 @@ int cell_unskip_hydro_tasks(struct cell *c, struct scheduler *s) {
   const int with_feedback = e->policy & engine_policy_feedback;
   const int with_timestep_limiter =
       (e->policy & engine_policy_timestep_limiter);
-  const int with_sinks = e->policy & engine_policy_sinks;
 
 #ifdef WITH_MPI
   const int with_star_formation = e->policy & engine_policy_star_formation;
-  if (with_sinks) error("Cannot use sink tasks and MPI");
 #endif
   int rebuild = 0;
 
@@ -1686,11 +1691,25 @@ int cell_unskip_hydro_tasks(struct cell *c, struct scheduler *s) {
       /* Store current values of dx_max and h_max. */
       else if (t->type == task_type_sub_self) {
         cell_activate_subcell_hydro_tasks(ci, NULL, s, with_timestep_limiter);
+
+        if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
+        if (ci_nodeID == nodeID && with_timestep_limiter)
+          cell_activate_limiter(ci, s);
       }
 
       /* Store current values of dx_max and h_max. */
       else if (t->type == task_type_sub_pair) {
         cell_activate_subcell_hydro_tasks(ci, cj, s, with_timestep_limiter);
+
+        /* Activate the drift tasks. */
+        if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
+        if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
+
+        /* Activate the limiter tasks. */
+        if (ci_nodeID == nodeID && with_timestep_limiter)
+          cell_activate_limiter(ci, s);
+        if (cj_nodeID == nodeID && with_timestep_limiter)
+          cell_activate_limiter(cj, s);
       }
     }
 
@@ -1930,9 +1949,6 @@ int cell_unskip_hydro_tasks(struct cell *c, struct scheduler *s) {
       cell_activate_star_formation_tasks(c->top, s, with_feedback);
       cell_activate_super_spart_drifts(c->top, s);
     }
-    if (with_sinks && c->top->sinks.star_formation_sink != NULL) {
-      cell_activate_star_formation_sink_tasks(c->top, s, with_feedback);
-    }
   }
   /* Additionally unskip force interactions between inactive local cell and
    * active remote cell. (The cell unskip will only be called for active cells,
@@ -2004,6 +2020,10 @@ int cell_unskip_gravity_tasks(struct cell *c, struct scheduler *s) {
   const int nodeID = e->nodeID;
   int rebuild = 0;
 
+#ifdef WITH_MPI
+  const int with_star_formation = e->policy & engine_policy_star_formation;
+#endif
+
   /* Un-skip the gravity tasks involved with this cell. */
   for (struct link *l = c->grav.grav; l != NULL; l = l->next) {
     struct task *t = l->t;
@@ -2059,6 +2079,17 @@ int cell_unskip_gravity_tasks(struct cell *c, struct scheduler *s) {
           cell_activate_drift_gpart(cj, s);
         }
 
+        /* Propagating new star counts? */
+        if (with_star_formation) {
+          if (ci_active && ci->hydro.count > 0) {
+            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_grav_counts);
+          }
+          if (cj_active && cj->hydro.count > 0) {
+            scheduler_activate_send(s, cj->mpi.send, task_subtype_grav_counts,
+                                    ci_nodeID);
+          }
+        }
+
       } else if (cj_nodeID != nodeID) {
         /* If the local cell is active, receive data from the foreign cell. */
         if (ci_active)
@@ -2075,6 +2106,17 @@ int cell_unskip_gravity_tasks(struct cell *c, struct scheduler *s) {
              itself. */
           cell_activate_drift_gpart(ci, s);
         }
+
+        /* Propagating new star counts? */
+        if (with_star_formation) {
+          if (cj_active && cj->hydro.count > 0) {
+            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_grav_counts);
+          }
+          if (ci_active && ci->hydro.count > 0) {
+            scheduler_activate_send(s, ci->mpi.send, task_subtype_grav_counts,
+                                    cj_nodeID);
+          }
+        }
       }
 #endif
     }
@@ -2244,6 +2286,10 @@ int cell_unskip_stars_tasks(struct cell *c, struct scheduler *s,
         cell_activate_subcell_stars_tasks(ci, NULL, s, with_star_formation,
                                           with_star_formation_sink,
                                           with_timestep_sync);
+
+        cell_activate_drift_spart(ci, s);
+        cell_activate_drift_part(ci, s);
+        if (with_timestep_sync) cell_activate_sync_part(ci, s);
       }
 
       else if (t->type == task_type_sub_pair) {
@@ -2251,6 +2297,18 @@ int cell_unskip_stars_tasks(struct cell *c, struct scheduler *s,
                                           with_star_formation_sink,
                                           with_timestep_sync);
 
+        /* Activate the drift tasks. */
+        if (ci_nodeID == nodeID) cell_activate_drift_spart(ci, s);
+        if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
+        if (cj_nodeID == nodeID && with_timestep_sync)
+          cell_activate_sync_part(cj, s);
+
+        /* Activate the drift tasks. */
+        if (cj_nodeID == nodeID) cell_activate_drift_spart(cj, s);
+        if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
+        if (ci_nodeID == nodeID && with_timestep_sync)
+          cell_activate_sync_part(ci, s);
+
         /* Activate stars_in for each cell that is part of
          * a sub_pair task as to not miss any dependencies */
         if (ci_nodeID == nodeID)
@@ -2563,7 +2621,8 @@ int cell_unskip_black_holes_tasks(struct cell *c, struct scheduler *s) {
   const int nodeID = e->nodeID;
   int rebuild = 0;
 
-  if (c->black_holes.drift != NULL && cell_is_active_black_holes(c, e)) {
+  if (c->black_holes.drift != NULL && c->black_holes.count > 0 &&
+      cell_is_active_black_holes(c, e)) {
     cell_activate_drift_bpart(c, s);
   }
 
@@ -2572,8 +2631,11 @@ int cell_unskip_black_holes_tasks(struct cell *c, struct scheduler *s) {
     struct task *t = l->t;
     struct cell *ci = t->ci;
     struct cell *cj = t->cj;
-    const int ci_active = cell_is_active_black_holes(ci, e);
-    const int cj_active = (cj != NULL) ? cell_is_active_black_holes(cj, e) : 0;
+    const int ci_active =
+        ci->black_holes.count > 0 && cell_is_active_black_holes(ci, e);
+    const int cj_active = (cj != NULL) ? (cj->black_holes.count > 0 &&
+                                          cell_is_active_black_holes(cj, e))
+                                       : 0;
 #ifdef WITH_MPI
     const int ci_nodeID = ci->nodeID;
     const int cj_nodeID = (cj != NULL) ? cj->nodeID : -1;
@@ -2588,7 +2650,7 @@ int cell_unskip_black_holes_tasks(struct cell *c, struct scheduler *s) {
 
       scheduler_activate(s, t);
 
-      /* Activate the drifts */
+      /* Activate the drifts & sync */
       if (t->type == task_type_self) {
         cell_activate_drift_part(ci, s);
         cell_activate_drift_bpart(ci, s);
@@ -2598,12 +2660,20 @@ int cell_unskip_black_holes_tasks(struct cell *c, struct scheduler *s) {
       /* Activate the drifts */
       else if (t->type == task_type_pair) {
 
-        /* Activate the drift tasks. */
+        /* Activate the drift & sync tasks.
+         * Note we need to drift *both* BH cells to deal with BH<->BH swallows
+         * But we only need to drift the gas cell if the *other* cell has an
+         * active BH */
         if (ci_nodeID == nodeID) cell_activate_drift_bpart(ci, s);
-        if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
+        if (ci_nodeID == nodeID && cj_active) cell_activate_drift_part(ci, s);
 
-        if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
+        if (cj_nodeID == nodeID && ci_active) cell_activate_drift_part(cj, s);
         if (cj_nodeID == nodeID) cell_activate_drift_bpart(cj, s);
+
+        if (ci_nodeID == nodeID && cj_active && with_timestep_sync)
+          cell_activate_sync_part(ci, s);
+        if (cj_nodeID == nodeID && ci_active && with_timestep_sync)
+          cell_activate_sync_part(cj, s);
       }
 
       /* Store current values of dx_max and h_max. */
@@ -2616,89 +2686,121 @@ int cell_unskip_black_holes_tasks(struct cell *c, struct scheduler *s) {
       else if (t->type == task_type_sub_pair) {
         cell_activate_subcell_black_holes_tasks(ci, cj, s, with_timestep_sync);
       }
+
+      if (t->type == task_type_pair || t->type == task_type_sub_pair) {
+        /* Activate bh_in for each cell that is part of
+         * a pair task as to not miss any dependencies */
+        if (ci_nodeID == nodeID)
+          scheduler_activate(s, ci->hydro.super->black_holes.black_holes_in);
+        if (cj_nodeID == nodeID)
+          scheduler_activate(s, cj->hydro.super->black_holes.black_holes_in);
+      }
     }
 
     /* Only interested in pair interactions as of here. */
     if (t->type == task_type_pair || t->type == task_type_sub_pair) {
 
-      /* Activate bh_in for each cell that is part of
-       * a pair task as to not miss any dependencies */
-      if (ci_nodeID == nodeID)
-        scheduler_activate(s, ci->hydro.super->black_holes.black_holes_in);
-      if (cj_nodeID == nodeID)
-        scheduler_activate(s, cj->hydro.super->black_holes.black_holes_in);
-
       /* Check whether there was too much particle motion, i.e. the
          cell neighbour conditions were violated. */
       if (cell_need_rebuild_for_black_holes_pair(ci, cj)) rebuild = 1;
       if (cell_need_rebuild_for_black_holes_pair(cj, ci)) rebuild = 1;
 
-      scheduler_activate(s, ci->hydro.super->black_holes.swallow_ghost_0);
-      scheduler_activate(s, cj->hydro.super->black_holes.swallow_ghost_0);
+      if (ci->hydro.super->black_holes.count > 0 && ci_active)
+        scheduler_activate(s, ci->hydro.super->black_holes.swallow_ghost_1);
+      if (cj->hydro.super->black_holes.count > 0 && cj_active)
+        scheduler_activate(s, cj->hydro.super->black_holes.swallow_ghost_1);
 
 #ifdef WITH_MPI
       /* Activate the send/recv tasks. */
       if (ci_nodeID != nodeID) {
 
-        /* Receive the foreign parts to compute BH accretion rates and do the
-         * swallowing */
-        scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rho);
-        scheduler_activate_recv(s, ci->mpi.recv, task_subtype_part_swallow);
+        if (ci_active || cj_active) {
+          /* We must exchange the foreign BHs no matter the activity status */
+          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_rho);
+          scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_rho,
+                                  ci_nodeID);
 
-        /* Send the local BHs to tag the particles to swallow and to do feedback
-         */
-        scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_rho,
-                                ci_nodeID);
-        scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_feedback,
-                                ci_nodeID);
+          /* Drift before you send */
+          if (cj->black_holes.count > 0) cell_activate_drift_bpart(cj, s);
+        }
 
-        /* Drift before you send */
-        cell_activate_drift_bpart(cj, s);
+        if (cj_active) {
 
-        /* Receive the foreign BHs to tag particles to swallow and for feedback
-         */
-        scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_rho);
-        scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_feedback);
+          /* Receive the foreign parts to compute BH accretion rates and do the
+           * swallowing */
+          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rho);
+          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_part_swallow);
+          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_merger);
 
-        /* Send the local part information */
-        scheduler_activate_send(s, cj->mpi.send, task_subtype_rho, ci_nodeID);
-        scheduler_activate_send(s, cj->mpi.send, task_subtype_part_swallow,
-                                ci_nodeID);
+          /* Send the local BHs to do feedback */
+          scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_feedback,
+                                  ci_nodeID);
 
-        /* Drift the cell which will be sent; note that not all sent
-           particles will be drifted, only those that are needed. */
-        cell_activate_drift_part(cj, s);
+          /* Drift before you send */
+          cell_activate_drift_bpart(cj, s);
+        }
+
+        if (ci_active) {
+
+          /* Receive the foreign BHs for feedback */
+          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_feedback);
+
+          /* Send the local part information */
+          scheduler_activate_send(s, cj->mpi.send, task_subtype_rho, ci_nodeID);
+          scheduler_activate_send(s, cj->mpi.send, task_subtype_part_swallow,
+                                  ci_nodeID);
+          scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_merger,
+                                  ci_nodeID);
+
+          /* Drift the cell which will be sent; note that not all sent
+             particles will be drifted, only those that are needed. */
+          if (cj->hydro.count > 0) cell_activate_drift_part(cj, s);
+        }
 
       } else if (cj_nodeID != nodeID) {
 
-        /* Receive the foreign parts to compute BH accretion rates and do the
-         * swallowing */
-        scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rho);
-        scheduler_activate_recv(s, cj->mpi.recv, task_subtype_part_swallow);
+        if (ci_active || cj_active) {
+          /* We must exchange the foreign BHs no matter the activity status */
+          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_rho);
+          scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_rho,
+                                  cj_nodeID);
 
-        /* Send the local BHs to tag the particles to swallow and to do feedback
-         */
-        scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_rho,
-                                cj_nodeID);
-        scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_feedback,
-                                cj_nodeID);
+          /* Drift before you send */
+          if (ci->black_holes.count > 0) cell_activate_drift_bpart(ci, s);
+        }
 
-        /* Drift before you send */
-        cell_activate_drift_bpart(ci, s);
+        if (ci_active) {
 
-        /* Receive the foreign BHs to tag particles to swallow and for feedback
-         */
-        scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_rho);
-        scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_feedback);
+          /* Receive the foreign parts to compute BH accretion rates and do the
+           * swallowing */
+          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rho);
+          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_part_swallow);
+          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_merger);
 
-        /* Send the local part information */
-        scheduler_activate_send(s, ci->mpi.send, task_subtype_rho, cj_nodeID);
-        scheduler_activate_send(s, ci->mpi.send, task_subtype_part_swallow,
-                                cj_nodeID);
+          /* Send the local BHs to do feedback */
+          scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_feedback,
+                                  cj_nodeID);
 
-        /* Drift the cell which will be sent; note that not all sent
-           particles will be drifted, only those that are needed. */
-        cell_activate_drift_part(ci, s);
+          /* Drift before you send */
+          cell_activate_drift_bpart(ci, s);
+        }
+
+        if (cj_active) {
+
+          /* Receive the foreign BHs for feedback */
+          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_feedback);
+
+          /* Send the local part information */
+          scheduler_activate_send(s, ci->mpi.send, task_subtype_rho, cj_nodeID);
+          scheduler_activate_send(s, ci->mpi.send, task_subtype_part_swallow,
+                                  cj_nodeID);
+          scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_merger,
+                                  cj_nodeID);
+
+          /* Drift the cell which will be sent; note that not all sent
+             particles will be drifted, only those that are needed. */
+          if (ci->hydro.count > 0) cell_activate_drift_part(ci, s);
+        }
       }
 #endif
     }
@@ -2806,22 +2908,27 @@ int cell_unskip_black_holes_tasks(struct cell *c, struct scheduler *s) {
   }
 
   /* Unskip all the other task types. */
-  if (c->nodeID == nodeID && cell_is_active_black_holes(c, e)) {
-
-    /* If the cell doesn't have any pair/sub_pair type tasks,
-     * then we haven't unskipped all the implicit tasks yet. */
+  if (cell_is_active_black_holes(c, e)) {
     if (c->black_holes.density_ghost != NULL)
       scheduler_activate(s, c->black_holes.density_ghost);
-    if (c->black_holes.swallow_ghost_0 != NULL)
-      scheduler_activate(s, c->black_holes.swallow_ghost_0);
     if (c->black_holes.swallow_ghost_1 != NULL)
       scheduler_activate(s, c->black_holes.swallow_ghost_1);
     if (c->black_holes.swallow_ghost_2 != NULL)
       scheduler_activate(s, c->black_holes.swallow_ghost_2);
+    if (c->black_holes.swallow_ghost_3 != NULL)
+      scheduler_activate(s, c->black_holes.swallow_ghost_3);
+  }
+  if (c->nodeID == nodeID && cell_is_active_black_holes(c, e)) {
     if (c->black_holes.black_holes_in != NULL)
       scheduler_activate(s, c->black_holes.black_holes_in);
     if (c->black_holes.black_holes_out != NULL)
       scheduler_activate(s, c->black_holes.black_holes_out);
+  }
+  if (c->nodeID == nodeID && c->black_holes.count > 0 &&
+      cell_is_active_black_holes(c, e)) {
+
+    /* If the cell doesn't have any pair/sub_pair type tasks,
+     * then we haven't unskipped all the implicit tasks yet. */
     if (c->kick1 != NULL) scheduler_activate(s, c->kick1);
     if (c->kick2 != NULL) scheduler_activate(s, c->kick2);
     if (c->timestep != NULL) scheduler_activate(s, c->timestep);
@@ -2848,6 +2955,7 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
 
   struct engine *e = s->space->e;
   const int with_timestep_sync = (e->policy & engine_policy_timestep_sync);
+  const int with_feedback = e->policy & engine_policy_feedback;
   const int nodeID = e->nodeID;
   int rebuild = 0;
 
@@ -2856,11 +2964,12 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
       cell_activate_drift_sink(c, s);
     }
 
-  /* Un-skip the star formation tasks involved with this cell. */
-  for (struct link *l = c->sinks.swallow; l != NULL; l = l->next) {
+  /* Un-skip the density tasks involved with this cell. */
+  for (struct link *l = c->sinks.density; l != NULL; l = l->next) {
     struct task *t = l->t;
     struct cell *ci = t->ci;
     struct cell *cj = t->cj;
+
 #ifdef WITH_MPI
     const int ci_nodeID = ci->nodeID;
     const int cj_nodeID = (cj != NULL) ? cj->nodeID : -1;
@@ -2871,71 +2980,37 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
 
     const int ci_active =
         cell_is_active_sinks(ci, e) || cell_is_active_hydro(ci, e);
-
     const int cj_active = (cj != NULL) && (cell_is_active_sinks(cj, e) ||
                                            cell_is_active_hydro(cj, e));
 
-    /* Activate the drifts */
-    if (t->type == task_type_self && ci_active) {
-      cell_activate_drift_sink(ci, s);
-      cell_activate_drift_part(ci, s);
-      cell_activate_sink_formation_tasks(ci->top, s);
-      if (with_timestep_sync) cell_activate_sync_part(ci, s);
-    }
-
     /* Only activate tasks that involve a local active cell. */
     if ((ci_active || cj_active) &&
         (ci_nodeID == nodeID || cj_nodeID == nodeID)) {
-      scheduler_activate(s, t);
 
-      if (t->type == task_type_pair) {
-        /* For the mergers */
-        if (cj_nodeID == nodeID) {
-          cell_activate_drift_sink(cj, s);
-          cell_activate_sink_formation_tasks(cj->top, s);
-        }
-        if (ci_nodeID == nodeID) {
-          cell_activate_drift_sink(ci, s);
-          if (ci->top != cj->top) {
-            cell_activate_sink_formation_tasks(ci->top, s);
-          }
-        }
-
-        /* Do ci */
-        if (ci_active) {
-          /* hydro for cj */
-          atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-          cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
-
-          /* Activate the drift tasks. */
-          if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
-          if (cj_nodeID == nodeID && with_timestep_sync)
-            cell_activate_sync_part(cj, s);
-
-          /* Check the sorts and activate them if needed. */
-          cell_activate_hydro_sorts(cj, t->flags, s);
-        }
-
-        /* Do cj */
-        if (cj_active) {
-          /* hydro for ci */
-          atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-          ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
+      scheduler_activate(s, t);
 
-          /* Activate the drift tasks. */
-          if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-          if (ci_nodeID == nodeID && with_timestep_sync)
-            cell_activate_sync_part(ci, s);
+      /* Activate drifts for self tasks */
+      if (t->type == task_type_self) {
+        cell_activate_drift_part(ci, s);
+        cell_activate_drift_sink(ci, s);
+      }
 
-          /* Check the sorts and activate them if needed. */
-          cell_activate_hydro_sorts(ci, t->flags, s);
-        }
+      /* Activate drifts for pair tasks */
+      else if (t->type == task_type_pair) {
+        if (ci_nodeID == nodeID) cell_activate_drift_sink(ci, s);
+        if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
+        if (ci_nodeID == nodeID) cell_activate_sink_formation_tasks(ci->top, s);
+        if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
+        if (cj_nodeID == nodeID) cell_activate_drift_sink(cj, s);
+        if (cj_nodeID == nodeID) cell_activate_sink_formation_tasks(cj->top, s);
       }
 
+      /* Store current values of dx_max and h_max. */
       else if (t->type == task_type_sub_self) {
         cell_activate_subcell_sinks_tasks(ci, NULL, s, with_timestep_sync);
       }
 
+      /* Store current values of dx_max and h_max. */
       else if (t->type == task_type_sub_pair) {
         cell_activate_subcell_sinks_tasks(ci, cj, s, with_timestep_sync);
       }
@@ -2943,25 +3018,27 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
 
     /* Only interested in pair interactions as of here. */
     if (t->type == task_type_pair || t->type == task_type_sub_pair) {
-      /* Check whether there was too much particle motion, i.e. the
-         cell neighbour conditions were violated. */
-      if (cell_need_rebuild_for_sinks_pair(ci, cj)) rebuild = 1;
-      if (cell_need_rebuild_for_sinks_pair(cj, ci)) rebuild = 1;
 
-      /* Activate all sink_in tasks for each cell involved
-       * in pair/sub_pair type tasks */
+      /* Activate sink_in for each cell that is part of
+       * a pair task as to not miss any dependencies */
       if (ci_nodeID == nodeID)
         scheduler_activate(s, ci->hydro.super->sinks.sink_in);
       if (cj_nodeID == nodeID)
         scheduler_activate(s, cj->hydro.super->sinks.sink_in);
 
-#ifdef WITH_MPI
+      /* Check whether there was too much particle motion, i.e. the
+         cell neighbour conditions were violated. */
+      if (cell_need_rebuild_for_sinks_pair(ci, cj)) rebuild = 1;
+      if (cell_need_rebuild_for_sinks_pair(cj, ci)) rebuild = 1;
+
+#if defined(WITH_MPI) && !defined(SWIFT_DEBUG_CHECKS)
       error("TODO");
 #endif
     }
   }
 
-  for (struct link *l = c->sinks.do_sink_swallow; l != NULL; l = l->next) {
+  /* Un-skip the swallow tasks involved with this cell. */
+  for (struct link *l = c->sinks.swallow; l != NULL; l = l->next) {
     struct task *t = l->t;
     struct cell *ci = t->ci;
     struct cell *cj = t->cj;
@@ -2975,7 +3052,32 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
 
     const int ci_active =
         cell_is_active_sinks(ci, e) || cell_is_active_hydro(ci, e);
+    const int cj_active = (cj != NULL) && (cell_is_active_sinks(cj, e) ||
+                                           cell_is_active_hydro(cj, e));
+
+    /* Only activate tasks that involve a local active cell. */
+    if ((ci_active || cj_active) &&
+        (ci_nodeID == nodeID || cj_nodeID == nodeID)) {
+
+      scheduler_activate(s, t);
+    }
+  }
 
+  /* Un-skip the do_sink_swallow tasks involved with this cell. */
+  for (struct link *l = c->sinks.do_sink_swallow; l != NULL; l = l->next) {
+    struct task *t = l->t;
+    struct cell *ci = t->ci;
+    struct cell *cj = t->cj;
+#ifdef WITH_MPI
+    const int ci_nodeID = ci->nodeID;
+    const int cj_nodeID = (cj != NULL) ? cj->nodeID : -1;
+#else
+    const int ci_nodeID = nodeID;
+    const int cj_nodeID = nodeID;
+#endif
+
+    const int ci_active =
+        cell_is_active_sinks(ci, e) || cell_is_active_hydro(ci, e);
     const int cj_active = (cj != NULL) && (cell_is_active_sinks(cj, e) ||
                                            cell_is_active_hydro(cj, e));
 
@@ -2986,6 +3088,7 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
     }
   }
 
+  /* Un-skip the do_gas_swallow tasks involved with this cell. */
   for (struct link *l = c->sinks.do_gas_swallow; l != NULL; l = l->next) {
     struct task *t = l->t;
     struct cell *ci = t->ci;
@@ -3000,7 +3103,6 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
 
     const int ci_active =
         cell_is_active_sinks(ci, e) || cell_is_active_hydro(ci, e);
-
     const int cj_active = (cj != NULL) && (cell_is_active_sinks(cj, e) ||
                                            cell_is_active_hydro(cj, e));
 
@@ -3025,11 +3127,21 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
       (cell_is_active_sinks(c, e) || cell_is_active_hydro(c, e))) {
 
     if (c->sinks.sink_in != NULL) scheduler_activate(s, c->sinks.sink_in);
+    if (c->top->sinks.sink_formation != NULL) {
+      cell_activate_sink_formation_tasks(c->top, s);
+      cell_activate_super_sink_drifts(c->top, s);
+    }
+    if (c->sinks.density_ghost != NULL)
+      scheduler_activate(s, c->sinks.density_ghost);
     if (c->sinks.sink_ghost1 != NULL)
       scheduler_activate(s, c->sinks.sink_ghost1);
     if (c->sinks.sink_ghost2 != NULL)
       scheduler_activate(s, c->sinks.sink_ghost2);
     if (c->sinks.sink_out != NULL) scheduler_activate(s, c->sinks.sink_out);
+    if (c->top->sinks.star_formation_sink != NULL) {
+      cell_activate_star_formation_sink_tasks(c->top, s, with_feedback);
+      cell_activate_super_sink_drifts(c->top, s);
+    }
     if (c->kick1 != NULL) scheduler_activate(s, c->kick1);
     if (c->kick2 != NULL) scheduler_activate(s, c->kick2);
     if (c->timestep != NULL) scheduler_activate(s, c->timestep);
@@ -3039,7 +3151,6 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
     if (c->csds != NULL) scheduler_activate(s, c->csds);
 #endif
   }
-
   return rebuild;
 }
 
@@ -3137,7 +3248,7 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s,
             scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rt_transport);
           }
         } else if (ci_active) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
+#ifdef MPI_SYMMETRIC_FORCE_INTERACTION_RT
           /* If the local cell is inactive and the remote cell is active, we
            * still need to receive stuff to be able to do the force interaction
            * on this node as well.
@@ -3161,7 +3272,7 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s,
                                     ci_nodeID);
           }
         } else if (cj_active) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
+#ifdef MPI_SYMMETRIC_FORCE_INTERACTION_RT
           /* If the foreign cell is inactive, but the local cell is active,
            * we still need to send stuff to be able to do the force interaction
            * on both nodes.
@@ -3190,7 +3301,7 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s,
             scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rt_transport);
           }
         } else if (cj_active) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
+#ifdef MPI_SYMMETRIC_FORCE_INTERACTION_RT
           /* If the local cell is inactive and the remote cell is active, we
            * still need to receive stuff to be able to do the force interaction
            * on this node as well.
@@ -3214,7 +3325,7 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s,
                                     cj_nodeID);
           }
         } else if (ci_active) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
+#ifdef MPI_SYMMETRIC_FORCE_INTERACTION_RT
           /* If the foreign cell is inactive, but the local cell is active,
            * we still need to send stuff to be able to do the force interaction
            * on both nodes
@@ -3275,7 +3386,7 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s,
       if (c->rt.rt_tchem != NULL) scheduler_activate(s, c->rt.rt_tchem);
       if (c->rt.rt_out != NULL) scheduler_activate(s, c->rt.rt_out);
     } else {
-#if defined(MPI_SYMMETRIC_FORCE_INTERACTION) && defined(WITH_MPI)
+#if defined(MPI_SYMMETRIC_FORCE_INTERACTION_RT) && defined(WITH_MPI)
       /* Additionally unskip force interactions between inactive local cell and
        * active remote cell. (The cell unskip will only be called for active
        * cells, so, we have to do this now, from the active remote cell). */
diff --git a/src/chemistry/AGORA/chemistry.h b/src/chemistry/AGORA/chemistry.h
index 424c187aaec9731dc1bfc0f110a54c2141c6d85d..c6c7d37cf678f54506982c1f4b4d254347f9fe09 100644
--- a/src/chemistry/AGORA/chemistry.h
+++ b/src/chemistry/AGORA/chemistry.h
@@ -294,10 +294,28 @@ __attribute__((always_inline)) INLINE static void chemistry_first_init_spart(
     const struct chemistry_global_data* data, struct spart* restrict sp) {
 
   for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
-    sp->chemistry_data.metal_mass_fraction[i] = data->initial_metallicities[i];
+    /* Bug fix (26.07.2024): Check that the initial me metallicities are non
+       negative. */
+    if (data->initial_metallicities[i] >= 0) {
+      /* Use the value from the parameter file */
+      sp->chemistry_data.metal_mass_fraction[i] =
+          data->initial_metallicities[i];
+    }
+    /* else : Use the value from the IC. We are reading the metal mass
+       fraction. So do not overwrite the metallicities */
   }
 }
 
+/**
+ * @brief Sets the chemistry properties of the sink particles to a valid start
+ * state.
+ *
+ * @param data The global chemistry information.
+ * @param sink Pointer to the sink particle data.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_first_init_sink(
+    const struct chemistry_global_data* data, struct sink* restrict sink) {}
+
 /**
  * @brief Initialise the chemistry properties of a black hole with
  * the chemistry properties of the gas it is born from.
diff --git a/src/chemistry/AGORA/chemistry_additions.h b/src/chemistry/AGORA/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..d166dc0debef6073f1913c6071a7945854980075
--- /dev/null
+++ b/src/chemistry/AGORA/chemistry_additions.h
@@ -0,0 +1,146 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_AGORA_ADDITIONS_H
+#define SWIFT_CHEMISTRY_AGORA_ADDITIONS_H
+
+/**
+ * @brief Resets the metal mass fluxes for schemes that use them.
+ *
+ * @param p The particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_reset_mass_fluxes(
+    struct part* restrict p) {
+  for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+    p->chemistry_data.metal_mass_fluxes[i] = 0.f;
+  }
+}
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {
+  /* For hydro schemes that exchange mass fluxes between the particles,
+   * we want to advect the metals. */
+  if (p->flux.dt > 0.) {
+
+    /* Check for vacuum? */
+    if (p->conserved.mass + p->flux.mass <= 0.) {
+      for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+        p->chemistry_data.metal_mass[i] = 0.;
+        p->chemistry_data.smoothed_metal_mass_fraction[i] = 0.;
+      }
+      chemistry_reset_mass_fluxes(p);
+      /* Nothing left to do */
+      return;
+    }
+
+    /* apply the metal mass fluxes and reset them */
+    const double* metal_fluxes = p->chemistry_data.metal_mass_fluxes;
+    for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+      p->chemistry_data.metal_mass[i] =
+          fmax(p->chemistry_data.metal_mass[i] + metal_fluxes[i], 0.);
+    }
+
+#ifdef SWIFT_DEBUG_CHECKS
+    const double iron_mass = p->chemistry_data.metal_mass[0];
+    const double total_metal_mass =
+        p->chemistry_data.metal_mass[AGORA_CHEMISTRY_ELEMENT_COUNT - 1];
+    if (iron_mass > total_metal_mass)
+      error("Iron mass grew larger than total metal mass!");
+    if (total_metal_mass > (p->conserved.mass + p->flux.mass) * (1. + 1.e-4))
+      error("Total metal mass grew larger than total particle mass!");
+#endif
+    chemistry_reset_mass_fluxes(p);
+  }
+}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * Metals are advected. I.e. a particle loses metals according to its own
+ * metal mass fractions and gains mass according to the neighboring particle's
+ * mass fractions.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {
+
+  const double mass_flux_integrated = mass_flux * flux_dt;
+  const float mi = pi->conserved.mass;
+  const float mi_inv = mi > 0 ? 1.f / mi : 0.f;
+  const float mj = pj->conserved.mass;
+  const float mj_inv = mj > 0 ? 1.f / mj : 0.f;
+
+  /* Convention: a positive mass flux means that pi is losing said mass and pj
+   * is gaining it. */
+  if (mass_flux > 0.f) {
+    /* pi is losing mass */
+    for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+    }
+  } else {
+    /* pi is gaining mass: */
+    for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+    }
+  }
+
+  /* update pj as well, even if it is inactive (flux.dt < 0.) */
+  if (mode == 1 || pj->flux.dt < 0.f) {
+    if (mass_flux > 0.f) {
+      /* pj is gaining mass */
+      for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+      }
+    } else {
+      /* pj is losing mass */
+      for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+      }
+    }
+  }
+}
+
+#endif  // SWIFT_CHEMISTRY_AGORA_ADDITIONS_H
diff --git a/src/chemistry/AGORA/chemistry_iact.h b/src/chemistry/AGORA/chemistry_iact.h
index 8b433f71dd90fc547256e564ee5a144ff1bd2358..72a14f309d387e5ebfe231d1bf8a7b354fb6fe57 100644
--- a/src/chemistry/AGORA/chemistry_iact.h
+++ b/src/chemistry/AGORA/chemistry_iact.h
@@ -103,7 +103,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
 }
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (symmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -122,13 +122,13 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
  *
  */
 __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, const float a, const float H,
-    const float time_base, const integertime_t t_current,
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H, const float time_base, const integertime_t t_current,
     const struct cosmology *cosmo, const int with_cosmology) {}
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (nonsymmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -147,9 +147,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
  *
  */
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_diffusion(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, const float a, const float H,
-    const float time_base, const integertime_t t_current,
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H, const float time_base, const integertime_t t_current,
     const struct cosmology *cosmo, const int with_cosmology) {}
 
 #endif /* SWIFT_AGORA_CHEMISTRY_IACT_H */
diff --git a/src/chemistry/AGORA/chemistry_io.h b/src/chemistry/AGORA/chemistry_io.h
index 457d1da8aaaf22134ca9100e1ad4f9fc68ddeffe..8704873707d0c4bbff45652221a367666832ee0c 100644
--- a/src/chemistry/AGORA/chemistry_io.h
+++ b/src/chemistry/AGORA/chemistry_io.h
@@ -99,6 +99,19 @@ INLINE static int chemistry_write_sparticles(const struct spart* sparts,
   return 1;
 }
 
+/**
+ * @brief Specifies which sink fields to write to a dataset
+ *
+ * @param sinks The #sink array.
+ * @param list The list of i/o properties to write.
+ *
+ * @return Returns the number of fields to write.
+ */
+INLINE static int chemistry_write_sinkparticles(const struct sink* sinks,
+                                                struct io_props* list) {
+  return 0;
+}
+
 /**
  * @brief Specifies which bparticle fields to write to a dataset
  *
diff --git a/src/chemistry/AGORA/chemistry_struct.h b/src/chemistry/AGORA/chemistry_struct.h
index 5bd442d6b91858bd529a482f8bf9ac066845cd42..4c49179ff0ceef279b4354b2dd8bde2fe6d8e638 100644
--- a/src/chemistry/AGORA/chemistry_struct.h
+++ b/src/chemistry/AGORA/chemistry_struct.h
@@ -46,6 +46,11 @@ struct chemistry_part_data {
   /*! Total mass of element in a particle. */
   double metal_mass[AGORA_CHEMISTRY_ELEMENT_COUNT];
 
+#ifdef HYDRO_DOES_MASS_FLUX
+  /*! Mass fluxes of the metals in a given element */
+  double metal_mass_fluxes[AGORA_CHEMISTRY_ELEMENT_COUNT];
+#endif
+
   /*! Smoothed fraction of the particle mass in a given element */
   double smoothed_metal_mass_fraction[AGORA_CHEMISTRY_ELEMENT_COUNT];
 };
diff --git a/src/chemistry/EAGLE/chemistry.h b/src/chemistry/EAGLE/chemistry.h
index f8d0815e78018955b601ccb8a6ed796c9318a0ec..52d0ffb1b7e81335c2eb987291e862021497c030 100644
--- a/src/chemistry/EAGLE/chemistry.h
+++ b/src/chemistry/EAGLE/chemistry.h
@@ -209,6 +209,16 @@ __attribute__((always_inline)) INLINE static void chemistry_first_init_spart(
   }
 }
 
+/**
+ * @brief Sets the chemistry properties of the sink particles to a valid start
+ * state.
+ *
+ * @param data The global chemistry information.
+ * @param sink Pointer to the sink particle data.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_first_init_sink(
+    const struct chemistry_global_data* data, struct sink* restrict sink) {}
+
 /**
  * @brief Initialises the chemistry properties.
  *
diff --git a/src/chemistry/EAGLE/chemistry_additions.h b/src/chemistry/EAGLE/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..21fbc402fc2715b11b1dcc861e6cf6d22ec14cc9
--- /dev/null
+++ b/src/chemistry/EAGLE/chemistry_additions.h
@@ -0,0 +1,183 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_EAGLE_ADDITIONS_H
+#define SWIFT_CHEMISTRY_EAGLE_ADDITIONS_H
+
+/**
+ * @brief Resets the metal mass fluxes for schemes that use them.
+ *
+ * @param p The particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_reset_mass_fluxes(
+    struct part* restrict p) {
+  for (int i = 0; i < chemistry_element_count; i++) {
+    p->chemistry_data.metal_mass_fluxes[i] = 0.f;
+  }
+  p->chemistry_data.metal_mass_flux_total = 0.f;
+}
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {
+  /* For hydro schemes that exchange mass fluxes between the particles,
+   * we want to advect the metals. */
+  if (p->flux.dt > 0.) {
+
+    /* update the metal mass fractions */
+
+    /* First compute the current metal masses */
+    const float current_mass_total = p->conserved.mass;
+    const float current_metal_mass_total =
+        current_mass_total * p->chemistry_data.metal_mass_fraction_total;
+    float current_metal_masses[chemistry_element_count];
+    for (int i = 0; i < chemistry_element_count; i++) {
+      current_metal_masses[i] =
+          current_mass_total * p->chemistry_data.metal_mass_fraction[i];
+    }
+
+    /* Add the mass fluxes */
+    const float new_mass_total = current_mass_total + p->flux.mass;
+
+    /* Check for vacuum? */
+    if (new_mass_total <= 0.) {
+      for (int i = 0; i < chemistry_element_count; i++) {
+        p->chemistry_data.metal_mass_fraction[i] = 0.f;
+        p->chemistry_data.smoothed_metal_mass_fraction[i] = 0.f;
+      }
+      p->chemistry_data.metal_mass_fraction_total = 0.f;
+      chemistry_reset_mass_fluxes(p);
+      /* Nothing left to do */
+      return;
+    }
+
+    const float new_metal_mass_total =
+        current_metal_mass_total + p->chemistry_data.metal_mass_flux_total;
+    float new_metal_masses[chemistry_element_count];
+    for (int i = 0; i < chemistry_element_count; i++) {
+      new_metal_masses[i] = fmaxf(
+          current_metal_masses[i] + p->chemistry_data.metal_mass_fluxes[i],
+          0.f);
+    }
+
+    /* Finally update the metal mass ratios and reset the fluxes */
+    const float new_mass_total_inv = 1.f / new_mass_total;
+    p->chemistry_data.metal_mass_fraction_total =
+        new_metal_mass_total * new_mass_total_inv;
+    for (int i = 0; i < chemistry_element_count; i++) {
+      p->chemistry_data.metal_mass_fraction[i] =
+          new_metal_masses[i] * new_mass_total_inv;
+    }
+
+#ifdef SWIFT_DEBUG_CHECKS
+    if (p->chemistry_data.metal_mass_fraction_total > 1.)
+      error("Total metal mass fraction grew larger than 1!");
+    float sum = 0.f;
+    for (int i = 0; i < chemistry_element_count; i++) {
+      sum += p->chemistry_data.metal_mass_fraction[i];
+    }
+    sum -= p->chemistry_data.metal_mass_fraction[chemistry_element_H];
+    sum -= p->chemistry_data.metal_mass_fraction[chemistry_element_He];
+    if (sum > p->chemistry_data.metal_mass_fraction_total)
+      error(
+          "Sum of element-wise metal mass fractions grew larger than total "
+          "metal mass fraction!");
+#endif
+    chemistry_reset_mass_fluxes(p);
+  }
+}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * Metals are advected. I.e. a particle loses metals according to its own
+ * metal mass fractions and gains mass according to the neighboring particle's
+ * mass fractions.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {
+
+  const float mass_flux_integrated = mass_flux * flux_dt;
+
+  /* Convention: a positive mass flux means that pi is losing said mass and pj
+   * is gaining it. */
+  if (mass_flux > 0.f) {
+    /* pi is losing mass */
+    pi->chemistry_data.metal_mass_flux_total -=
+        mass_flux_integrated * pi->chemistry_data.metal_mass_fraction_total;
+    for (int i = 0; i < chemistry_element_count; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pi->chemistry_data.metal_mass_fraction[i];
+    }
+  } else {
+    /* pi is gaining mass: */
+    pi->chemistry_data.metal_mass_flux_total -=
+        mass_flux_integrated * pj->chemistry_data.metal_mass_fraction_total;
+    for (int i = 0; i < chemistry_element_count; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pj->chemistry_data.metal_mass_fraction[i];
+    }
+  }
+
+  /* update pj as well, even if it is inactive (flux.dt < 0.) */
+  if (mode == 1 || pj->flux.dt < 0.f) {
+    if (mass_flux > 0.f) {
+      /* pj is gaining mass */
+      pj->chemistry_data.metal_mass_flux_total +=
+          mass_flux_integrated * pi->chemistry_data.metal_mass_fraction_total;
+      for (int i = 0; i < chemistry_element_count; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pi->chemistry_data.metal_mass_fraction[i];
+      }
+    } else {
+      /* pj is losing mass */
+      pj->chemistry_data.metal_mass_flux_total +=
+          mass_flux_integrated * pj->chemistry_data.metal_mass_fraction_total;
+      for (int i = 0; i < chemistry_element_count; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pj->chemistry_data.metal_mass_fraction[i];
+      }
+    }
+  }
+}
+
+#endif  // SWIFT_CHEMISTRY_EAGLE_ADDITIONS_H
diff --git a/src/chemistry/EAGLE/chemistry_iact.h b/src/chemistry/EAGLE/chemistry_iact.h
index 290f399a5e73da9141263476880f77878fba3f61..3c1e151960b2489d82b2216f5ec31a9ec9208671 100644
--- a/src/chemistry/EAGLE/chemistry_iact.h
+++ b/src/chemistry/EAGLE/chemistry_iact.h
@@ -133,7 +133,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
 }
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (symmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -158,7 +158,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
     const struct cosmology *cosmo, const int with_cosmology) {}
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (nonsymmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
diff --git a/src/chemistry/EAGLE/chemistry_io.h b/src/chemistry/EAGLE/chemistry_io.h
index 6f61e41dc80b94525887f859afca809956661b82..13acd11a782ac110fc3301ca54888f942c59caea 100644
--- a/src/chemistry/EAGLE/chemistry_io.h
+++ b/src/chemistry/EAGLE/chemistry_io.h
@@ -214,6 +214,19 @@ INLINE static int chemistry_write_sparticles(const struct spart* sparts,
   return 12;
 }
 
+/**
+ * @brief Specifies which sink fields to write to a dataset
+ *
+ * @param sinks The #sink array.
+ * @param list The list of i/o properties to write.
+ *
+ * @return Returns the number of fields to write.
+ */
+INLINE static int chemistry_write_sinkparticles(const struct sink* sinks,
+                                                struct io_props* list) {
+  return 0;
+}
+
 /**
  * @brief Specifies which black hole particle fields to write to a dataset
  *
diff --git a/src/chemistry/EAGLE/chemistry_struct.h b/src/chemistry/EAGLE/chemistry_struct.h
index eedcfb8f977c693d9001948136c7a5fc81b7ef95..4d50514acbc94a71d95d50db5663b37d2f73512b 100644
--- a/src/chemistry/EAGLE/chemistry_struct.h
+++ b/src/chemistry/EAGLE/chemistry_struct.h
@@ -19,6 +19,8 @@
 #ifndef SWIFT_CHEMISTRY_STRUCT_EAGLE_H
 #define SWIFT_CHEMISTRY_STRUCT_EAGLE_H
 
+#include <config.h>
+
 /**
  * @brief The individual elements traced in the EAGLE model.
  */
@@ -58,6 +60,14 @@ struct chemistry_part_data {
   /*! Fraction of the particle mass in *all* metals */
   float metal_mass_fraction_total;
 
+#ifdef HYDRO_DOES_MASS_FLUX
+  /*! Mass fluxes of the metals in a given element */
+  float metal_mass_fluxes[chemistry_element_count];
+
+  /*! Mass flux in *all* metals */
+  float metal_mass_flux_total;
+#endif
+
   /*! Smoothed fraction of the particle mass in a given element */
   float smoothed_metal_mass_fraction[chemistry_element_count];
 
diff --git a/src/chemistry/GEAR/chemistry.h b/src/chemistry/GEAR/chemistry.h
index 6267ffb514ea68984f9669e4c93a8e44a3bf5f5a..f9c4282e04cc999d7560cca0559f39bd1216abdb 100644
--- a/src/chemistry/GEAR/chemistry.h
+++ b/src/chemistry/GEAR/chemistry.h
@@ -48,15 +48,52 @@
 INLINE static void chemistry_copy_star_formation_properties(
     struct part* p, const struct xpart* xp, struct spart* sp) {
 
+  /* gas mass after update */
   float mass = hydro_get_mass(p);
 
   /* Store the chemistry struct in the star particle */
-  for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
-    sp->chemistry_data.metal_mass_fraction[i] =
-        p->chemistry_data.smoothed_metal_mass_fraction[i];
+  for (int k = 0; k < GEAR_CHEMISTRY_ELEMENT_COUNT; k++) {
+    sp->chemistry_data.metal_mass_fraction[k] =
+        p->chemistry_data.smoothed_metal_mass_fraction[k];
 
     /* Remove the metals taken by the star. */
-    p->chemistry_data.metal_mass[i] *= mass / (mass + sp->mass);
+    p->chemistry_data.metal_mass[k] *= mass / (mass + sp->mass);
+  }
+}
+
+/**
+ * @brief Copies the chemistry properties of the sink particle over to the
+ * stellar particle.
+ *
+ * @param sink the sink particle with its properties.
+ * @param sp the new star particles.
+ */
+INLINE static void chemistry_copy_sink_properties_to_star(struct sink* sink,
+                                                          struct spart* sp) {
+
+  /* Store the chemistry struct in the star particle */
+  for (int k = 0; k < GEAR_CHEMISTRY_ELEMENT_COUNT; k++) {
+    sp->chemistry_data.metal_mass_fraction[k] =
+        sink->chemistry_data.metal_mass_fraction[k];
+  }
+}
+
+/**
+ * @brief Copies the chemistry properties of the gas particle over to the
+ * sink particle.
+ *
+ * @param p the gas particles.
+ * @param xp the additional properties of the gas particles.
+ * @param sink the new created star particle with its properties.
+ */
+INLINE static void chemistry_copy_sink_properties(const struct part* p,
+                                                  const struct xpart* xp,
+                                                  struct sink* sink) {
+
+  /* Store the chemistry struct in the star particle */
+  for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+    sink->chemistry_data.metal_mass_fraction[i] =
+        p->chemistry_data.smoothed_metal_mass_fraction[i];
   }
 }
 
@@ -68,8 +105,9 @@ INLINE static void chemistry_copy_star_formation_properties(
  */
 static INLINE void chemistry_print_backend(
     const struct chemistry_global_data* data) {
-
-  message("Chemistry function is 'Gear'.");
+  if (engine_rank == 0) {
+    message("Chemistry function is 'Gear'.");
+  }
 }
 
 /**
@@ -246,10 +284,12 @@ static INLINE void chemistry_init_backend(struct swift_params* parameter_file,
   const float initial_metallicity = parser_get_param_float(
       parameter_file, "GEARChemistry:initial_metallicity");
 
-  if (initial_metallicity < 0) {
-    message("Setting the initial metallicity from the snapshot.");
-  } else {
-    message("Setting the initial metallicity from the parameter file.");
+  if (engine_rank == 0) {
+    if (initial_metallicity < 0) {
+      message("Setting the initial metallicity from the snapshot.");
+    } else {
+      message("Setting the initial metallicity from the parameter file.");
+    }
   }
 
   /* Set the initial metallicities */
@@ -426,7 +466,37 @@ __attribute__((always_inline)) INLINE static void chemistry_first_init_spart(
     const struct chemistry_global_data* data, struct spart* restrict sp) {
 
   for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
-    sp->chemistry_data.metal_mass_fraction[i] = data->initial_metallicities[i];
+    /* Bug fix (26.07.2024): Check that the initial me metallicities are non
+       negative. */
+    if (data->initial_metallicities[i] >= 0) {
+      /* Use the value from the parameter file */
+      sp->chemistry_data.metal_mass_fraction[i] =
+          data->initial_metallicities[i];
+    }
+    /* else : Use the value from the IC. We are reading the metal mass
+     fraction. So do not overwrite the metallicities */
+  }
+}
+
+/* Add chemistry first init sink ? */
+
+/**
+ * @brief Sets the chemistry properties of the sink particles to a valid start
+ * state.
+ *
+ * @param data The global chemistry information.
+ * @param sink Pointer to the sink particle data.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_first_init_sink(
+    const struct chemistry_global_data* data, struct sink* restrict sink) {
+
+  for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+    /* Use the value from the parameter file */
+    if (data->initial_metallicities[i] >= 0) {
+      sink->chemistry_data.metal_mass_fraction[i] =
+          data->initial_metallicities[i];
+    }
+    /* else : read the metallicities from the ICs. */
   }
 }
 
@@ -435,13 +505,17 @@ __attribute__((always_inline)) INLINE static void chemistry_first_init_spart(
  *
  * @param si_data The black hole data to add to.
  * @param sj_data The gas data to use.
- * @param gas_mass The mass of the gas particle.
+ * @param mi_old The mass of the #sink i before accreting the #part p.
  */
 __attribute__((always_inline)) INLINE static void chemistry_add_sink_to_sink(
-    struct chemistry_sink_data* si_data,
-    const struct chemistry_sink_data* sj_data) {
+    struct sink* si, const struct sink* sj, const double mi_old) {
+
+  for (int k = 0; k < GEAR_CHEMISTRY_ELEMENT_COUNT; k++) {
+    double mk = si->chemistry_data.metal_mass_fraction[k] * mi_old +
+                sj->chemistry_data.metal_mass_fraction[k] * sj->mass;
 
-  // To be implemented.
+    si->chemistry_data.metal_mass_fraction[k] = mk / si->mass;
+  }
 }
 
 /**
@@ -449,13 +523,20 @@ __attribute__((always_inline)) INLINE static void chemistry_add_sink_to_sink(
  *
  * @param sp_data The sink data to add to.
  * @param p_data The gas data to use.
- * @param gas_mass The mass of the gas particle.
+ * @param ms_old The mass of the #sink before accreting the #part p.
  */
 __attribute__((always_inline)) INLINE static void chemistry_add_part_to_sink(
-    struct chemistry_sink_data* sp_data,
-    const struct chemistry_part_data* p_data, const double gas_mass) {
+    struct sink* s, const struct part* p, const double ms_old) {
+
+  /* gas mass */
+  const float mass = hydro_get_mass(p);
 
-  // To be implemented.
+  for (int k = 0; k < GEAR_CHEMISTRY_ELEMENT_COUNT; k++) {
+    double mk = s->chemistry_data.metal_mass_fraction[k] * ms_old +
+                p->chemistry_data.smoothed_metal_mass_fraction[k] * mass;
+
+    s->chemistry_data.metal_mass_fraction[k] = mk / s->mass;
+  }
 }
 
 /**
@@ -564,6 +645,20 @@ chemistry_get_star_total_iron_mass_fraction_for_feedback(
   return sp->chemistry_data.metal_mass_fraction[0];
 }
 
+/**
+ * @brief Returns the total iron mass fraction of the
+ * sink particle to be used in feedback/enrichment related routines.
+ * We assume iron to be stored at index 0.
+ *
+ * @param sp Pointer to the particle data.
+ */
+__attribute__((always_inline)) INLINE static double
+chemistry_get_sink_total_iron_mass_fraction_for_feedback(
+    const struct sink* restrict sink) {
+
+  return sink->chemistry_data.metal_mass_fraction[0];
+}
+
 /**
  * @brief Returns the abundances (metal mass fraction) of the
  * star particle to be used in feedback/enrichment related routines.
diff --git a/src/chemistry/GEAR/chemistry_additions.h b/src/chemistry/GEAR/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..a872e636a57e37a80c02411dd0a3b208ef747a31
--- /dev/null
+++ b/src/chemistry/GEAR/chemistry_additions.h
@@ -0,0 +1,151 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_GEAR_ADDITIONS_H
+#define SWIFT_CHEMISTRY_GEAR_ADDITIONS_H
+
+/**
+ * @brief Resets the metal mass fluxes for schemes that use them.
+ *
+ * @param p The particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_reset_mass_fluxes(
+    struct part* restrict p) {
+  for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+    p->chemistry_data.metal_mass_fluxes[i] = 0.f;
+  }
+}
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {
+  /* For hydro schemes that exchange mass fluxes between the particles,
+   * we want to advect the metals. */
+  if (p->flux.dt > 0.) {
+
+    /* Check for vacuum? */
+    if (p->conserved.mass + p->flux.mass <= 0.) {
+      for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+        p->chemistry_data.metal_mass[i] = 0.;
+        p->chemistry_data.smoothed_metal_mass_fraction[i] = 0.;
+      }
+      chemistry_reset_mass_fluxes(p);
+      /* Nothing left to do */
+      return;
+    }
+
+    /* apply the metal mass fluxes and reset them */
+    const double* metal_fluxes = p->chemistry_data.metal_mass_fluxes;
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+      p->chemistry_data.metal_mass[i] =
+          fmax(p->chemistry_data.metal_mass[i] + metal_fluxes[i], 0.);
+    }
+
+#ifdef SWIFT_DEBUG_CHECKS
+    double sum = 0.;
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT - 1; i++) {
+      sum += p->chemistry_data.metal_mass[i];
+    }
+    const double total_metal_mass =
+        p->chemistry_data.metal_mass[GEAR_CHEMISTRY_ELEMENT_COUNT - 1];
+    if (sum > total_metal_mass)
+      error(
+          "Sum of element-wise metal masses grew larger than total metal "
+          "mass!");
+    if (total_metal_mass > p->conserved.mass + p->flux.mass)
+      error("Total metal mass grew larger than the particle mass!");
+#endif
+    chemistry_reset_mass_fluxes(p);
+  }
+}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * Metals are advected. I.e. a particle loses metals according to its own
+ * metal mass fractions and gains mass according to the neighboring particle's
+ * mass fractions.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {
+
+  const double mass_flux_integrated = mass_flux * flux_dt;
+  const float mi = pi->conserved.mass;
+  const float mi_inv = mi > 0 ? 1.f / mi : 0.f;
+  const float mj = pj->conserved.mass;
+  const float mj_inv = mj > 0 ? 1.f / mj : 0.f;
+
+  /* Convention: a positive mass flux means that pi is losing said mass and pj
+   * is gaining it. */
+  if (mass_flux > 0.f) {
+    /* pi is losing mass */
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+    }
+  } else {
+    /* pi is gaining mass: */
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+    }
+  }
+
+  /* update pj as well, even if it is inactive (flux.dt < 0.) */
+  if (mode == 1 || pj->flux.dt < 0.f) {
+    if (mass_flux > 0.f) {
+      /* pj is gaining mass */
+      for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+      }
+    } else {
+      /* pj is losing mass */
+      for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+      }
+    }
+  }
+}
+
+#endif  // SWIFT_CHEMISTRY_GEAR_ADDITIONS_H
diff --git a/src/chemistry/GEAR/chemistry_iact.h b/src/chemistry/GEAR/chemistry_iact.h
index 40a31cd125e48f5c99cc7265f9cb5c84c75c8dc8..458252320909173f0881e217c15bd20dd205e24f 100644
--- a/src/chemistry/GEAR/chemistry_iact.h
+++ b/src/chemistry/GEAR/chemistry_iact.h
@@ -108,7 +108,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
 }
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (symmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -133,7 +133,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
     const struct cosmology *cosmo, const int with_cosmology) {}
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (nonsymmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
diff --git a/src/chemistry/GEAR/chemistry_io.h b/src/chemistry/GEAR/chemistry_io.h
index 0cba098a7387a125ca3ffba16d4e12f44940640b..83c5c8765733a683eae4521ed85ed2856265c1c6 100644
--- a/src/chemistry/GEAR/chemistry_io.h
+++ b/src/chemistry/GEAR/chemistry_io.h
@@ -107,6 +107,26 @@ INLINE static int chemistry_write_sparticles(const struct spart* sparts,
   return 1;
 }
 
+/**
+ * @brief Specifies which sink fields to write to a dataset
+ *
+ * @param sinks The #sink array.
+ * @param list The list of i/o properties to write.
+ *
+ * @return Returns the number of fields to write.
+ */
+INLINE static int chemistry_write_sinkparticles(const struct sink* sinks,
+                                                struct io_props* list) {
+
+  /* List what we want to write */
+  list[0] = io_make_output_field(
+      "MetalMassFractions", DOUBLE, GEAR_CHEMISTRY_ELEMENT_COUNT,
+      UNIT_CONV_NO_UNITS, 0.f, sinks, chemistry_data.metal_mass_fraction,
+      "Mass fraction of each element");
+
+  return 1;
+}
+
 /**
  * @brief Specifies which black hole particle fields to write to a dataset
  *
diff --git a/src/chemistry/GEAR/chemistry_struct.h b/src/chemistry/GEAR/chemistry_struct.h
index 3b26c112192b29100760aeec5062359bf3dba081..e0ce8111b10cf827df7cc001056e0c46cefc29f3 100644
--- a/src/chemistry/GEAR/chemistry_struct.h
+++ b/src/chemistry/GEAR/chemistry_struct.h
@@ -44,6 +44,11 @@ struct chemistry_part_data {
   /*! Total mass of element in a particle. */
   double metal_mass[GEAR_CHEMISTRY_ELEMENT_COUNT];
 
+#ifdef HYDRO_DOES_MASS_FLUX
+  /*! Mass fluxes of the metals in a given element */
+  double metal_mass_fluxes[GEAR_CHEMISTRY_ELEMENT_COUNT];
+#endif
+
   /*! Smoothed fraction of the particle mass in a given element */
   double smoothed_metal_mass_fraction[GEAR_CHEMISTRY_ELEMENT_COUNT];
 };
@@ -65,6 +70,10 @@ struct chemistry_bpart_data {};
 /**
  * @brief Chemical abundances traced by the #sink in the GEAR model.
  */
-struct chemistry_sink_data {};
+struct chemistry_sink_data {
+
+  /*! Total mass of element in a particle. */
+  double metal_mass_fraction[GEAR_CHEMISTRY_ELEMENT_COUNT];
+};
 
 #endif /* SWIFT_CHEMISTRY_STRUCT_GEAR_H */
diff --git a/src/chemistry/GEAR_DIFFUSION/chemistry.h b/src/chemistry/GEAR_DIFFUSION/chemistry.h
index 80cbcbc4e40909e07a6727dd7b8cc6fe72926dca..3fe6b30da414ef1a0dcb0c863885acb717c14c40 100644
--- a/src/chemistry/GEAR_DIFFUSION/chemistry.h
+++ b/src/chemistry/GEAR_DIFFUSION/chemistry.h
@@ -413,8 +413,35 @@ __attribute__((always_inline)) INLINE static void chemistry_first_init_spart(
     const struct chemistry_global_data* data, struct spart* restrict sp) {
 
   for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
-    sp->chemistry_data.metal_mass_fraction[i] =
-        data->initial_metallicities[i] * sp->mass;
+    /* Bug fix (26.07.2024): Check that the initial me metallicities are non
+       negative. */
+    if (data->initial_metallicities[i] >= 0) {
+      /* Use the value from the parameter file */
+      sp->chemistry_data.metal_mass_fraction[i] =
+          data->initial_metallicities[i];
+    }
+    /* else : Use the value from the IC. We are reading the metal mass
+     fraction. So do not overwrite the metallicities */
+  }
+}
+
+/**
+ * @brief Sets the chemistry properties of the sink particles to a valid start
+ * state.
+ *
+ * @param data The global chemistry information.
+ * @param sink Pointer to the sink particle data.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_first_init_sink(
+    const struct chemistry_global_data* data, struct sink* restrict sink) {
+
+  for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+    /* Use the value from the parameter file */
+    if (data->initial_metallicities[i] >= 0) {
+      sink->chemistry_data.metal_mass_fraction[i] =
+          data->initial_metallicities[i];
+    }
+    /* else : read the metallicities from the ICs. */
   }
 }
 
diff --git a/src/chemistry/GEAR_DIFFUSION/chemistry_iact.h b/src/chemistry/GEAR_DIFFUSION/chemistry_iact.h
index 1c88378b20d0b9ceddba4012dc3402ca191b40e2..c569ea4897a42963c5dec5c5179577cd051616e4 100644
--- a/src/chemistry/GEAR_DIFFUSION/chemistry_iact.h
+++ b/src/chemistry/GEAR_DIFFUSION/chemistry_iact.h
@@ -140,7 +140,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
 }
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (symmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -226,7 +226,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
 }
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (nonsymmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
diff --git a/src/chemistry/GEAR_DIFFUSION/chemistry_io.h b/src/chemistry/GEAR_DIFFUSION/chemistry_io.h
index 29761c0efc759533b7cf53f105f7d6575ee9d790..de08b83c29d320818378a99029b34d99430ea0f7 100644
--- a/src/chemistry/GEAR_DIFFUSION/chemistry_io.h
+++ b/src/chemistry/GEAR_DIFFUSION/chemistry_io.h
@@ -106,6 +106,19 @@ INLINE static int chemistry_write_sparticles(const struct spart* sparts,
   return 1;
 }
 
+/**
+ * @brief Specifies which sink fields to write to a dataset
+ *
+ * @param sinks The #sink array.
+ * @param list The list of i/o properties to write.
+ *
+ * @return Returns the number of fields to write.
+ */
+INLINE static int chemistry_write_sinkparticles(const struct sink* sinks,
+                                                struct io_props* list) {
+  return 0;
+}
+
 /**
  * @brief Specifies which black hole particle fields to write to a dataset
  *
diff --git a/src/chemistry/QLA/chemistry.h b/src/chemistry/QLA/chemistry.h
index 4c09c0bb8972f0ef9f4b79bd9fdea7a77cd96bbb..d1909b9cc68f21541c198e9f06cfed80be7a7a44 100644
--- a/src/chemistry/QLA/chemistry.h
+++ b/src/chemistry/QLA/chemistry.h
@@ -189,6 +189,16 @@ __attribute__((always_inline)) INLINE static void chemistry_init_part(
 __attribute__((always_inline)) INLINE static void chemistry_first_init_spart(
     const struct chemistry_global_data* data, struct spart* restrict sp) {}
 
+/**
+ * @brief Sets the chemistry properties of the sink particles to a valid start
+ * state.
+ *
+ * @param data The global chemistry information.
+ * @param sink Pointer to the sink particle data.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_first_init_sink(
+    const struct chemistry_global_data* data, struct sink* restrict sink) {}
+
 /**
  * @brief Initialise the chemistry properties of a black hole with
  * the chemistry properties of the gas it is born from.
diff --git a/src/chemistry/QLA/chemistry_additions.h b/src/chemistry/QLA/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..2cfcddeee2d9d57fd62203bddf8f17c4a7f6ebc0
--- /dev/null
+++ b/src/chemistry/QLA/chemistry_additions.h
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_QLA_ADDITIONS_H
+#define SWIFT_CHEMISTRY_QLA_ADDITIONS_H
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {}
+
+#endif  // SWIFT_CHEMISTRY_QLA_ADDITIONS_H
diff --git a/src/chemistry/QLA/chemistry_iact.h b/src/chemistry/QLA/chemistry_iact.h
index f35d780148ecc20847f4b2858aa805bd9cb2a8fe..314d4944a49b374f084b483b2ca341f6d886b224 100644
--- a/src/chemistry/QLA/chemistry_iact.h
+++ b/src/chemistry/QLA/chemistry_iact.h
@@ -61,7 +61,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
     const float H) {}
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (symmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -86,7 +86,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
     const struct cosmology *cosmo, const int with_cosmology) {}
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (nonsymmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
diff --git a/src/chemistry/QLA/chemistry_io.h b/src/chemistry/QLA/chemistry_io.h
index d02da1ec8e1f83441eaa18398f8e795eeccf5a48..6f0e3764dc7de92c6a5a475924a29a3deb99ccd3 100644
--- a/src/chemistry/QLA/chemistry_io.h
+++ b/src/chemistry/QLA/chemistry_io.h
@@ -76,6 +76,19 @@ INLINE static int chemistry_write_sparticles(const struct spart* sparts,
   return 0;
 }
 
+/**
+ * @brief Specifies which sink fields to write to a dataset
+ *
+ * @param sinks The #sink array.
+ * @param list The list of i/o properties to write.
+ *
+ * @return Returns the number of fields to write.
+ */
+INLINE static int chemistry_write_sinkparticles(const struct sink* sinks,
+                                                struct io_props* list) {
+  return 0;
+}
+
 /**
  * @brief Specifies which bparticle fields to write to a dataset
  *
diff --git a/src/chemistry/none/chemistry.h b/src/chemistry/none/chemistry.h
index 239bc6a0047760962b0024073f4fddb6c1e632bc..39da65af78c0660206c84644f57f94546005698c 100644
--- a/src/chemistry/none/chemistry.h
+++ b/src/chemistry/none/chemistry.h
@@ -192,6 +192,16 @@ __attribute__((always_inline)) INLINE static void chemistry_init_part(
 __attribute__((always_inline)) INLINE static void chemistry_first_init_spart(
     const struct chemistry_global_data* data, struct spart* restrict sp) {}
 
+/**
+ * @brief Sets the chemistry properties of the sink particles to a valid start
+ * state.
+ *
+ * @param data The global chemistry information.
+ * @param sink Pointer to the sink particle data.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_first_init_sink(
+    const struct chemistry_global_data* data, struct sink* restrict sink) {}
+
 /**
  * @brief Initialise the chemistry properties of a black hole with
  * the chemistry properties of the gas it is born from.
diff --git a/src/chemistry/none/chemistry_additions.h b/src/chemistry/none/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..3401dd245ff02b9f39d414317a553d260c05f470
--- /dev/null
+++ b/src/chemistry/none/chemistry_additions.h
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_NONE_ADDITIONS_H
+#define SWIFT_CHEMISTRY_NONE_ADDITIONS_H
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {}
+
+#endif  // SWIFT_CHEMISTRY_NONE_ADDITIONS_H
diff --git a/src/chemistry/none/chemistry_iact.h b/src/chemistry/none/chemistry_iact.h
index f662881c8f7633d4cdf74981328d0fec4d4d1df6..29a9aefec8bfae39c320accb92c9c2760c0cabb5 100644
--- a/src/chemistry/none/chemistry_iact.h
+++ b/src/chemistry/none/chemistry_iact.h
@@ -39,8 +39,9 @@
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_chemistry(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, const float a, const float H) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {}
 
 /**
  * @brief do chemistry computation after the runner_iact_density (non symmetric
@@ -56,11 +57,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_chemistry(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    const struct part *restrict pj, const float a, const float H) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, const struct part *restrict pj, const float a,
+    const float H) {}
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (symmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -79,13 +81,13 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_chemistry(
  *
  */
 __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, const float a, const float H,
-    const float time_base, const integertime_t t_current,
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H, const float time_base, const integertime_t t_current,
     const struct cosmology *cosmo, const int with_cosmology) {}
 
 /**
- * @brief do metal diffusion computation in the <FORCE LOOP>
+ * @brief do metal diffusion computation in the force loop
  * (nonsymmetric version)
  *
  * @param r2 Comoving square distance between the two particles.
@@ -104,9 +106,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_diffusion(
  *
  */
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_diffusion(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, const float a, const float H,
-    const float time_base, const integertime_t t_current,
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H, const float time_base, const integertime_t t_current,
     const struct cosmology *cosmo, const int with_cosmology) {}
 
 #endif /* SWIFT_NONE_CHEMISTRY_IACT_H */
diff --git a/src/chemistry/none/chemistry_io.h b/src/chemistry/none/chemistry_io.h
index 062c0339dcf20935934b55c006ad5d30b9d7b467..ba45e207866918f6b40f727f7c28f04f1f77fb95 100644
--- a/src/chemistry/none/chemistry_io.h
+++ b/src/chemistry/none/chemistry_io.h
@@ -76,6 +76,19 @@ INLINE static int chemistry_write_sparticles(const struct spart* sparts,
   return 0;
 }
 
+/**
+ * @brief Specifies which sink fields to write to a dataset
+ *
+ * @param sinks The #sink array.
+ * @param list The list of i/o properties to write.
+ *
+ * @return Returns the number of fields to write.
+ */
+INLINE static int chemistry_write_sinkparticles(const struct sink* sinks,
+                                                struct io_props* list) {
+  return 0;
+}
+
 /**
  * @brief Specifies which bparticle fields to write to a dataset
  *
diff --git a/src/chemistry_additions.h b/src/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..ae8f3bfa3658898286cd6e88eb87cf6d5eac39f2
--- /dev/null
+++ b/src/chemistry_additions.h
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_CHEMISTRY_ADDITIONS_H
+#define SWIFT_CHEMISTRY_ADDITIONS_H
+
+/**
+ * @file src/chemistry_additions.h
+ * @brief Branches between the different additional functions required outside
+ * of the chemistry files (e.g. in hydro loops);
+ * Specifically for functions used for advection of tracked elements for
+ * hydro schemes with mass fluxes.
+ **/
+
+/* Config parameters. */
+#include <config.h>
+
+#ifdef HYDRO_DOES_MASS_FLUX
+/* Import the right chemistry definition */
+#if defined(CHEMISTRY_AGORA)
+#include "./chemistry/AGORA/chemistry_additions.h"
+#elif defined(CHEMISTRY_EAGLE)
+#include "./chemistry/EAGLE/chemistry_additions.h"
+#elif defined(CHEMISTRY_GEAR)
+#include "./chemistry/GEAR/chemistry_additions.h"
+#elif defined(CHEMISTRY_NONE)
+#include "./chemistry/none/chemistry_additions.h"
+#elif defined(CHEMISTRY_NONE)
+#include "./chemistry/QLA/chemistry_additions.h"
+#else
+#error "Metal advection unimpmlemented for selected chemistry scheme!"
+#endif
+#else
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {}
+#endif
+
+#endif  // SWIFT_CHEMISTRY_ADDITIONS_H
diff --git a/src/common_io.c b/src/common_io.c
index 145eba9015802b8a5ecc871c16ddfd02bff37559..c64e6470aab0999247f2b214390526a56e2cc71b 100644
--- a/src/common_io.c
+++ b/src/common_io.c
@@ -101,6 +101,8 @@ hid_t io_hdf5_type(enum IO_DATA_TYPE type) {
       return H5T_NATIVE_DOUBLE;
     case CHAR:
       return H5T_NATIVE_CHAR;
+    case BOOL:
+      return H5T_NATIVE_HBOOL;
     default:
       error("Unknown type");
       return 0;
@@ -470,6 +472,16 @@ void io_write_attribute_i(hid_t grp, const char* name, int data) {
   io_write_attribute(grp, name, INT, &data, 1);
 }
 
+/**
+ * @brief Writes a bool value (passed as an int) as an attribute
+ * @param grp The group in which to write
+ * @param name The name of the attribute
+ * @param data The value to write
+ */
+void io_write_attribute_b(hid_t grp, const char* name, int data) {
+  io_write_attribute(grp, name, BOOL, &data, 1);
+}
+
 /**
  * @brief Writes a long value as an attribute
  * @param grp The group in which to write
@@ -507,10 +519,12 @@ void io_write_attribute_s(hid_t grp, const char* name, const char* str) {
  * @param e The #engine containing the meta-data.
  * @param internal_units The system of units used internally.
  * @param snapshot_units The system of units used in snapshots.
+ * @param fof Is this a FOF output? If so don't write subgrid info.
  */
 void io_write_meta_data(hid_t h_file, const struct engine* e,
                         const struct unit_system* internal_units,
-                        const struct unit_system* snapshot_units) {
+                        const struct unit_system* snapshot_units,
+                        const int fof) {
 
   hid_t h_grp;
 
@@ -523,54 +537,57 @@ void io_write_meta_data(hid_t h_file, const struct engine* e,
   /* Print the physical constants */
   phys_const_print_snapshot(h_file, e->physical_constants);
 
-  /* Print the SPH parameters */
-  if (e->policy & engine_policy_hydro) {
-    h_grp = H5Gcreate(h_file, "/HydroScheme", H5P_DEFAULT, H5P_DEFAULT,
+  if (!fof) {
+
+    /* Print the SPH parameters */
+    if (e->policy & engine_policy_hydro) {
+      h_grp = H5Gcreate(h_file, "/HydroScheme", H5P_DEFAULT, H5P_DEFAULT,
+                        H5P_DEFAULT);
+      if (h_grp < 0) error("Error while creating SPH group");
+      hydro_props_print_snapshot(h_grp, e->hydro_properties);
+      hydro_write_flavour(h_grp);
+      mhd_write_flavour(h_grp);
+      H5Gclose(h_grp);
+    }
+
+    /* Print the subgrid parameters */
+    h_grp = H5Gcreate(h_file, "/SubgridScheme", H5P_DEFAULT, H5P_DEFAULT,
                       H5P_DEFAULT);
-    if (h_grp < 0) error("Error while creating SPH group");
-    hydro_props_print_snapshot(h_grp, e->hydro_properties);
-    hydro_write_flavour(h_grp);
-    mhd_write_flavour(h_grp);
+    if (h_grp < 0) error("Error while creating subgrid group");
+    hid_t h_grp_columns =
+        H5Gcreate(h_grp, "NamedColumns", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
+    if (h_grp_columns < 0) error("Error while creating named columns group");
+    entropy_floor_write_flavour(h_grp);
+    extra_io_write_flavour(h_grp, h_grp_columns);
+    cooling_write_flavour(h_grp, h_grp_columns, e->cooling_func);
+    chemistry_write_flavour(h_grp, h_grp_columns, e);
+    tracers_write_flavour(h_grp);
+    feedback_write_flavour(e->feedback_props, h_grp);
+    rt_write_flavour(h_grp, h_grp_columns, e, internal_units, snapshot_units,
+                     e->rt_props);
     H5Gclose(h_grp);
-  }
 
-  /* Print the subgrid parameters */
-  h_grp = H5Gcreate(h_file, "/SubgridScheme", H5P_DEFAULT, H5P_DEFAULT,
-                    H5P_DEFAULT);
-  if (h_grp < 0) error("Error while creating subgrid group");
-  hid_t h_grp_columns =
-      H5Gcreate(h_grp, "NamedColumns", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
-  if (h_grp_columns < 0) error("Error while creating named columns group");
-  entropy_floor_write_flavour(h_grp);
-  extra_io_write_flavour(h_grp, h_grp_columns);
-  cooling_write_flavour(h_grp, h_grp_columns, e->cooling_func);
-  chemistry_write_flavour(h_grp, h_grp_columns, e);
-  tracers_write_flavour(h_grp);
-  feedback_write_flavour(e->feedback_props, h_grp);
-  rt_write_flavour(h_grp, h_grp_columns, e, internal_units, snapshot_units,
-                   e->rt_props);
-  H5Gclose(h_grp);
+    /* Print the gravity parameters */
+    if (e->policy & engine_policy_self_gravity) {
+      h_grp = H5Gcreate(h_file, "/GravityScheme", H5P_DEFAULT, H5P_DEFAULT,
+                        H5P_DEFAULT);
+      if (h_grp < 0) error("Error while creating gravity group");
+      gravity_props_print_snapshot(h_grp, e->gravity_properties);
+      H5Gclose(h_grp);
+    }
 
-  /* Print the gravity parameters */
-  if (e->policy & engine_policy_self_gravity) {
-    h_grp = H5Gcreate(h_file, "/GravityScheme", H5P_DEFAULT, H5P_DEFAULT,
-                      H5P_DEFAULT);
-    if (h_grp < 0) error("Error while creating gravity group");
-    gravity_props_print_snapshot(h_grp, e->gravity_properties);
-    H5Gclose(h_grp);
-  }
+    /* Print the stellar parameters */
+    if (e->policy & engine_policy_stars) {
+      h_grp = H5Gcreate(h_file, "/StarsScheme", H5P_DEFAULT, H5P_DEFAULT,
+                        H5P_DEFAULT);
+      if (h_grp < 0) error("Error while creating stars group");
+      stars_props_print_snapshot(h_grp, h_grp_columns, e->stars_properties);
+      H5Gclose(h_grp);
+    }
 
-  /* Print the stellar parameters */
-  if (e->policy & engine_policy_stars) {
-    h_grp = H5Gcreate(h_file, "/StarsScheme", H5P_DEFAULT, H5P_DEFAULT,
-                      H5P_DEFAULT);
-    if (h_grp < 0) error("Error while creating stars group");
-    stars_props_print_snapshot(h_grp, h_grp_columns, e->stars_properties);
-    H5Gclose(h_grp);
+    H5Gclose(h_grp_columns);
   }
 
-  H5Gclose(h_grp_columns);
-
   /* Print the cosmological model  */
   h_grp =
       H5Gcreate(h_file, "/Cosmology", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
@@ -1610,7 +1627,7 @@ void io_collect_gparts_neutrino_to_write(
  */
 void io_make_snapshot_subdir(const char* dirname) {
 
-  if (strcmp(dirname, ".") != 0 && strnlen(dirname, PARSER_MAX_LINE_SIZE) > 0) {
+  if (strcmp(dirname, ".") != 0 && strnlen(dirname, FILENAME_BUFFER_SIZE) > 0) {
     safe_checkdir(dirname, /*create=*/1);
   }
 }
@@ -1795,6 +1812,7 @@ void io_select_sink_fields(const struct sink* const sinks,
                            int* const num_fields, struct io_props* const list) {
 
   sink_write_particles(sinks, list, num_fields, with_cosmology);
+  *num_fields += chemistry_write_sinkparticles(sinks, list + *num_fields);
 }
 
 /**
diff --git a/src/common_io.h b/src/common_io.h
index d7334a83fddf90aca12940cfe3290955fe65f10b..69a89fdf285332d26abe233714c36cb066649bab 100644
--- a/src/common_io.h
+++ b/src/common_io.h
@@ -31,6 +31,8 @@
 #define PARTICLE_GROUP_BUFFER_SIZE 50
 #define FILENAME_BUFFER_SIZE 150
 #define IO_BUFFER_ALIGNMENT 1024
+#define HDF5_LOWEST_FILE_FORMAT_VERSION H5F_LIBVER_V18
+#define HDF5_HIGHEST_FILE_FORMAT_VERSION H5F_LIBVER_LATEST
 
 /* Avoid cyclic inclusion problems */
 struct cell;
@@ -66,6 +68,7 @@ enum IO_DATA_TYPE {
   FLOAT,
   DOUBLE,
   CHAR,
+  BOOL,
   SIZE_T,
 };
 
@@ -95,13 +98,15 @@ void io_write_attribute(hid_t grp, const char* name, enum IO_DATA_TYPE type,
 void io_write_attribute_d(hid_t grp, const char* name, double data);
 void io_write_attribute_f(hid_t grp, const char* name, float data);
 void io_write_attribute_i(hid_t grp, const char* name, int data);
+void io_write_attribute_b(hid_t grp, const char* name, int data);
 void io_write_attribute_l(hid_t grp, const char* name, long data);
 void io_write_attribute_ll(hid_t grp, const char* name, long long data);
 void io_write_attribute_s(hid_t grp, const char* name, const char* str);
 
 void io_write_meta_data(hid_t h_file, const struct engine* e,
                         const struct unit_system* internal_units,
-                        const struct unit_system* snapshot_units);
+                        const struct unit_system* snapshot_units,
+                        const int fof);
 
 void io_write_code_description(hid_t h_file);
 void io_write_engine_policy(hid_t h_file, const struct engine* e);
diff --git a/src/common_io_copy.c b/src/common_io_copy.c
index 2be7cb6e4aacfc6796d6592ae1c8f8abfaf36ac2..38d128a14540936bfb9cd392242ca815d3afaad0 100644
--- a/src/common_io_copy.c
+++ b/src/common_io_copy.c
@@ -488,7 +488,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
 
   } else { /* Converting particle to data */
 
-    if (props.convert_part_f != NULL) {
+    if (props.type == FLOAT && props.parts != NULL) {
 
       /* Prepare some parameters */
       float* temp_f = (float*)temp;
@@ -500,7 +500,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_part_f_mapper, temp_f, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_part_i != NULL) {
+    } else if (props.type == INT && props.parts != NULL) {
 
       /* Prepare some parameters */
       int* temp_i = (int*)temp;
@@ -512,7 +512,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_part_i_mapper, temp_i, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_part_d != NULL) {
+    } else if (props.type == DOUBLE && props.parts != NULL) {
 
       /* Prepare some parameters */
       double* temp_d = (double*)temp;
@@ -524,7 +524,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_part_d_mapper, temp_d, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_part_l != NULL) {
+    } else if (props.type == LONGLONG && props.parts != NULL) {
 
       /* Prepare some parameters */
       long long* temp_l = (long long*)temp;
@@ -536,7 +536,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_part_l_mapper, temp_l, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_gpart_f != NULL) {
+    } else if (props.type == FLOAT && props.gparts != NULL) {
 
       /* Prepare some parameters */
       float* temp_f = (float*)temp;
@@ -548,7 +548,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_gpart_f_mapper, temp_f, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_gpart_i != NULL) {
+    } else if (props.type == INT && props.gparts != NULL) {
 
       /* Prepare some parameters */
       int* temp_i = (int*)temp;
@@ -560,7 +560,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_gpart_i_mapper, temp_i, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_gpart_d != NULL) {
+    } else if (props.type == DOUBLE && props.gparts != NULL) {
 
       /* Prepare some parameters */
       double* temp_d = (double*)temp;
@@ -572,7 +572,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_gpart_d_mapper, temp_d, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_gpart_l != NULL) {
+    } else if (props.type == LONGLONG && props.gparts != NULL) {
 
       /* Prepare some parameters */
       long long* temp_l = (long long*)temp;
@@ -584,7 +584,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_gpart_l_mapper, temp_l, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_spart_f != NULL) {
+    } else if (props.type == FLOAT && props.sparts != NULL) {
 
       /* Prepare some parameters */
       float* temp_f = (float*)temp;
@@ -596,7 +596,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_spart_f_mapper, temp_f, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_spart_i != NULL) {
+    } else if (props.type == INT && props.sparts != NULL) {
 
       /* Prepare some parameters */
       int* temp_i = (int*)temp;
@@ -608,7 +608,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_spart_i_mapper, temp_i, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_spart_d != NULL) {
+    } else if (props.type == DOUBLE && props.sparts != NULL) {
 
       /* Prepare some parameters */
       double* temp_d = (double*)temp;
@@ -620,7 +620,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_spart_d_mapper, temp_d, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_spart_l != NULL) {
+    } else if (props.type == LONGLONG && props.sparts != NULL) {
 
       /* Prepare some parameters */
       long long* temp_l = (long long*)temp;
@@ -632,7 +632,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_spart_l_mapper, temp_l, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_sink_f != NULL) {
+    } else if (props.type == FLOAT && props.sinks != NULL) {
 
       /* Prepare some parameters */
       float* temp_f = (float*)temp;
@@ -644,7 +644,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_sink_f_mapper, temp_f, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_sink_i != NULL) {
+    } else if (props.type == INT && props.sinks != NULL) {
 
       /* Prepare some parameters */
       int* temp_i = (int*)temp;
@@ -656,7 +656,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_sink_i_mapper, temp_i, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_sink_d != NULL) {
+    } else if (props.type == DOUBLE && props.sinks != NULL) {
 
       /* Prepare some parameters */
       double* temp_d = (double*)temp;
@@ -668,7 +668,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_sink_d_mapper, temp_d, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_sink_l != NULL) {
+    } else if (props.type == LONGLONG && props.sinks != NULL) {
 
       /* Prepare some parameters */
       long long* temp_l = (long long*)temp;
@@ -680,7 +680,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_sink_l_mapper, temp_l, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_bpart_f != NULL) {
+    } else if (props.type == FLOAT && props.bparts != NULL) {
 
       /* Prepare some parameters */
       float* temp_f = (float*)temp;
@@ -692,7 +692,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_bpart_f_mapper, temp_f, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_bpart_i != NULL) {
+    } else if (props.type == INT && props.bparts != NULL) {
 
       /* Prepare some parameters */
       int* temp_i = (int*)temp;
@@ -704,7 +704,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_bpart_i_mapper, temp_i, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_bpart_d != NULL) {
+    } else if (props.type == DOUBLE && props.bparts != NULL) {
 
       /* Prepare some parameters */
       double* temp_d = (double*)temp;
@@ -716,7 +716,7 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      io_convert_bpart_d_mapper, temp_d, N, copySize,
                      threadpool_auto_chunk_size, (void*)&props);
 
-    } else if (props.convert_bpart_l != NULL) {
+    } else if (props.type == LONGLONG && props.bparts != NULL) {
 
       /* Prepare some parameters */
       long long* temp_l = (long long*)temp;
@@ -729,7 +729,9 @@ void io_copy_temp_buffer(void* temp, const struct engine* e,
                      threadpool_auto_chunk_size, (void*)&props);
 
     } else {
-      error("Missing conversion function");
+
+      if (N != 0 && props.ptr_func != NULL)
+        error("Missing conversion function");
     }
   }
 
diff --git a/src/common_io_fields.c b/src/common_io_fields.c
index c85bb674759e9742a6fa5937a424088d51717d77..6ca66e11fe97a4acb3f536765e7c4058e104b508 100644
--- a/src/common_io_fields.c
+++ b/src/common_io_fields.c
@@ -400,8 +400,9 @@ void io_write_output_field_parameter(const char* filename, int with_cosmology,
         strcpy(&comment_write_buffer[PARSER_MAX_LINE_SIZE / 2 - 4], "...");
       }
 
-      fprintf(file, "  %s_%s: %s  # %s : %s\n", list[i].name,
-              part_type_names[ptype], "on", comment_write_buffer, unit_buffer);
+      fprintf(file, "  %s_%s: %s  # (%dD - %zd bytes / dim) %s : %s\n",
+              list[i].name, part_type_names[ptype], "on", list[i].dimension,
+              io_sizeof_type(list[i].type), comment_write_buffer, unit_buffer);
     }
 
     fprintf(file, "\n");
diff --git a/src/const.h b/src/const.h
index 429fe2275a518df5c57402ab1f7276e2df39be1e..75be9963f5a87182a7689ba43fb923fe79783075 100644
--- a/src/const.h
+++ b/src/const.h
@@ -27,7 +27,7 @@
 
 /* Type of gradients to use (GIZMO_SPH only) */
 /* If no option is chosen, no gradients are used (first order scheme) */
-//#define GRADIENTS_SPH
+// #define GRADIENTS_SPH
 #define GRADIENTS_GIZMO
 
 /* Types of slope limiter to use (GIZMO_SPH only) */
@@ -40,11 +40,11 @@
 
 /* Options to control the movement of particles for GIZMO_SPH. */
 /* This option disables particle movement */
-//#define GIZMO_FIX_PARTICLES
+// #define GIZMO_FIX_PARTICLES
 /* Try to keep cells regular by adding a correction velocity. */
-//#define GIZMO_STEER_MOTION
+// #define GIZMO_STEER_MOTION
 /* Use the total energy instead of the thermal energy as conserved variable. */
-//#define GIZMO_TOTAL_ENERGY
+// #define GIZMO_TOTAL_ENERGY
 
 /* Options to control handling of unphysical values (GIZMO_SPH only). */
 /* In GIZMO, mass and energy (and hence density and pressure) can in principle
@@ -53,7 +53,7 @@
    If no options are selected below, we assume (and pray) that this will not
    happen, and add no restrictions to how these variables are treated. */
 /* Check for unphysical values and crash if they occur. */
-//#define GIZMO_UNPHYSICAL_ERROR
+// #define GIZMO_UNPHYSICAL_ERROR
 /* Check for unphysical values and reset them to safe values. */
 #define GIZMO_UNPHYSICAL_RESCUE
 /* Show a warning message if an unphysical value was reset (only works if
@@ -65,9 +65,9 @@
 /* Parameters that control how GIZMO handles pathological particle
    configurations. */
 /* Show a warning message if a pathological configuration has been detected. */
-//#define GIZMO_PATHOLOGICAL_WARNING
+// #define GIZMO_PATHOLOGICAL_WARNING
 /* Crash if a pathological configuration has been detected. */
-//#define GIZMO_PATHOLOGICAL_ERROR
+// #define GIZMO_PATHOLOGICAL_ERROR
 /* Maximum allowed gradient matrix condition number. If the condition number of
    the gradient matrix (defined in equation C1 in Hopkins, 2015) is larger than
    this value, we artificially increase the number of neighbours to get a more
@@ -81,26 +81,14 @@
    gradient matrix and use SPH gradients instead. */
 #define const_gizmo_min_wcorr 0.5f
 
-/* Types of gradients to use for SHADOWFAX_SPH */
-/* If no option is chosen, no gradients are used (first order scheme) */
-#define SHADOWFAX_GRADIENTS
-
-/* SHADOWFAX_SPH slope limiters */
-#define SHADOWFAX_SLOPE_LIMITER_PER_FACE
-#define SHADOWFAX_SLOPE_LIMITER_CELL_WIDE
-
-/* Options to control SHADOWFAX_SPH */
-/* This option disables cell movement */
-//#define SHADOWFAX_FIX_CELLS
-/* This option enables cell steering, i.e. trying to keep the cells regular by
-   adding a correction to the cell velocities.*/
-#define SHADOWFAX_STEER_CELL_MOTION
-/* This option evolves the total energy instead of the thermal energy */
-//#define SHADOWFAX_TOTAL_ENERGY
+/* Options controlling ShadowSWIFT */
+/* Options controlling acceleration strategies*/
+/*! @brief Option enabling a more relaxed completeness criterion */
+#define SHADOWSWIFT_RELAXED_COMPLETENESS
 
 /* Source terms */
 #define SOURCETERMS_NONE
-//#define SOURCETERMS_SN_FEEDBACK
+// #define SOURCETERMS_SN_FEEDBACK
 
 /* GRACKLE doesn't really like exact zeroes, so use something
  * comparatively small instead. */
diff --git a/src/cooling/EAGLE/cooling.c b/src/cooling/EAGLE/cooling.c
index 4799488ff3318c7cdb3e592c4e27440a0815dad6..61e5270904238b4dd954122232c4ed7171bf395b 100644
--- a/src/cooling/EAGLE/cooling.c
+++ b/src/cooling/EAGLE/cooling.c
@@ -126,10 +126,13 @@ __attribute__((always_inline)) INLINE void get_redshift_index(
  * @param pressure_floor Properties of the pressure floor.
  * @param cooling The #cooling_function_data used in the run.
  * @param s The space data, including a pointer to array of particles
+ * @param time The current system time
  */
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s) {
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time) {
 
   /* Current redshift */
   const float redshift = cosmo->z;
@@ -523,7 +526,8 @@ void cooling_cool_part(const struct phys_const *phys_const,
   hydro_set_physical_internal_energy_dt(p, cosmo, cooling_du_dt);
 
   /* Store the radiated energy */
-  xp->cooling_data.radiated_energy -= hydro_get_mass(p) * cooling_du_dt * dt;
+  xp->cooling_data.radiated_energy -=
+      hydro_get_mass(p) * (cooling_du_dt - hydro_du_dt) * dt;
 }
 
 /**
@@ -573,6 +577,28 @@ __attribute__((always_inline)) INLINE void cooling_first_init_part(
   xp->cooling_data.radiated_energy = 0.f;
 }
 
+/**
+ * @brief Perform additional init on the cooling properties of the
+ * (x-)particles that requires the density to be known.
+ *
+ * Nothing to do here.
+ *
+ * @param phys_const The physical constant in internal units.
+ * @param us The unit system.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param cosmo The current cosmological model.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE void cooling_post_init_part(
+    const struct phys_const *restrict phys_const,
+    const struct unit_system *restrict us,
+    const struct hydro_props *hydro_props,
+    const struct cosmology *restrict cosmo,
+    const struct cooling_function_data *cooling, const struct part *restrict p,
+    struct xpart *restrict xp) {}
+
 /**
  * @brief Compute the temperature based on gas properties.
  *
@@ -667,6 +693,30 @@ float cooling_get_temperature(
   return exp10(log_10_T);
 }
 
+/**
+ * @brief Compute the electron number density of a #part based on the cooling
+ * function.
+ *
+ * Does not exist in this model. We return 0.
+ *
+ * @param phys_const #phys_const data structure.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param us The internal system of units.
+ * @param cosmo #cosmology data structure.
+ * @param cooling #cooling_function_data struct.
+ * @param p #part data.
+ * @param xp Pointer to the #xpart data.
+ */
+double cooling_get_electron_density(const struct phys_const *phys_const,
+                                    const struct hydro_props *hydro_props,
+                                    const struct unit_system *us,
+                                    const struct cosmology *cosmo,
+                                    const struct cooling_function_data *cooling,
+                                    const struct part *p,
+                                    const struct xpart *xp) {
+  return 0.;
+}
+
 /**
  * @brief Compute the electron pressure of a #part based on the cooling
  * function.
@@ -1059,7 +1109,8 @@ void cooling_restore_tables(struct cooling_function_data *cooling,
   /* Force a re-read of the cooling tables */
   cooling->z_index = -10;
   cooling->previous_z_index = eagle_cooling_N_redshifts - 2;
-  cooling_update(cosmo, /*pfloor=*/NULL, cooling, /*space=*/NULL);
+  cooling_update(/*phys_const=*/NULL, cosmo, /*pfloor=*/NULL, cooling,
+                 /*space=*/NULL, /*time=*/0);
 }
 
 /**
diff --git a/src/cooling/EAGLE/cooling.h b/src/cooling/EAGLE/cooling.h
index ce590c2cf72399e86dd73dc19d145f2a38f9def4..c95964eb6dd5fca78891a3ed43ec4388ad321699 100644
--- a/src/cooling/EAGLE/cooling.h
+++ b/src/cooling/EAGLE/cooling.h
@@ -37,9 +37,11 @@ struct pressure_floor_props;
 struct space;
 struct phys_const;
 
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s);
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time);
 
 void cooling_cool_part(const struct phys_const *phys_const,
                        const struct unit_system *us,
@@ -58,6 +60,14 @@ float cooling_timestep(const struct cooling_function_data *cooling,
                        const struct hydro_props *hydro_props,
                        const struct part *p, const struct xpart *xp);
 
+double cooling_get_electron_density(const struct phys_const *phys_const,
+                                    const struct hydro_props *hydro_props,
+                                    const struct unit_system *us,
+                                    const struct cosmology *cosmo,
+                                    const struct cooling_function_data *cooling,
+                                    const struct part *p,
+                                    const struct xpart *xp);
+
 double cooling_get_electron_pressure(
     const struct phys_const *phys_const, const struct hydro_props *hydro_props,
     const struct unit_system *us, const struct cosmology *cosmo,
@@ -79,6 +89,27 @@ void cooling_first_init_part(
     const struct cooling_function_data *restrict cooling,
     const struct part *restrict p, struct xpart *restrict xp);
 
+/**
+ * @brief Sets the cooling properties of the (x-)particles to a valid start
+ * state. The function requires the density to be defined and thus must
+ * be called after its computation.
+ *
+ * @param phys_const The #phys_const.
+ * @param us The #unit_system.
+ * @param hydro_props The #hydro_props.
+ * @param cosmo The #cosmology.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+void cooling_post_init_part(
+    const struct phys_const *restrict phys_const,
+    const struct unit_system *restrict us,
+    const struct hydro_props *hydro_props,
+    const struct cosmology *restrict cosmo,
+    const struct cooling_function_data *restrict cooling,
+    const struct part *restrict p, struct xpart *restrict xp);
+
 float cooling_get_temperature_from_gas(
     const struct phys_const *phys_const, const struct cosmology *cosmo,
     const struct cooling_function_data *cooling, const float rho_phys,
@@ -147,7 +178,4 @@ void cooling_print_backend(const struct cooling_function_data *cooling);
 
 void cooling_clean(struct cooling_function_data *data);
 
-/*! Stub defined to let the BH model compile */
-#define colibre_cooling_N_elementtypes 1
-
 #endif /* SWIFT_COOLING_EAGLE_H */
diff --git a/src/cooling/PS2020/cooling.c b/src/cooling/PS2020/cooling.c
index 3292841019f4328f7c767cfde1a1bca5fa3db716..fb90cbf687ca3657c122a5572bf65fa5f80035d7 100644
--- a/src/cooling/PS2020/cooling.c
+++ b/src/cooling/PS2020/cooling.c
@@ -72,10 +72,13 @@ static const double bracket_factor = 1.5;
  * @param pressure_floor Properties of the pressure floor.
  * @param cooling The #cooling_function_data used in the run.
  * @param s The space data, including a pointer to array of particles
+ * @param time The current system time
  */
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s) {
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time) {
 
   /* Extra energy for reionization? */
   if (!cooling->H_reion_done) {
@@ -950,6 +953,26 @@ __attribute__((always_inline)) INLINE void cooling_first_init_part(
   p->cooling_data.subgrid_dens = -1.f;
 }
 
+/**
+ * @brief Perform additional init on the cooling properties of the
+ * (x-)particles that requires the density to be known.
+ *
+ * Nothing to do here.
+ *
+ * @param phys_const The physical constant in internal units.
+ * @param us The unit system.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param cosmo The current cosmological model.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE void cooling_post_init_part(
+    const struct phys_const *phys_const, const struct unit_system *us,
+    const struct hydro_props *hydro_props, const struct cosmology *cosmo,
+    const struct cooling_function_data *cooling, struct part *p,
+    struct xpart *xp) {}
+
 /**
  * @brief Compute the fraction of Hydrogen that is in HI based
  * on the pressure of the gas.
@@ -1583,7 +1606,8 @@ void cooling_restore_tables(struct cooling_function_data *cooling,
   read_cooling_header(cooling);
   read_cooling_tables(cooling);
 
-  cooling_update(cosmo, /*pfloor=*/NULL, cooling, /*space=*/NULL);
+  cooling_update(/*phys_const=*/NULL, cosmo, /*pfloor=*/NULL, cooling,
+                 /*space=*/NULL, /*time=*/0);
 }
 
 /**
diff --git a/src/cooling/PS2020/cooling.h b/src/cooling/PS2020/cooling.h
index 1bacfaa86a2d81e038c4cf71fa03ee05638e4fa2..3af8492898a1b790fa841def1e0ea8aa53838d41 100644
--- a/src/cooling/PS2020/cooling.h
+++ b/src/cooling/PS2020/cooling.h
@@ -37,9 +37,11 @@ struct pressure_floor_props;
 struct feedback_props;
 struct space;
 
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s);
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time);
 
 void cooling_cool_part(const struct phys_const *phys_const,
                        const struct unit_system *us,
@@ -65,6 +67,26 @@ void cooling_first_init_part(const struct phys_const *phys_const,
                              const struct cooling_function_data *cooling,
                              struct part *p, struct xpart *xp);
 
+/**
+ * @brief Sets the cooling properties of the (x-)particles to a valid start
+ * state. The function requires the density to be defined and thus must
+ * be called after its computation.
+ *
+ * @param phys_const The #phys_const.
+ * @param us The #unit_system.
+ * @param hydro_props The #hydro_props.
+ * @param cosmo The #cosmology.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+void cooling_post_init_part(const struct phys_const *phys_const,
+                            const struct unit_system *us,
+                            const struct hydro_props *hydro_props,
+                            const struct cosmology *cosmo,
+                            const struct cooling_function_data *cooling,
+                            struct part *p, struct xpart *xp);
+
 float cooling_get_temperature_from_gas(
     const struct phys_const *phys_const, const struct cosmology *cosmo,
     const struct cooling_function_data *cooling, const float rho_phys,
diff --git a/src/cooling/PS2020/cooling_io.h b/src/cooling/PS2020/cooling_io.h
index dc088df626936a11f879ba2b3cb554d94f70dd8a..242ad0391c95903c055394086bd5efd7b5f116f5 100644
--- a/src/cooling/PS2020/cooling_io.h
+++ b/src/cooling/PS2020/cooling_io.h
@@ -177,18 +177,18 @@ __attribute__((always_inline)) INLINE static int cooling_write_particles(
       "Temperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE, 0.f, parts, xparts,
       convert_part_T, "Temperatures of the gas particles");
 
-  list[1] = io_make_output_field_convert_part(
+  list[1] = io_make_physical_output_field_convert_part(
       "SubgridTemperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE, 0.f, parts,
-      xparts, convert_part_sub_T,
+      xparts, /*can convert to comoving=*/1, convert_part_sub_T,
       "The subgrid temperatures if the particles are within deltaT of the "
       "entropy floor the subgrid temperature is calculated assuming a "
       "pressure equilibrium on the entropy floor, if the particles are "
       "above deltaT of the entropy floor the subgrid temperature is "
       "identical to the SPH temperature.");
 
-  list[2] = io_make_output_field_convert_part(
-      "SubgridPhysicalDensities", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, parts,
-      xparts, convert_part_sub_rho,
+  list[2] = io_make_physical_output_field_convert_part(
+      "SubgridPhysicalDensities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f, parts,
+      xparts, /*can convert to comoving=*/1, convert_part_sub_rho,
       "The subgrid physical density if the particles are within deltaT of the "
       "entropy floor the subgrid density is calculated assuming a pressure "
       "equilibrium on the entropy floor, if the particles are above deltaT "
@@ -222,15 +222,15 @@ __attribute__((always_inline)) INLINE static int cooling_write_particles(
       "floor, by extrapolating to the equilibrium curve assuming constant "
       "pressure.");
 
-  list[6] = io_make_output_field_convert_part(
-      "ElectronNumberDensities", DOUBLE, 1, UNIT_CONV_NUMBER_DENSITY, 0.f,
-      parts, xparts, convert_part_e_density,
+  list[6] = io_make_physical_output_field_convert_part(
+      "ElectronNumberDensities", DOUBLE, 1, UNIT_CONV_NUMBER_DENSITY, -3.f,
+      parts, xparts, /*can convert to comoving=*/1, convert_part_e_density,
       "Electron number densities in the physical frame computed based on the "
       "cooling tables. This is 0 for star-forming particles.");
 
-  list[7] = io_make_output_field_convert_part(
+  list[7] = io_make_physical_output_field_convert_part(
       "ComptonYParameters", DOUBLE, 1, UNIT_CONV_AREA, 0.f, parts, xparts,
-      convert_part_y_compton,
+      /*can convert to comoving=*/0, convert_part_y_compton,
       "Compton y parameters in the physical frame computed based on the "
       "cooling tables. This is 0 for star-forming particles.");
 
diff --git a/src/cooling/QLA/cooling.c b/src/cooling/QLA/cooling.c
index dfe6e04edf357f76e72a573ffcd89b73c983a289..79167d24f547a1b0885897395bce712a6a367a8f 100644
--- a/src/cooling/QLA/cooling.c
+++ b/src/cooling/QLA/cooling.c
@@ -71,10 +71,13 @@ static const double bracket_factor = 1.5;
  * @param pressure_floor Properties of the pressure floor.
  * @param cooling The #cooling_function_data used in the run.
  * @param s The space data, including a pointer to array of particles
+ * @param time The current system time
  */
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s) {
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time) {
 
   /* Extra energy for reionization? */
   if (!cooling->H_reion_done) {
@@ -762,6 +765,26 @@ __attribute__((always_inline)) INLINE void cooling_first_init_part(
     const struct cooling_function_data *cooling, struct part *p,
     struct xpart *xp) {}
 
+/**
+ * @brief Perform additional init on the cooling properties of the
+ * (x-)particles that requires the density to be known.
+ *
+ * Nothing to do here.
+ *
+ * @param phys_const The physical constant in internal units.
+ * @param us The unit system.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param cosmo The current cosmological model.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE void cooling_post_init_part(
+    const struct phys_const *phys_const, const struct unit_system *us,
+    const struct hydro_props *hydro_props, const struct cosmology *cosmo,
+    const struct cooling_function_data *cooling, struct part *p,
+    struct xpart *xp) {}
+
 /**
  * @brief Compute the fraction of Hydrogen that is in HI based
  * on the pressure of the gas.
@@ -1137,7 +1160,8 @@ void cooling_restore_tables(struct cooling_function_data *cooling,
   read_cooling_header(cooling);
   read_cooling_tables(cooling);
 
-  cooling_update(cosmo, /*pfloor=*/NULL, cooling, /*space=*/NULL);
+  cooling_update(/*phys_const=*/NULL, cosmo, /*pfloor=*/NULL, cooling,
+                 /*space=*/NULL, /*time=*/0);
 }
 
 /**
diff --git a/src/cooling/QLA/cooling.h b/src/cooling/QLA/cooling.h
index dca77090a4bac04bcd6316a9f3b8ca8c0e74af05..8ef8b792404529acf46f352ae05cbaa61e2ba366 100644
--- a/src/cooling/QLA/cooling.h
+++ b/src/cooling/QLA/cooling.h
@@ -38,9 +38,11 @@ struct pressure_floor_props;
 struct phys_const;
 struct space;
 
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s);
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time);
 
 void cooling_cool_part(const struct phys_const *phys_const,
                        const struct unit_system *us,
@@ -66,6 +68,26 @@ void cooling_first_init_part(const struct phys_const *phys_const,
                              const struct cooling_function_data *cooling,
                              struct part *p, struct xpart *xp);
 
+/**
+ * @brief Sets the cooling properties of the (x-)particles to a valid start
+ * state. The function requires the density to be defined and thus must
+ * be called after its computation.
+ *
+ * @param phys_const The #phys_const.
+ * @param us The #unit_system.
+ * @param hydro_props The #hydro_props.
+ * @param cosmo The #cosmology.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+void cooling_post_init_part(const struct phys_const *phys_const,
+                            const struct unit_system *us,
+                            const struct hydro_props *hydro_props,
+                            const struct cosmology *cosmo,
+                            const struct cooling_function_data *cooling,
+                            struct part *p, struct xpart *xp);
+
 float cooling_get_temperature_from_gas(
     const struct phys_const *phys_const, const struct cosmology *cosmo,
     const struct cooling_function_data *cooling, const float rho_phys,
diff --git a/src/cooling/QLA_EAGLE/cooling.c b/src/cooling/QLA_EAGLE/cooling.c
index 8e7bb884c912ad24466f8241392d236d2c6740f5..811db57538d08d9ee6ca4c085ad57af0b46dbd65 100644
--- a/src/cooling/QLA_EAGLE/cooling.c
+++ b/src/cooling/QLA_EAGLE/cooling.c
@@ -126,10 +126,13 @@ __attribute__((always_inline)) INLINE void get_redshift_index(
  * @param pressure_floor The properties of the pressure floor.
  * @param cooling The #cooling_function_data used in the run.
  * @param s The space data, including a pointer to array of particles
+ * @param time The current system time
  */
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s) {
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time) {
 
   /* Current redshift */
   const float redshift = cosmo->z;
@@ -568,6 +571,28 @@ __attribute__((always_inline)) INLINE void cooling_first_init_part(
     const struct cooling_function_data *restrict cooling,
     const struct part *restrict p, struct xpart *restrict xp) {}
 
+/**
+ * @brief Perform additional init on the cooling properties of the
+ * (x-)particles that requires the density to be known.
+ *
+ * Nothing to do here.
+ *
+ * @param phys_const The physical constant in internal units.
+ * @param us The unit system.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param cosmo The current cosmological model.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE void cooling_post_init_part(
+    const struct phys_const *restrict phys_const,
+    const struct unit_system *restrict us,
+    const struct hydro_props *hydro_props,
+    const struct cosmology *restrict cosmo,
+    const struct cooling_function_data *restrict cooling,
+    const struct part *restrict p, struct xpart *restrict xp) {}
+
 /**
  * @brief Compute the temperature based on gas properties.
  *
@@ -1068,7 +1093,8 @@ void cooling_restore_tables(struct cooling_function_data *cooling,
   /* Force a re-read of the cooling tables */
   cooling->z_index = -10;
   cooling->previous_z_index = qla_eagle_cooling_N_redshifts - 2;
-  cooling_update(cosmo, /*pfloor=*/NULL, cooling, /*space=*/NULL);
+  cooling_update(/*phys_const=*/NULL, cosmo, /*pfloor=*/NULL, cooling,
+                 /*space=*/NULL, /*time=*/0);
 }
 
 /**
diff --git a/src/cooling/QLA_EAGLE/cooling.h b/src/cooling/QLA_EAGLE/cooling.h
index 0abafc6323be12715c1b5f2de453c99b6dccf58a..9ddd756a69174694454b0ab5102c275791120912 100644
--- a/src/cooling/QLA_EAGLE/cooling.h
+++ b/src/cooling/QLA_EAGLE/cooling.h
@@ -37,9 +37,11 @@ struct pressure_floor_props;
 struct space;
 struct phys_const;
 
-void cooling_update(const struct cosmology *cosmo,
+void cooling_update(const struct phys_const *phys_const,
+                    const struct cosmology *cosmo,
                     const struct pressure_floor_props *pressure_floor,
-                    struct cooling_function_data *cooling, struct space *s);
+                    struct cooling_function_data *cooling, struct space *s,
+                    const double time);
 
 void cooling_cool_part(const struct phys_const *phys_const,
                        const struct unit_system *us,
@@ -88,6 +90,27 @@ void cooling_first_init_part(
     const struct cooling_function_data *restrict cooling,
     const struct part *restrict p, struct xpart *restrict xp);
 
+/**
+ * @brief Sets the cooling properties of the (x-)particles to a valid start
+ * state. The function requires the density to be defined and thus must
+ * be called after its computation.
+ *
+ * @param phys_const The #phys_const.
+ * @param us The #unit_system.
+ * @param hydro_props The #hydro_props.
+ * @param cosmo The #cosmology.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+void cooling_post_init_part(
+    const struct phys_const *restrict phys_const,
+    const struct unit_system *restrict us,
+    const struct hydro_props *hydro_props,
+    const struct cosmology *restrict cosmo,
+    const struct cooling_function_data *restrict cooling,
+    const struct part *restrict p, struct xpart *restrict xp);
+
 float cooling_get_temperature_from_gas(
     const struct phys_const *phys_const, const struct cosmology *cosmo,
     const struct cooling_function_data *cooling, const float rho_phys,
diff --git a/src/cooling/const_du/cooling.h b/src/cooling/const_du/cooling.h
index 22c90bc679437cee9e7057263b930bdc4b5a6286..0ab86457c639fcdfd94361fc2984699d94a3bcd7 100644
--- a/src/cooling/const_du/cooling.h
+++ b/src/cooling/const_du/cooling.h
@@ -58,11 +58,12 @@
  * @param pressure_floor The properties of the pressure floor.
  * @param cooling The #cooling_function_data used in the run.
  * @param s The #space containing all the particles.
+ * @param time The current system time
  */
 INLINE static void cooling_update(
-    const struct cosmology* cosmo,
+    const struct phys_const* phys_const, const struct cosmology* cosmo,
     const struct pressure_floor_props* pressure_floor,
-    struct cooling_function_data* cooling, struct space* s) {
+    struct cooling_function_data* cooling, struct space* s, const double time) {
   // Add content if required.
 }
 
@@ -226,6 +227,28 @@ __attribute__((always_inline)) INLINE static void cooling_first_init_part(
   xp->cooling_data.radiated_energy = 0.f;
 }
 
+/**
+ * @brief Perform additional init on the cooling properties of the
+ * (x-)particles that requires the density to be known.
+ *
+ * Nothing to do here.
+ *
+ * @param phys_const The physical constant in internal units.
+ * @param us The unit system.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param cosmo The current cosmological model.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static void cooling_post_init_part(
+    const struct phys_const* restrict phys_const,
+    const struct unit_system* restrict us,
+    const struct hydro_props* hydro_props,
+    const struct cosmology* restrict cosmo,
+    const struct cooling_function_data* cooling, const struct part* restrict p,
+    struct xpart* restrict xp) {}
+
 /**
  * @brief Compute the temperature of a #part based on the cooling function.
  *
diff --git a/src/cooling/const_lambda/cooling.h b/src/cooling/const_lambda/cooling.h
index c204996e40d69f8837d7fbeddb4dbc125e98718f..bc9d3e44fede03864b90184ee9aa577da3cfd45f 100644
--- a/src/cooling/const_lambda/cooling.h
+++ b/src/cooling/const_lambda/cooling.h
@@ -54,11 +54,12 @@
  * @param pressure_floor The properties of the pressure floor.
  * @param cooling The #cooling_function_data used in the run.
  * @param s The #space containing all the particles.
+ * @param time The current system time
  */
 INLINE static void cooling_update(
-    const struct cosmology* cosmo,
+    const struct phys_const* phys_const, const struct cosmology* cosmo,
     const struct pressure_floor_props* pressure_floor,
-    struct cooling_function_data* cooling, struct space* s) {
+    struct cooling_function_data* cooling, struct space* s, const double time) {
   // Add content if required.
 }
 
@@ -314,6 +315,28 @@ __attribute__((always_inline)) INLINE static void cooling_first_init_part(
   xp->cooling_data.radiated_energy = 0.f;
 }
 
+/**
+ * @brief Perform additional init on the cooling properties of the
+ * (x-)particles that requires the density to be known.
+ *
+ * Nothing to do here.
+ *
+ * @param phys_const The physical constant in internal units.
+ * @param us The unit system.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param cosmo The current cosmological model.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static void cooling_post_init_part(
+    const struct phys_const* restrict phys_const,
+    const struct unit_system* restrict us,
+    const struct hydro_props* hydro_props,
+    const struct cosmology* restrict cosmo,
+    const struct cooling_function_data* cooling, const struct part* restrict p,
+    struct xpart* restrict xp) {}
+
 /**
  * @brief Compute the temperature of a #part based on the cooling function.
  *
diff --git a/src/cooling/const_lambda/cooling_io.h b/src/cooling/const_lambda/cooling_io.h
index 9494731d047b4f1b7b03d03e85d128c286351ae9..120528f3b3087307b10400ec8b8afc4572c1bafb 100644
--- a/src/cooling/const_lambda/cooling_io.h
+++ b/src/cooling/const_lambda/cooling_io.h
@@ -80,9 +80,9 @@ __attribute__((always_inline)) INLINE static int cooling_write_particles(
       "Temperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE, 0.f, parts, xparts,
       convert_part_T, "Temperatures of the gas particles");
 
-  list[1] = io_make_output_field(
+  list[1] = io_make_physical_output_field(
       "RadiatedEnergies", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, xparts,
-      cooling_data.radiated_energy,
+      cooling_data.radiated_energy, /*can convert to comoving=*/0,
       "Thermal energies radiated by the cooling mechanism");
 
   return 2;
diff --git a/src/cooling/grackle/cooling.c b/src/cooling/grackle/cooling.c
index 2c4e093c9b8f986c1a7abec36aa68d17eb8cadc6..dbf5052b37e247b178f4b2ef6778e1a8cfe524c8 100644
--- a/src/cooling/grackle/cooling.c
+++ b/src/cooling/grackle/cooling.c
@@ -64,18 +64,38 @@ gr_float cooling_time(const struct phys_const* phys_const,
                       const struct cooling_function_data* cooling,
                       const struct part* p, struct xpart* xp);
 
+double cooling_get_physical_density(
+    const struct part* p, const struct cosmology* cosmo,
+    const struct cooling_function_data* cooling);
+
+/**
+ * @brief Record the time when cooling was switched off for a particle.
+ *
+ * @param p #part data.
+ * @param xp Pointer to the #xpart data.
+ * @param time The time when the cooling was switched off.
+ */
+INLINE void cooling_set_part_time_cooling_off(struct part* p, struct xpart* xp,
+                                              const double time) {
+  xp->cooling_data.time_last_event = time;
+}
+
 /**
  * @brief Common operations performed on the cooling function at a
  * given time-step or redshift.
  *
+ * @param phys_const The #phys_const.
  * @param cosmo The current cosmological model.
  * @param pressure_floor Properties of the pressure floor.
  * @param cooling The #cooling_function_data used in the run.
  * @param s The #space containing all the particles.
+ * @param time The current system time
  */
-void cooling_update(const struct cosmology* cosmo,
+void cooling_update(const struct phys_const* phys_const,
+                    const struct cosmology* cosmo,
                     const struct pressure_floor_props* pressure_floor,
-                    struct cooling_function_data* cooling, struct space* s) {
+                    struct cooling_function_data* cooling, struct space* s,
+                    const double time) {
   /* set current time */
   if (cooling->redshift == -1)
     cooling->units.a_value = cosmo->a;
@@ -175,45 +195,7 @@ void cooling_compute_equilibrium(const struct phys_const* phys_const,
                                  const struct cooling_function_data* cooling,
                                  const struct part* p, struct xpart* xp) {
 
-  /* TODO: this can fail spectacularly and needs to be replaced. */
-
-  /* get temporary data */
-  struct part p_tmp = *p;
-  struct cooling_function_data cooling_tmp = *cooling;
-  cooling_tmp.chemistry.with_radiative_cooling = 0;
-  /* need density for computation, therefore quick estimate */
-  p_tmp.rho = 0.2387 * p_tmp.mass / pow(p_tmp.h, 3);
-
-  /* compute time step */
-  const double alpha = 0.01;
-  double dt = fabs(cooling_time(phys_const, us, hydro_properties, cosmo,
-                                &cooling_tmp, &p_tmp, xp));
-  cooling_new_energy(phys_const, us, cosmo, hydro_properties, &cooling_tmp,
-                     &p_tmp, xp, dt, dt);
-  dt = alpha * fabs(cooling_time(phys_const, us, hydro_properties, cosmo,
-                                 &cooling_tmp, &p_tmp, xp));
-
-  /* init simple variables */
-  int step = 0;
-  const int max_step = cooling_tmp.max_step;
-  const float conv_limit = cooling_tmp.convergence_limit;
-  struct xpart old;
-
-  do {
-    /* update variables */
-    step += 1;
-    old = *xp;
-
-    /* update chemistry */
-    cooling_new_energy(phys_const, us, cosmo, hydro_properties, &cooling_tmp,
-                       &p_tmp, xp, dt, dt);
-  } while (step < max_step && !cooling_converged(xp, &old, conv_limit));
-
-  if (step == max_step)
-    error(
-        "A particle element fraction failed to converge."
-        "You can change 'GrackleCooling:MaxSteps' or "
-        "'GrackleCooling:ConvergenceLimit' to avoid this problem");
+  return;
 }
 
 /**
@@ -241,34 +223,195 @@ void cooling_first_init_part(const struct phys_const* phys_const,
 #if COOLING_GRACKLE_MODE >= 1
   gr_float zero = 1.e-20;
 
+  /* NOTE: if the ratio with respect to hydrogen is given, use it. Instead,
+   * we assume neutral gas.
+   * A better determination of the abundances can be done in
+   * cooling_post_init_part */
+
+  /* Compute nH (formally divided by the gas density and assuming the proton
+   * mass to be one) */
+  double nH = 1.0;
+  if (grackle_data != NULL) {
+    nH = grackle_data->HydrogenFractionByMass;
+  }
+
+  /* Compute nHe (formally divided by the gas density and assuming the proton
+   * mass to be one) */
+  double nHe = 0.0;
+  if (grackle_data != NULL) {
+    nHe = (1.f - grackle_data->HydrogenFractionByMass) / 4.f;
+  }
+
+  /* Electron density */
+  double ne = zero;
+
+  /* Sum of mass fraction (to test its consistency) */
+  double Xtot = 0.f;
+
   /* primordial chemistry >= 1 */
-  xp->cooling_data.HI_frac = zero;
-  xp->cooling_data.HII_frac = grackle_data->HydrogenFractionByMass;
-  xp->cooling_data.HeI_frac = zero;
-  xp->cooling_data.HeII_frac = zero;
-  xp->cooling_data.HeIII_frac = 1. - grackle_data->HydrogenFractionByMass;
-  xp->cooling_data.e_frac = xp->cooling_data.HII_frac +
-                            0.25 * xp->cooling_data.HeII_frac +
-                            0.5 * xp->cooling_data.HeIII_frac;
+
+  /* Hydrogen I */
+  if (cooling->initial_nHII_to_nH_ratio >= 0.f)
+    xp->cooling_data.HI_frac = nH * (1.f - cooling->initial_nHII_to_nH_ratio);
+  else
+    xp->cooling_data.HI_frac = nH;
+
+  Xtot += xp->cooling_data.HI_frac;
+
+  /* Hydrogen II */
+  if (cooling->initial_nHII_to_nH_ratio >= 0.f) {
+    double nHII = nH * cooling->initial_nHII_to_nH_ratio;
+    xp->cooling_data.HII_frac = nHII;
+    ne += nHII;
+  } else
+    xp->cooling_data.HII_frac = zero;
+
+  Xtot += xp->cooling_data.HII_frac;
+
+  /* Helium I */
+  if (cooling->initial_nHeI_to_nH_ratio >= 0.f) {
+    double nHeI = nH * cooling->initial_nHeI_to_nH_ratio;
+    xp->cooling_data.HeI_frac = nHeI * 4.f;
+  } else
+    xp->cooling_data.HeI_frac = nHe * 4.f;
+
+  Xtot += xp->cooling_data.HeI_frac;
+
+  /* Helium II */
+  if (cooling->initial_nHeII_to_nH_ratio >= 0.f) {
+    double nHeII = nH * cooling->initial_nHeII_to_nH_ratio;
+    xp->cooling_data.HeII_frac = nHeII * 4.f;
+    ne += nHeII;
+  } else
+    xp->cooling_data.HeII_frac = zero;
+
+  Xtot += xp->cooling_data.HeII_frac;
+
+  /* Helium III */
+  if (cooling->initial_nHeIII_to_nH_ratio >= 0.f) {
+    double nHeIII = nH * cooling->initial_nHeIII_to_nH_ratio;
+    xp->cooling_data.HeIII_frac = nHeIII * 4.f;
+    ne += 2.f * nHeIII;
+  } else
+    xp->cooling_data.HeIII_frac = zero;
+
+  Xtot += xp->cooling_data.HeIII_frac;
+
+  /* electron mass fraction (multiplied by the proton mass (Grackle convention)
+   */
+  xp->cooling_data.e_frac = ne;
+
 #endif  // MODE >= 1
 
 #if COOLING_GRACKLE_MODE >= 2
   /* primordial chemistry >= 2 */
-  xp->cooling_data.HM_frac = zero;
-  xp->cooling_data.H2I_frac = zero;
-  xp->cooling_data.H2II_frac = zero;
+
+  /* Hydrogen- */
+  if (cooling->initial_nHM_to_nH_ratio >= 0.f) {
+    double nHM = nH * cooling->initial_nHM_to_nH_ratio;
+    xp->cooling_data.HM_frac = nHM;
+    ne -= nHM;
+  } else
+    xp->cooling_data.HM_frac = zero;
+
+  Xtot += xp->cooling_data.HM_frac;
+
+  /* H2I */
+  if (cooling->initial_nH2I_to_nH_ratio >= 0.f) {
+    double nH2I = nH * cooling->initial_nH2I_to_nH_ratio;
+    xp->cooling_data.H2I_frac = nH2I * 2.f;
+  } else
+    xp->cooling_data.H2I_frac = zero;
+
+  Xtot += xp->cooling_data.H2I_frac;
+
+  /* H2II */
+  if (cooling->initial_nH2II_to_nH_ratio >= 0.f) {
+    double nH2II = nH * cooling->initial_nH2II_to_nH_ratio;
+    xp->cooling_data.H2II_frac = nH2II * 2.f;
+    ne += nH2II;
+  } else
+    xp->cooling_data.H2II_frac = zero;
+
+  Xtot += xp->cooling_data.H2II_frac;
+
+  /* electron mass fraction (multiplied by the proton mass (Grackle convention)
+   */
+  xp->cooling_data.e_frac = ne;
+
 #endif  // MODE >= 2
 
 #if COOLING_GRACKLE_MODE >= 3
   /* primordial chemistry >= 3 */
-  xp->cooling_data.DI_frac = grackle_data->DeuteriumToHydrogenRatio *
-                             grackle_data->HydrogenFractionByMass;
-  xp->cooling_data.DII_frac = zero;
-  xp->cooling_data.HDI_frac = zero;
+
+  /* Deuterium I */
+  if (cooling->initial_nDI_to_nH_ratio >= 0.f) {
+    double nDI = nH * cooling->initial_nDI_to_nH_ratio;
+    xp->cooling_data.DI_frac = nDI * 2.f;
+  } else {
+    if (grackle_data != NULL) {
+      xp->cooling_data.DI_frac = grackle_data->DeuteriumToHydrogenRatio *
+                                 grackle_data->HydrogenFractionByMass;
+    }
+  }
+
+  Xtot += xp->cooling_data.DI_frac;
+
+  /* Deuterium II */
+  if (cooling->initial_nDII_to_nH_ratio >= 0.f) {
+    double nDII = nH * cooling->initial_nDII_to_nH_ratio;
+    xp->cooling_data.DII_frac = nDII * 2.f;
+    ne += nDII;
+  } else
+    xp->cooling_data.DII_frac = zero;
+
+  Xtot += xp->cooling_data.DII_frac;
+
+  /* HD I */
+  if (cooling->initial_nHDI_to_nH_ratio >= 0.f) {
+    double nHDI = nH * cooling->initial_nHDI_to_nH_ratio;
+    xp->cooling_data.HDI_frac = nHDI * 3.f;
+  } else
+    xp->cooling_data.HDI_frac = zero;
+
+  Xtot += xp->cooling_data.HDI_frac;
+
+  /* electron mass fraction (multiplied by the proton mass (Grackle convention)
+   */
+  xp->cooling_data.e_frac = ne;
+
+  if (fabs(Xtot - 1.0) > 1e-3)
+    error("Got total mass fraction of gas = %.6g", Xtot);
+
 #endif  // MODE >= 3
+}
+
+/**
+ * @brief Sets the cooling properties of the (x-)particles to a valid start
+ * state. The function requires the density to be defined and thus must
+ * be called after its computation.
+ *
+ * @param phys_const The #phys_const.
+ * @param us The #unit_system.
+ * @param hydro_props The #hydro_props.
+ * @param cosmo The #cosmology.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+void cooling_post_init_part(const struct phys_const* phys_const,
+                            const struct unit_system* us,
+                            const struct hydro_props* hydro_props,
+                            const struct cosmology* cosmo,
+                            const struct cooling_function_data* cooling,
+                            const struct part* p, struct xpart* xp) {
+
+  // const float rho = hydro_get_physical_density(p, cosmo);
+  // const float energy = hydro_get_physical_internal_energy(p, xp, cosmo);
+  // message("rho = %g energy = %g",rho,energy);
 
 #if COOLING_GRACKLE_MODE > 0
-  /* TODO: this can fail spectacularly and needs to be replaced. */
+  /* The function below currently does nothing. Will have to be updated. */
   cooling_compute_equilibrium(phys_const, us, hydro_props, cosmo, cooling, p,
                               xp);
 #endif
@@ -291,22 +434,58 @@ float cooling_get_radiated_energy(const struct xpart* xp) {
  */
 void cooling_print_backend(const struct cooling_function_data* cooling) {
 
+  if (engine_rank != 0) {
+    return;
+  }
+
   message("Cooling function is 'Grackle'.");
-  message("Using Grackle = %i", cooling->chemistry.use_grackle);
-  message("Chemical network = %i", cooling->chemistry.primordial_chemistry);
+  message("Using Grackle = %i", cooling->chemistry_data.use_grackle);
+  message("Chemical network = %i",
+          cooling->chemistry_data.primordial_chemistry);
   message("CloudyTable = %s", cooling->cloudy_table);
   message("Redshift = %g", cooling->redshift);
   message("UV background = %d", cooling->with_uv_background);
-  message("Metal cooling = %i", cooling->chemistry.metal_cooling);
+  message("Metal cooling = %i", cooling->chemistry_data.metal_cooling);
   message("Self Shielding = %i", cooling->self_shielding_method);
+  message("Maximal density = %e", cooling->cooling_density_max);
   if (cooling->self_shielding_method == -1) {
     message("Self Shelding density = %g", cooling->self_shielding_threshold);
   }
+
   message("Thermal time = %g", cooling->thermal_time);
-  message("Specific Heating Rates = %i",
-          cooling->provide_specific_heating_rates);
-  message("Volumetric Heating Rates = %i",
-          cooling->provide_volumetric_heating_rates);
+  message("Specific Heating Rates = %g", cooling->specific_heating_rates);
+  message("Volumetric Heating Rates = %g", cooling->volumetric_heating_rates);
+
+  message("grackle_chemistry_data.RT_heating_rate = %g",
+          cooling->RT_heating_rate);
+  message("grackle_chemistry_data.RT_HI_ionization_rate = %g",
+          cooling->RT_HI_ionization_rate);
+  message("grackle_chemistry_data.RT_HeI_ionization_rate = %g",
+          cooling->RT_HeI_ionization_rate);
+  message("grackle_chemistry_data.RT_HeII_ionization_rate = %g",
+          cooling->RT_HeII_ionization_rate);
+  message("grackle_chemistry_data.RT_H2_dissociation_rate = %g",
+          cooling->RT_H2_dissociation_rate);
+  message("cooling.initial_nHII_to_nH_ratio= %g",
+          cooling->initial_nHII_to_nH_ratio);
+  message("cooling.initial_nHeI_to_nH_ratio= %g",
+          cooling->initial_nHeI_to_nH_ratio);
+  message("cooling.initial_nHeII_to_nH_ratio= %g",
+          cooling->initial_nHeII_to_nH_ratio);
+  message("cooling.initial_nHeIII_to_nH_ratio= %g",
+          cooling->initial_nHeIII_to_nH_ratio);
+  message("cooling.initial_nDI_to_nH_ratio= %g",
+          cooling->initial_nDI_to_nH_ratio);
+  message("cooling.initial_nDII_to_nH_ratio= %g",
+          cooling->initial_nDII_to_nH_ratio);
+  message("cooling.initial_nHM_to_nH_ratio= %g",
+          cooling->initial_nHM_to_nH_ratio);
+  message("cooling.initial_nH2I_to_nH_ratio= %g",
+          cooling->initial_nH2I_to_nH_ratio);
+  message("cooling.initial_nH2II_to_nH_ratio= %g",
+          cooling->initial_nH2II_to_nH_ratio);
+  message("cooling.initial_nHDI_to_nH_ratio= %g",
+          cooling->initial_nHDI_to_nH_ratio);
   message("Units:");
   message("\tComoving = %i", cooling->units.comoving_coordinates);
   message("\tLength = %g", cooling->units.length_units);
@@ -314,6 +493,51 @@ void cooling_print_backend(const struct cooling_function_data* cooling) {
   message("\tTime = %g", cooling->units.time_units);
   message("\tScale Factor = %g (units: %g)", cooling->units.a_value,
           cooling->units.a_units);
+
+  message("Grackle parameters:");
+  message("grackle_chemistry_data.use_grackle = %d",
+          cooling->chemistry_data.use_grackle);
+  message("grackle_chemistry_data.with_radiative_cooling %d",
+          cooling->chemistry_data.with_radiative_cooling);
+  message("grackle_chemistry_data.primordial_chemistry = %d",
+          cooling->chemistry_data.primordial_chemistry);
+  message("grackle_chemistry_data.three_body_rate = %d",
+          cooling->chemistry_data.three_body_rate);
+  message("grackle_chemistry_data.cmb_temperature_floor = %d",
+          cooling->chemistry_data.cmb_temperature_floor);
+  message("grackle_chemistry_data.cie_cooling = %d",
+          cooling->chemistry_data.cie_cooling);
+  message("grackle_chemistry_data.dust_chemistry = %d",
+          cooling->chemistry_data.dust_chemistry);
+  message("grackle_chemistry_data.metal_cooling = %d",
+          cooling->chemistry_data.metal_cooling);
+  message("grackle_chemistry_data.UVbackground = %d",
+          cooling->chemistry_data.UVbackground);
+  message("grackle_chemistry_data.CaseBRecombination = %d",
+          cooling->chemistry_data.CaseBRecombination);
+  message("grackle_chemistry_data.grackle_data_file = %s",
+          cooling->chemistry_data.grackle_data_file);
+  message("grackle_chemistry_data.use_radiative_transfer = %d",
+          cooling->chemistry_data.use_radiative_transfer);
+  message("grackle_chemistry_data.use_volumetric_heating_rate = %d",
+          cooling->chemistry_data.use_volumetric_heating_rate);
+  message("grackle_chemistry_data.use_specific_heating_rate = %d",
+          cooling->chemistry_data.use_specific_heating_rate);
+  message("grackle_chemistry_data.self_shielding_method = %d",
+          cooling->chemistry_data.self_shielding_method);
+  message("grackle_chemistry_data.HydrogenFractionByMass = %.3g",
+          cooling->chemistry_data.HydrogenFractionByMass);
+  message("grackle_chemistry_data.Gamma = %.6g", cooling->chemistry_data.Gamma);
+  message("grackle_chemistry_data.cie_cooling = %d",
+          cooling->chemistry_data.cie_cooling);
+  message("grackle_chemistry_data.three_body_rate = %d",
+          cooling->chemistry_data.three_body_rate);
+  message("grackle_chemistry_data.h2_on_dust = %d",
+          cooling->chemistry_data.h2_on_dust);
+  message("grackle_chemistry_data.use_dust_density_field = %d",
+          cooling->chemistry_data.use_dust_density_field);
+  message("grackle_chemistry_data.local_dust_to_gas_ratio = %.3g",
+          cooling->chemistry_data.local_dust_to_gas_ratio);
 }
 
 /**
@@ -529,19 +753,84 @@ void cooling_copy_from_grackle3(grackle_field_data* data, const struct part* p,
  */
 void cooling_copy_to_grackle(grackle_field_data* data, const struct part* p,
                              struct xpart* xp, gr_float rho,
-                             gr_float species_densities[12]) {
+                             gr_float species_densities[12],
+                             const struct cooling_function_data* cooling,
+                             const struct phys_const* phys_const) {
+
+  const float time_units = cooling->units.time_units;
 
   cooling_copy_to_grackle1(data, p, xp, rho, species_densities);
   cooling_copy_to_grackle2(data, p, xp, rho, species_densities);
   cooling_copy_to_grackle3(data, p, xp, rho, species_densities);
 
-  data->volumetric_heating_rate = NULL;
-  data->specific_heating_rate = NULL;
-  data->RT_heating_rate = NULL;
-  data->RT_HI_ionization_rate = NULL;
-  data->RT_HeI_ionization_rate = NULL;
-  data->RT_HeII_ionization_rate = NULL;
-  data->RT_H2_dissociation_rate = NULL;
+  if (cooling->chemistry_data.use_volumetric_heating_rate) {
+    gr_float* volumetric_heating_rate = (gr_float*)malloc(sizeof(gr_float));
+    *volumetric_heating_rate = cooling->volumetric_heating_rates;
+    data->volumetric_heating_rate = volumetric_heating_rate;
+  }
+
+  if (cooling->chemistry_data.use_specific_heating_rate) {
+    gr_float* specific_heating_rate = (gr_float*)malloc(sizeof(gr_float));
+    *specific_heating_rate = cooling->specific_heating_rates;
+    data->specific_heating_rate = specific_heating_rate;
+  }
+
+  if (cooling->chemistry_data.use_radiative_transfer) {
+
+    /* heating rate */
+    gr_float* RT_heating_rate = (gr_float*)malloc(sizeof(gr_float));
+    *RT_heating_rate = cooling->RT_heating_rate;
+    /* Note to self:
+     * If cooling->RT_heating_rate is computed properly, i.e. using
+     * the HI density, and then being HI density dependent, we need
+     * to divide it as follow. If it is assumed to be already normed
+     * as it is so when providing it via some parameters, we keep it
+     * unchanged.
+     */
+    /* Grackle wants heating rate in units of / nHI_cgs */
+    // const double nHI_cgs = species_densities[0]
+    //                      / phys_const->const_proton_mass
+    //                      / pow(length_units,3);
+    //*RT_heating_rate /= nHI_cgs;
+    data->RT_heating_rate = RT_heating_rate;
+
+    /* HI ionization rate */
+    gr_float* RT_HI_ionization_rate = (gr_float*)malloc(sizeof(gr_float));
+    *RT_HI_ionization_rate = cooling->RT_HI_ionization_rate;
+    /* Grackle wants it in 1/internal_time_units */
+    *RT_HI_ionization_rate /= (1. / time_units);
+    data->RT_HI_ionization_rate = RT_HI_ionization_rate;
+
+    /* HeI ionization rate */
+    gr_float* RT_HeI_ionization_rate = (gr_float*)malloc(sizeof(gr_float));
+    *RT_HeI_ionization_rate = cooling->RT_HeI_ionization_rate;
+    /* Grackle wants it in 1/internal_time_units */
+    *RT_HeI_ionization_rate /= (1. / time_units);
+    data->RT_HeI_ionization_rate = RT_HeI_ionization_rate;
+
+    /* HeII ionization rate */
+    gr_float* RT_HeII_ionization_rate = (gr_float*)malloc(sizeof(gr_float));
+    *RT_HeII_ionization_rate = cooling->RT_HeII_ionization_rate;
+    /* Grackle wants it in 1/internal_time_units */
+    *RT_HeII_ionization_rate /= (1. / time_units);
+    data->RT_HeII_ionization_rate = RT_HeII_ionization_rate;
+
+    /* H2 ionization rate */
+    gr_float* RT_H2_dissociation_rate = (gr_float*)malloc(sizeof(gr_float));
+    *RT_H2_dissociation_rate = cooling->RT_H2_dissociation_rate;
+    /* Grackle wants it in 1/internal_time_units */
+    *RT_H2_dissociation_rate /= (1. / time_units);
+    data->RT_H2_dissociation_rate = RT_H2_dissociation_rate;
+
+  } else {
+    data->volumetric_heating_rate = NULL;
+    data->specific_heating_rate = NULL;
+    data->RT_heating_rate = NULL;
+    data->RT_HI_ionization_rate = NULL;
+    data->RT_HeI_ionization_rate = NULL;
+    data->RT_HeII_ionization_rate = NULL;
+    data->RT_H2_dissociation_rate = NULL;
+  }
 
   gr_float* metal_density = (gr_float*)malloc(sizeof(gr_float));
   *metal_density = chemistry_get_total_metal_mass_fraction_for_cooling(p) * rho;
@@ -560,11 +849,25 @@ void cooling_copy_to_grackle(grackle_field_data* data, const struct part* p,
  * @param rho The particle density.
  */
 void cooling_copy_from_grackle(grackle_field_data* data, const struct part* p,
-                               struct xpart* xp, gr_float rho) {
+                               struct xpart* xp, gr_float rho,
+                               const struct cooling_function_data* cooling) {
   cooling_copy_from_grackle1(data, p, xp, rho);
   cooling_copy_from_grackle2(data, p, xp, rho);
   cooling_copy_from_grackle3(data, p, xp, rho);
 
+  if (cooling->chemistry_data.use_volumetric_heating_rate)
+    free(data->volumetric_heating_rate);
+  if (cooling->chemistry_data.use_specific_heating_rate)
+    free(data->specific_heating_rate);
+
+  if (cooling->chemistry_data.use_radiative_transfer) {
+    free(data->RT_heating_rate);
+    free(data->RT_HI_ionization_rate);
+    free(data->RT_HeI_ionization_rate);
+    free(data->RT_HeII_ionization_rate);
+    free(data->RT_H2_dissociation_rate);
+  }
+
   free(data->metal_density);
 }
 
@@ -622,7 +925,8 @@ gr_float cooling_new_energy(const struct phys_const* phys_const,
 
   /* set current time */
   code_units units = cooling->units;
-  chemistry_data chemistry_grackle = cooling->chemistry;
+  chemistry_data chemistry_grackle = cooling->chemistry_data;
+  chemistry_data_storage rates_grackle = cooling->chemistry_rates;
 
   /* initialize data */
   grackle_field_data data;
@@ -640,7 +944,7 @@ gr_float cooling_new_energy(const struct phys_const* phys_const,
   data.grid_end = grid_end;
 
   /* general particle data */
-  gr_float density = hydro_get_physical_density(p, cosmo);
+  gr_float density = cooling_get_physical_density(p, cosmo, cooling);
   gr_float energy = hydro_get_physical_internal_energy(p, xp, cosmo) +
                     dt_therm * hydro_get_physical_internal_energy_dt(p, cosmo);
   energy = max(energy, hydro_props->minimal_internal_energy);
@@ -660,19 +964,20 @@ gr_float cooling_new_energy(const struct phys_const* phys_const,
   data.z_velocity = NULL;
 
   /* copy to grackle structure */
-  cooling_copy_to_grackle(&data, p, xp, density, species_densities);
+  cooling_copy_to_grackle(&data, p, xp, density, species_densities, cooling,
+                          phys_const);
 
   /* Apply the self shielding if requested */
   cooling_apply_self_shielding(cooling, &chemistry_grackle, p, cosmo);
 
   /* solve chemistry */
-  if (local_solve_chemistry(&chemistry_grackle, &grackle_rates, &units, &data,
+  if (local_solve_chemistry(&chemistry_grackle, &rates_grackle, &units, &data,
                             dt) == 0) {
     error("Error in solve_chemistry.");
   }
 
   /* copy from grackle data to particle */
-  cooling_copy_from_grackle(&data, p, xp, density);
+  cooling_copy_from_grackle(&data, p, xp, density, cooling);
 
   return energy;
 }
@@ -702,7 +1007,8 @@ gr_float cooling_time(const struct phys_const* phys_const,
 
   /* initialize data */
   grackle_field_data data;
-  chemistry_data chemistry_grackle = cooling->chemistry;
+  chemistry_data chemistry_grackle = cooling->chemistry_data;
+  chemistry_data_storage rates_grackle = cooling->chemistry_rates;
 
   /* set values */
   /* grid */
@@ -716,7 +1022,7 @@ gr_float cooling_time(const struct phys_const* phys_const,
   data.grid_end = grid_end;
 
   /* general particle data */
-  gr_float density = hydro_get_physical_density(p, cosmo);
+  gr_float density = cooling_get_physical_density(p, cosmo, cooling);
   gr_float energy = hydro_get_physical_internal_energy(p, xp, cosmo);
   energy = max(energy, hydro_props->minimal_internal_energy);
 
@@ -733,20 +1039,21 @@ gr_float cooling_time(const struct phys_const* phys_const,
 
   gr_float species_densities[12];
   /* copy data from particle to grackle data */
-  cooling_copy_to_grackle(&data, p, xp, density, species_densities);
+  cooling_copy_to_grackle(&data, p, xp, density, species_densities, cooling,
+                          phys_const);
 
   /* Apply the self shielding if requested */
   cooling_apply_self_shielding(cooling, &chemistry_grackle, p, cosmo);
 
   /* Compute cooling time */
   gr_float cooling_time;
-  if (local_calculate_cooling_time(&chemistry_grackle, &grackle_rates, &units,
+  if (local_calculate_cooling_time(&chemistry_grackle, &rates_grackle, &units,
                                    &data, &cooling_time) == 0) {
     error("Error in calculate_cooling_time.");
   }
 
   /* copy from grackle data to particle */
-  cooling_copy_from_grackle(&data, p, xp, density);
+  cooling_copy_from_grackle(&data, p, xp, density, cooling);
 
   /* compute rate */
   return cooling_time;
@@ -973,11 +1280,13 @@ void cooling_init_units(const struct unit_system* us,
 void cooling_init_grackle(struct cooling_function_data* cooling) {
 
 #ifdef SWIFT_DEBUG_CHECKS
-  /* enable verbose for grackle */
-  grackle_verbose = 1;
+  /* Enable verbose for grackle for rank 0 only. */
+  if (engine_rank == 0) {
+    grackle_verbose = 1;
+  }
 #endif
 
-  chemistry_data* chemistry = &cooling->chemistry;
+  chemistry_data* chemistry = &cooling->chemistry_data;
 
   /* Create a chemistry object for parameters and rate data. */
   if (set_default_chemistry_parameters(chemistry) == 0) {
@@ -993,28 +1302,53 @@ void cooling_init_grackle(struct cooling_function_data* cooling) {
   chemistry->primordial_chemistry = cooling->primordial_chemistry;
   chemistry->metal_cooling = cooling->with_metal_cooling;
   chemistry->UVbackground = cooling->with_uv_background;
+  chemistry->three_body_rate = cooling->H2_three_body_rate;
+  chemistry->cmb_temperature_floor = cooling->cmb_temperature_floor;
+  chemistry->cie_cooling = cooling->H2_cie_cooling;
+  chemistry->h2_on_dust = cooling->H2_on_dust;
   chemistry->grackle_data_file = cooling->cloudy_table;
 
-  /* radiative transfer */
-  chemistry->use_radiative_transfer = cooling->provide_specific_heating_rates ||
-                                      cooling->provide_volumetric_heating_rates;
-  chemistry->use_volumetric_heating_rate =
-      cooling->provide_volumetric_heating_rates;
-  chemistry->use_specific_heating_rate =
-      cooling->provide_specific_heating_rates;
-
-  if (cooling->provide_specific_heating_rates &&
-      cooling->provide_volumetric_heating_rates)
-    message(
-        "WARNING: You should specified either the specific or the volumetric "
+  if (cooling->local_dust_to_gas_ratio > 0)
+    chemistry->local_dust_to_gas_ratio = cooling->local_dust_to_gas_ratio;
+
+    /* radiative transfer */
+#if COOLING_GRACKLE_MODE == 0
+  if (cooling->use_radiative_transfer)
+    error(
+        "The parameter use_radiative_transfer cannot be set to 1 in Grackle "
+        "mode 0 !");
+#endif
+
+  chemistry->use_radiative_transfer = cooling->use_radiative_transfer;
+
+  if (cooling->volumetric_heating_rates > 0)
+    chemistry->use_volumetric_heating_rate = 1;
+
+  if (cooling->specific_heating_rates > 0)
+    chemistry->use_specific_heating_rate = 1;
+
+  /* hydrogen fraction by mass */
+  chemistry->HydrogenFractionByMass = cooling->HydrogenFractionByMass;
+
+  /* use the Case B recombination rates */
+  chemistry->CaseBRecombination = 1;
+
+  if (cooling->specific_heating_rates > 0 &&
+      cooling->volumetric_heating_rates > 0)
+    error(
+        "You should specified either the specific or the volumetric "
         "heating rates, not both");
 
   /* self shielding */
-  chemistry->self_shielding_method = cooling->self_shielding_method;
+  if (cooling->self_shielding_method <= 0)
+    chemistry->self_shielding_method = 0;
+  else
+    chemistry->self_shielding_method = cooling->self_shielding_method;
 
-  /* Initialize the chemistry object. */
-  if (initialize_chemistry_data(&cooling->units) == 0) {
-    error("Error in initialize_chemistry_data.");
+  if (local_initialize_chemistry_data(&cooling->chemistry_data,
+                                      &cooling->chemistry_rates,
+                                      &cooling->units) == 0) {
+    error("Error in initialize_chemistry_data");
   }
 }
 
@@ -1037,7 +1371,7 @@ void cooling_init_backend(struct swift_params* parameter_file,
     error("Grackle with multiple particles not implemented");
 
   /* read parameters */
-  cooling_read_parameters(parameter_file, cooling, phys_const);
+  cooling_read_parameters(parameter_file, cooling, phys_const, us);
 
   /* Set up the units system. */
   cooling_init_units(us, phys_const, cooling);
@@ -1052,7 +1386,9 @@ void cooling_init_backend(struct swift_params* parameter_file,
  * @param cooling the cooling data structure.
  */
 void cooling_clean(struct cooling_function_data* cooling) {
-  _free_chemistry_data(&cooling->chemistry, &grackle_rates);
+  /* Clean up grackle data. This is a call to a grackle function */
+  local_free_chemistry_data(&cooling->chemistry_data,
+                            &cooling->chemistry_rates);
 }
 
 /**
@@ -1087,3 +1423,41 @@ void cooling_struct_restore(struct cooling_function_data* cooling, FILE* stream,
   /* Set up grackle */
   cooling_init_grackle(cooling);
 }
+
+/**
+ * @brief Get the density of the #part. If the density is bigger than
+ * cooling_density_max, then we floor the density to this value.
+ *
+ * This function ensures that we pass to grackle a density value that is not
+ * to big to ensure good working of grackle.
+ *
+ * Note: This function is called in cooling_time() and cooling_new_energy().
+ *
+ * @param p #part data.
+ * @param cosmo #cosmology data structure.
+ * @param cooling #cooling_function_data struct.
+ */
+double cooling_get_physical_density(
+    const struct part* p, const struct cosmology* cosmo,
+    const struct cooling_function_data* cooling) {
+
+  const double part_density = hydro_get_physical_density(p, cosmo);
+  const double cooling_max_density = cooling->cooling_density_max;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (cooling_max_density > 0.0 && part_density > cooling_max_density) {
+    warning(
+        "Gas particle %lld physical density (%e) is higher than the maximal "
+        "physical density set in the parameter files (%e).",
+        p->id, part_density, cooling_max_density);
+  }
+#endif
+  /* Maximal density cooling is defined */
+  if (cooling_max_density > 0.0) {
+    return fminf(part_density, cooling_max_density);
+  }
+  /* else ( if cooling_max_density <= 0) we do not want to use a density
+     threshold, then return the part density. */
+
+  return part_density;
+}
diff --git a/src/cooling/grackle/cooling.h b/src/cooling/grackle/cooling.h
index 3584bd7f188c9a6ea5bf0501635f771753318d10..e5849a421eff0763217b09c8bcf15734471a1b94 100644
--- a/src/cooling/grackle/cooling.h
+++ b/src/cooling/grackle/cooling.h
@@ -44,9 +44,11 @@ struct swift_params;
 #define GRACKLE_NPART 1
 #define GRACKLE_RANK 3
 
-void cooling_update(const struct cosmology* cosmo,
+void cooling_update(const struct phys_const* phys_const,
+                    const struct cosmology* cosmo,
                     const struct pressure_floor_props* pressure_floor,
-                    struct cooling_function_data* cooling, struct space* s);
+                    struct cooling_function_data* cooling, struct space* s,
+                    const double time);
 
 void cooling_first_init_part(const struct phys_const* phys_const,
                              const struct unit_system* us,
@@ -55,6 +57,26 @@ void cooling_first_init_part(const struct phys_const* phys_const,
                              const struct cooling_function_data* cooling,
                              const struct part* p, struct xpart* xp);
 
+/**
+ * @brief Sets the cooling properties of the (x-)particles to a valid start
+ * state. The function requires the density to be defined and thus must
+ * be called after its computation.
+ *
+ * @param phys_const The #phys_const.
+ * @param us The #unit_system.
+ * @param hydro_props The #hydro_props.
+ * @param cosmo The #cosmology.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+void cooling_post_init_part(const struct phys_const* phys_const,
+                            const struct unit_system* us,
+                            const struct hydro_props* hydro_properties,
+                            const struct cosmology* cosmo,
+                            const struct cooling_function_data* cooling,
+                            const struct part* p, struct xpart* xp);
+
 /**
  * @brief Returns the subgrid temperature of a particle.
  *
@@ -83,6 +105,16 @@ INLINE static float cooling_get_subgrid_density(const struct part* p,
   return -1.f;
 }
 
+/**
+ * @brief Record the time when cooling was switched off for a particle.
+ *
+ * @param p #part data.
+ * @param xp Pointer to the #xpart data.
+ * @param time The time when the cooling was switched off.
+ */
+void cooling_set_part_time_cooling_off(struct part* p, struct xpart* xp,
+                                       const double time);
+
 float cooling_get_radiated_energy(const struct xpart* restrict xp);
 void cooling_print_backend(const struct cooling_function_data* cooling);
 
diff --git a/src/cooling/grackle/cooling_io.h b/src/cooling/grackle/cooling_io.h
index 290b9d894b212b2a70c6f87bc7b3347b18d77087..f2400729297e900d69a37ee3030f9a09758f8801 100644
--- a/src/cooling/grackle/cooling_io.h
+++ b/src/cooling/grackle/cooling_io.h
@@ -25,6 +25,7 @@
 #include "cooling_struct.h"
 #include "io_properties.h"
 #include "physical_constants.h"
+#include "units.h"
 
 #ifdef HAVE_HDF5
 
@@ -98,17 +99,15 @@ __attribute__((always_inline)) INLINE static int cooling_write_particles(
 #endif
 
 #if COOLING_GRACKLE_MODE >= 2
-  list += num;
-
-  list[0] =
+  list[6] =
       io_make_output_field("HM", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
                            cooling_data.HM_frac, "H- mass fraction");
 
-  list[1] =
+  list[7] =
       io_make_output_field("H2I", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
                            cooling_data.H2I_frac, "H2I mass fraction");
 
-  list[2] =
+  list[8] =
       io_make_output_field("H2II", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
                            cooling_data.H2II_frac, "H2II mass fraction");
 
@@ -116,20 +115,17 @@ __attribute__((always_inline)) INLINE static int cooling_write_particles(
 #endif
 
 #if COOLING_GRACKLE_MODE >= 3
-  list += num;
-
-  list[0] =
+  list[9] =
       io_make_output_field("DI", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
                            cooling_data.DI_frac, "DI mass fraction");
 
-  list[1] =
+  list[10] =
       io_make_output_field("DII", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
                            cooling_data.DII_frac, "DII mass fraction");
 
-  list[2] =
+  list[11] =
       io_make_output_field("HDI", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
                            cooling_data.HDI_frac, "HDI mass fraction");
-
   num += 3;
 #endif
 
@@ -145,7 +141,7 @@ __attribute__((always_inline)) INLINE static int cooling_write_particles(
  */
 __attribute__((always_inline)) INLINE static void cooling_read_parameters(
     struct swift_params* parameter_file, struct cooling_function_data* cooling,
-    const struct phys_const* phys_const) {
+    const struct phys_const* phys_const, const struct unit_system* us) {
 
   parser_get_param_string(parameter_file, "GrackleCooling:cloudy_table",
                           cooling->cloudy_table);
@@ -161,6 +157,21 @@ __attribute__((always_inline)) INLINE static void cooling_read_parameters(
     error("Cannot run primordial chemistry %i when compiled with %i",
           cooling->primordial_chemistry, COOLING_GRACKLE_MODE);
 
+  cooling->H2_three_body_rate = parser_get_opt_param_int(
+      parameter_file, "GrackleCooling:H2_three_body_rate", 0);
+
+  cooling->H2_cie_cooling = parser_get_opt_param_int(
+      parameter_file, "GrackleCooling:H2_cie_cooling", 0);
+
+  cooling->H2_on_dust =
+      parser_get_opt_param_int(parameter_file, "GrackleCooling:H2_on_dust", 0);
+
+  cooling->local_dust_to_gas_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:local_dust_to_gas_ratio", -1);
+
+  cooling->cmb_temperature_floor = parser_get_opt_param_int(
+      parameter_file, "GrackleCooling:cmb_temperature_floor", 1);
+
   cooling->with_uv_background =
       parser_get_param_int(parameter_file, "GrackleCooling:with_UV_background");
 
@@ -170,11 +181,62 @@ __attribute__((always_inline)) INLINE static void cooling_read_parameters(
   cooling->with_metal_cooling =
       parser_get_param_int(parameter_file, "GrackleCooling:with_metal_cooling");
 
-  cooling->provide_volumetric_heating_rates = parser_get_opt_param_int(
-      parameter_file, "GrackleCooling:provide_volumetric_heating_rates", 0);
+  cooling->use_radiative_transfer = parser_get_opt_param_int(
+      parameter_file, "GrackleCooling:use_radiative_transfer", 0);
+
+  cooling->RT_heating_rate = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:RT_heating_rate_cgs", 0);
+
+  cooling->RT_HI_ionization_rate = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:RT_HI_ionization_rate_cgs", 0);
+
+  cooling->RT_HeI_ionization_rate = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:RT_HeI_ionization_rate_cgs", 0);
 
-  cooling->provide_specific_heating_rates = parser_get_opt_param_int(
-      parameter_file, "GrackleCooling:provide_specific_heating_rates", 0);
+  cooling->RT_HeII_ionization_rate = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:RT_HeII_ionization_rate_cgs", 0);
+
+  cooling->RT_H2_dissociation_rate = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:RT_H2_dissociation_rate_cgs", 0);
+
+  cooling->volumetric_heating_rates = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:volumetric_heating_rates_cgs", 0);
+
+  cooling->specific_heating_rates = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:specific_heating_rates_cgs", 0);
+
+  cooling->HydrogenFractionByMass = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:HydrogenFractionByMass", 0.76);
+
+  cooling->initial_nHII_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nHII_to_nH_ratio", -1);
+
+  cooling->initial_nHeI_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nHeI_to_nH_ratio", -1);
+
+  cooling->initial_nHeII_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nHeII_to_nH_ratio", -1);
+
+  cooling->initial_nHeIII_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nHeIII_to_nH_ratio", -1);
+
+  cooling->initial_nDI_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nDI_to_nH_ratio", -1);
+
+  cooling->initial_nDII_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nDII_to_nH_ratio", -1);
+
+  cooling->initial_nHM_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nHM_to_nH_ratio", -1);
+
+  cooling->initial_nH2I_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nH2I_to_nH_ratio", -1);
+
+  cooling->initial_nH2II_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nH2II_to_nH_ratio", -1);
+
+  cooling->initial_nHDI_to_nH_ratio = parser_get_opt_param_double(
+      parameter_file, "GrackleCooling:initial_nHDI_to_nH_ratio", -1);
 
   /* Self shielding */
   cooling->self_shielding_method = parser_get_opt_param_int(
@@ -196,6 +258,16 @@ __attribute__((always_inline)) INLINE static void cooling_read_parameters(
   cooling->thermal_time = parser_get_param_double(
       parameter_file, "GrackleCooling:thermal_time_myr");
   cooling->thermal_time *= phys_const->const_year * 1e6;
+
+  /* Maximal cooling density (in acc) */
+  cooling->cooling_density_max = parser_get_param_double(
+      parameter_file, "GrackleCooling:maximal_density_Hpcm3");
+
+  /* Convert from acc to intenal units */
+  const double m_p_cgs = phys_const->const_proton_mass *
+                         units_cgs_conversion_factor(us, UNIT_CONV_MASS);
+  cooling->cooling_density_max *=
+      m_p_cgs / units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
 }
 
 #endif /* SWIFT_COOLING_GRACKLE_IO_H */
diff --git a/src/cooling/grackle/cooling_properties.h b/src/cooling/grackle/cooling_properties.h
index 385ecdf59fec48d94130426389acd732c922083c..b4e724b1fba3427ce0b0b7dc3c96d6c725be7339 100644
--- a/src/cooling/grackle/cooling_properties.h
+++ b/src/cooling/grackle/cooling_properties.h
@@ -19,6 +19,9 @@
 #ifndef SWIFT_COOLING_PROPERTIES_GRACKLE_H
 #define SWIFT_COOLING_PROPERTIES_GRACKLE_H
 
+/* skip deprecation warnings. I cleaned old API calls. */
+#define OMIT_LEGACY_INTERNAL_GRACKLE_FUNC
+
 /* include grackle */
 #include <grackle.h>
 
@@ -41,6 +44,22 @@ struct cooling_function_data {
   /*! Chemistry network */
   int primordial_chemistry;
 
+  /*! Set the three-body reaction rate (see grackle documentation) */
+  int H2_three_body_rate;
+
+  /*! Enable/disable H2 collision-induced emission cooling from Ripamonti & Abel
+   * (2004) */
+  int H2_cie_cooling;
+
+  /*! Flag to enable H2 formation on dust grains */
+  int H2_on_dust;
+
+  /*! The ratio of total dust mass to gas mass in the local Universe. */
+  double local_dust_to_gas_ratio;
+
+  /*! Enable/disable CMB temperature floor */
+  int cmb_temperature_floor;
+
   /*! Redshift to use for the UV backgroud (-1 to use cosmological one) */
   double redshift;
 
@@ -48,16 +67,71 @@ struct cooling_function_data {
   code_units units;
 
   /*! grackle chemistry data */
-  chemistry_data chemistry;
+  chemistry_data chemistry_data;
+
+  /*! grackle chemistry data storage
+   * (needed for local function calls) */
+  chemistry_data_storage chemistry_rates;
 
   /*! Enable/Disable metal cooling */
   int with_metal_cooling;
 
-  /*! User provide volumetric heating rates */
-  int provide_volumetric_heating_rates;
+  /*! Arrays of ionization and heating rates are provided */
+  int use_radiative_transfer;
+
+  /*! Grackle RT_heating_rate (in IU) */
+  float RT_heating_rate;
+
+  /*! Grackle RT_HI_ionization_rate (in IU) */
+  float RT_HI_ionization_rate;
+
+  /*! Grackle RT_HeI_ionization_rate (in IU) */
+  float RT_HeI_ionization_rate;
+
+  /*! Grackle RT_HeII_ionization_rate (in IU) */
+  float RT_HeII_ionization_rate;
+
+  /*! Grackle RT_H2_dissociation_rate (in IU) */
+  float RT_H2_dissociation_rate;
+
+  /*! Volumetric heating rates */
+  float volumetric_heating_rates;
 
-  /*! User provide specific heating rates */
-  int provide_specific_heating_rates;
+  /*! Specific heating rates */
+  float specific_heating_rates;
+
+  /*! Hydrogen fraction by mass */
+  float HydrogenFractionByMass;
+
+  /*! initial nHII to nH ratio (number density ratio) */
+  float initial_nHII_to_nH_ratio;
+
+  /*! initial nHeI to nH ratio (number density ratio) */
+  float initial_nHeI_to_nH_ratio;
+
+  /*! initial nHeII to nH ratio (number density ratio) */
+  float initial_nHeII_to_nH_ratio;
+
+  /*! initial nHeIII to nH ratio (number density ratio) */
+  float initial_nHeIII_to_nH_ratio;
+
+  /*! initial nDI to nH ratio (number density ratio) */
+  float initial_nDI_to_nH_ratio;
+
+  /*! initial nDII to nH ratio (number density ratio) */
+  float initial_nDII_to_nH_ratio;
+
+  /*! initial nHM to nH ratio (number density ratio) */
+  float initial_nHM_to_nH_ratio;
+
+  /*! initial nH2I to nH ratio (number density ratio) */
+  float initial_nH2I_to_nH_ratio;
+
+  /*! initial nH2II to nH ratio (number density ratio) */
+  float initial_nH2II_to_nH_ratio;
+
+  /*! initial nHDI to nH ratio (number density ratio) */
+  float initial_nHDI_to_nH_ratio;
 
   /*! Self shielding method (1 -> 3 for grackle's ones, 0 for none and -1 for
    * GEAR) */
@@ -77,6 +151,9 @@ struct cooling_function_data {
 
   /*! Duration for switching off cooling after an event (e.g. supernovae) */
   double thermal_time;
+
+  /*! Maximal allowed density for cooling (in internal units). */
+  double cooling_density_max;
 };
 
 #endif /* SWIFT_COOLING_PROPERTIES_GRACKLE_H */
diff --git a/src/cooling/none/cooling.h b/src/cooling/none/cooling.h
index 404f5ff48497f4461c1a1ab0cab8a5a7b163c9c8..d7955c4e4b1902796b855012e3f454f69e0eb70e 100644
--- a/src/cooling/none/cooling.h
+++ b/src/cooling/none/cooling.h
@@ -45,11 +45,12 @@
  * @param cooling The #cooling_function_data used in the run.
  * @param pressure_floor Properties of the pressure floor.
  * @param s The #space containing all the particles.
+ * @param time The current system time
  */
 INLINE static void cooling_update(
-    const struct cosmology* cosmo,
+    const struct phys_const* phys_const, const struct cosmology* cosmo,
     const struct pressure_floor_props* pressure_floor,
-    struct cooling_function_data* cooling, struct space* s) {
+    struct cooling_function_data* cooling, struct space* s, const double time) {
   // Add content if required.
 }
 
@@ -127,6 +128,28 @@ __attribute__((always_inline)) INLINE static void cooling_first_init_part(
     const struct cooling_function_data* data, const struct part* restrict p,
     struct xpart* restrict xp) {}
 
+/**
+ * @brief Perform additional init on the cooling properties of the
+ * (x-)particles that requires the density to be known.
+ *
+ * Nothing to do here.
+ *
+ * @param phys_const The physical constant in internal units.
+ * @param us The unit system.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param cosmo The current cosmological model.
+ * @param cooling The properties of the cooling function.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static void cooling_post_init_part(
+    const struct phys_const* restrict phys_const,
+    const struct unit_system* restrict us,
+    const struct hydro_props* hydro_props,
+    const struct cosmology* restrict cosmo,
+    const struct cooling_function_data* cooling, const struct part* restrict p,
+    struct xpart* restrict xp) {}
+
 /**
  * @brief Compute the temperature of a #part based on the cooling function.
  *
@@ -254,6 +277,19 @@ __attribute__((always_inline)) INLINE static float cooling_get_radiated_energy(
   return 0.f;
 }
 
+/**
+ * @brief Record the time when cooling was switched off for a particle.
+ *
+ * In none model, this function does nothing.
+ *
+ * @param p #part data.
+ * @param xp Pointer to the #xpart data.
+ * @param time The time when the cooling was switched off.
+ */
+INLINE static void cooling_set_part_time_cooling_off(struct part* p,
+                                                     struct xpart* xp,
+                                                     const double time) {}
+
 /**
  * @brief Split the coolong content of a particle into n pieces
  *
diff --git a/src/cosmology.c b/src/cosmology.c
index 23915f925378b2f5dd76e7b23cd858ef85c40d44..ef98cdb815890a7401870b3520bcca0bf8196668 100644
--- a/src/cosmology.c
+++ b/src/cosmology.c
@@ -813,7 +813,9 @@ void cosmology_init_tables(struct cosmology *c) {
 
     /* Choose value of expansion factor for check */
     const double dloga = (c->log_a_end - c->log_a_begin) / (n - 1);
-    const double a = exp(c->log_a_begin + dloga * i);
+    double a = exp(c->log_a_begin + dloga * i);
+    a = fmax(a, c->a_begin);
+    a = fmin(a, c->a_end);
 
     /* Verify that converting expansion factor to time and back recovers the
      * original value */
@@ -830,11 +832,10 @@ void cosmology_init_tables(struct cosmology *c) {
     if (frac_error > max_error_distance) max_error_distance = frac_error;
   }
 
-  message("Max fractional error in a to age of universe round trip = %16.8e\n",
+  message("Max fractional error in a to age of universe round trip = %16.8e",
           max_error_time);
-  message(
-      "Max fractional error in a to comoving distance round trip = %16.8e\n",
-      max_error_distance);
+  message("Max fractional error in a to comoving distance round trip = %16.8e",
+          max_error_distance);
 
 #endif /* SWIFT_DEBUG_CHECKS */
 
diff --git a/src/cosmology.h b/src/cosmology.h
index a5fa2f32e41dfd5c4c9d1c141f83ecad489302d7..ef5adf491f9c86f4cd05b45d66f2034801e3c680 100644
--- a/src/cosmology.h
+++ b/src/cosmology.h
@@ -113,7 +113,7 @@ struct cosmology {
   /*! Scale-factor at the previous time-step */
   double a_old;
 
-  /*! Redshit at the previous time-step */
+  /*! Redshift at the previous time-step */
   double z_old;
 
   /*------------------------------------------------------------------ */
diff --git a/src/csds.c b/src/csds.c
index 0dbd8e4221d8edc8b561436f460cd273ff7d34c7..7459f83cc35404fe99b5c1acc11a7e7a0f565e2c 100644
--- a/src/csds.c
+++ b/src/csds.c
@@ -1232,7 +1232,7 @@ void csds_struct_restore(struct csds_writer *log, FILE *stream) {
 
   /* Restore the pointers */
   for (int i = 0; i < swift_type_count; i++) {
-    if (log->field_pointers == NULL) continue;
+    if (log->field_pointers[i] == NULL) continue;
 
     log->field_pointers[i] =
         log->list_fields + (log->field_pointers[i] - old_list_fields);
diff --git a/src/csds_io.h b/src/csds_io.h
index 3f7de3ef12d8284e02b911124752af91e58989ce..77d738b6c62d73043647a4195b88bcb250fd2fe0 100644
--- a/src/csds_io.h
+++ b/src/csds_io.h
@@ -165,12 +165,10 @@ struct csds_field {
  * @param conversion_func The conversion function.
  * @param field_size The size of the field to write.
  */
-#define csds_define_field_from_function_hydro(csds_field, field_name,     \
-                                              conversion_func, size)      \
-  {                                                                       \
-    csds_define_field_from_function_general(csds_field, field_name,       \
-                                            conversion_func, size, hydro) \
-  }
+#define csds_define_field_from_function_hydro(csds_field, field_name, \
+                                              conversion_func, size)  \
+  {csds_define_field_from_function_general(csds_field, field_name,    \
+                                           conversion_func, size, hydro)}
 
 /**
  * @brief Define a field from a function for stars.
@@ -180,12 +178,10 @@ struct csds_field {
  * @param conversion_func The conversion function.
  * @param field_size The size of the field to write.
  */
-#define csds_define_field_from_function_stars(csds_field, field_name,     \
-                                              conversion_func, size)      \
-  {                                                                       \
-    csds_define_field_from_function_general(csds_field, field_name,       \
-                                            conversion_func, size, stars) \
-  }
+#define csds_define_field_from_function_stars(csds_field, field_name, \
+                                              conversion_func, size)  \
+  {csds_define_field_from_function_general(csds_field, field_name,    \
+                                           conversion_func, size, stars)}
 
 /**
  * @brief Define a field from a function for gravity.
@@ -195,12 +191,10 @@ struct csds_field {
  * @param conversion_func The conversion function.
  * @param field_size The size of the field to write.
  */
-#define csds_define_field_from_function_gravity(csds_field, field_name,  \
-                                                conversion_func, size)   \
-  {                                                                      \
-    csds_define_field_from_function_general(csds_field, field_name,      \
-                                            conversion_func, size, grav) \
-  }
+#define csds_define_field_from_function_gravity(csds_field, field_name, \
+                                                conversion_func, size)  \
+  {csds_define_field_from_function_general(csds_field, field_name,      \
+                                           conversion_func, size, grav)}
 
 void csds_write_description(struct csds_writer *log, struct engine *e);
 
diff --git a/src/debug.c b/src/debug.c
index 978da76415b20caf0724f7c4328a8d431bf16aa8..2db8f69dff21c88ddd3f5ef4d9639e02ce394d89 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -70,10 +70,12 @@
 #include "./hydro/Phantom/hydro_debug.h"
 #elif defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
 #include "./hydro/Gizmo/hydro_debug.h"
-#elif defined(SHADOWFAX_SPH)
+#elif defined(SHADOWSWIFT)
 #include "./hydro/Shadowswift/hydro_debug.h"
 #elif defined(PLANETARY_SPH)
 #include "./hydro/Planetary/hydro_debug.h"
+#elif defined(REMIX_SPH)
+#include "./hydro/REMIX/hydro_debug.h"
 #elif defined(SPHENIX_SPH)
 #include "./hydro/SPHENIX/hydro_debug.h"
 #elif defined(GASOLINE_SPH)
@@ -242,8 +244,8 @@ int checkSpacehmax(struct space *s) {
   float cell_sinks_h_max = 0.0f;
   for (int k = 0; k < s->nr_cells; k++) {
     if (s->cells_top[k].nodeID == s->e->nodeID &&
-        s->cells_top[k].sinks.r_cut_max > cell_sinks_h_max) {
-      cell_sinks_h_max = s->cells_top[k].sinks.r_cut_max;
+        s->cells_top[k].sinks.h_max > cell_sinks_h_max) {
+      cell_sinks_h_max = s->cells_top[k].sinks.h_max;
     }
   }
 
@@ -266,8 +268,8 @@ int checkSpacehmax(struct space *s) {
   /* Now all the sinks. */
   float sink_h_max = 0.0f;
   for (size_t k = 0; k < s->nr_sinks; k++) {
-    if (s->sinks[k].r_cut > sink_h_max) {
-      sink_h_max = s->sinks[k].r_cut;
+    if (s->sinks[k].h > sink_h_max) {
+      sink_h_max = s->sinks[k].h;
     }
   }
 
@@ -315,17 +317,17 @@ int checkSpacehmax(struct space *s) {
   /* sink */
   for (int k = 0; k < s->nr_cells; k++) {
     if (s->cells_top[k].nodeID == s->e->nodeID) {
-      if (s->cells_top[k].sinks.r_cut_max > sink_h_max) {
+      if (s->cells_top[k].sinks.h_max > sink_h_max) {
         message("cell %d is inconsistent (%f > %f)", k,
-                s->cells_top[k].sinks.r_cut_max, sink_h_max);
+                s->cells_top[k].sinks.h_max, sink_h_max);
       }
     }
   }
 
   for (size_t k = 0; k < s->nr_sinks; k++) {
-    if (s->sinks[k].r_cut > cell_sinks_h_max) {
+    if (s->sinks[k].h > cell_sinks_h_max) {
       message("spart %lld is inconsistent (%f > %f)", s->sinks[k].id,
-              s->sinks[k].r_cut, cell_sinks_h_max);
+              s->sinks[k].h, cell_sinks_h_max);
     }
   }
 
@@ -436,7 +438,7 @@ int checkCellhdxmax(const struct cell *c, int *depth) {
                       sp->x_diff[1] * sp->x_diff[1] +
                       sp->x_diff[2] * sp->x_diff[2];
 
-    sinks_h_max = max(sinks_h_max, sp->r_cut);
+    sinks_h_max = max(sinks_h_max, sp->h);
     sinks_dx_max = max(sinks_dx_max, sqrtf(dx2));
   }
 
@@ -476,9 +478,9 @@ int checkCellhdxmax(const struct cell *c, int *depth) {
     result = 0;
   }
 
-  if (c->sinks.r_cut_max != sinks_h_max) {
+  if (c->sinks.h_max != sinks_h_max) {
     message("%d Inconsistent sinks_h_max: cell %f != parts %f", *depth,
-            c->sinks.r_cut_max, sinks_h_max);
+            c->sinks.h_max, sinks_h_max);
     message("location: %f %f %f", c->loc[0], c->loc[1], c->loc[2]);
     result = 0;
   }
diff --git a/src/distributed_io.c b/src/distributed_io.c
index b60958a3bea25239acf0c1d031b762efa66cef58..45500fdf0f801ab4e64198b7e6356cdead41c75d 100644
--- a/src/distributed_io.c
+++ b/src/distributed_io.c
@@ -56,13 +56,17 @@
 #include "sink_io.h"
 #include "star_formation_io.h"
 #include "stars_io.h"
+#include "swift_lustre_api.h"
 #include "tools.h"
 #include "units.h"
 #include "version.h"
 #include "xmf.h"
 
 /* Are we timing the i/o? */
-//#define IO_SPEED_MEASUREMENT
+// #define IO_SPEED_MEASUREMENT
+
+/* Max number of entries that can be written for a given particle type */
+static const int io_max_size_output_list = 100;
 
 /**
  * @brief Writes a data array in given HDF5 group.
@@ -236,6 +240,9 @@ void write_distributed_array(
   io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
   io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
   io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -401,6 +408,9 @@ void write_array_virtual(struct engine* e, hid_t grp, const char* fileName_base,
   io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
   io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
   io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -452,6 +462,7 @@ void write_array_virtual(struct engine* e, hid_t grp, const char* fileName_base,
  * @param numFields The number of fields to write for each particle type.
  * @param internal_units The #unit_system used internally.
  * @param snapshot_units The #unit_system used in the snapshots.
+ * @param fof Is this a snapshot related to a stand-alone FOF call?
  * @param subsample_any Are any fields being subsampled?
  * @param subsample_fraction The subsampling fraction of each particle type.
  */
@@ -463,7 +474,7 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
                         const int numFields[swift_type_count],
                         char current_selection_name[FIELD_BUFFER_SIZE],
                         const struct unit_system* internal_units,
-                        const struct unit_system* snapshot_units,
+                        const struct unit_system* snapshot_units, const int fof,
                         const int subsample_any,
                         const float subsample_fraction[swift_type_count]) {
 
@@ -498,8 +509,14 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
    * specific output */
   xmf_write_outputheader(xmfFile, fileName, e->time);
 
+  /* Set the minimal API version to avoid issues with advanced features */
+  hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+  herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                    HDF5_HIGHEST_FILE_FORMAT_VERSION);
+  if (err < 0) error("Error setting the hdf5 API version");
+
   /* Open HDF5 file with the chosen parameters */
-  hid_t h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+  hid_t h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
   if (h_file < 0) error("Error while opening file '%s'.", fileName);
 
   /* Open header to write simulation properties */
@@ -604,7 +621,7 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
   ic_info_write_hdf5(e->ics_metadata, h_file);
 
   /* Write all the meta-data */
-  io_write_meta_data(h_file, e, internal_units, snapshot_units);
+  io_write_meta_data(h_file, e, internal_units, snapshot_units, fof);
 
   /* Loop over all particle types */
   for (int ptype = 0; ptype < swift_type_count; ptype++) {
@@ -641,8 +658,8 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
     io_write_attribute_ll(h_grp, "TotalNumberOfParticles", N_total[ptype]);
 
     int num_fields = 0;
-    struct io_props list[100];
-    bzero(list, 100 * sizeof(struct io_props));
+    struct io_props list[io_max_size_output_list];
+    bzero(list, io_max_size_output_list * sizeof(struct io_props));
 
     /* Write particle fields from the particle structure */
     switch (ptype) {
@@ -683,6 +700,15 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
         error("Particle Type %d not yet supported. Aborting", ptype);
     }
 
+    /* Verify we are not going to crash when writing below */
+    if (num_fields >= io_max_size_output_list)
+      error("Too many fields to write for particle type %d", ptype);
+    for (int i = 0; i < num_fields; ++i) {
+      if (!list[i].is_used) error("List of field contains an empty entry!");
+      if (!list[i].dimension)
+        error("Dimension of field '%s' is <= 1!", list[i].name);
+    }
+
     /* Did the user specify a non-standard default for the entire particle
      * type? */
     const enum lossy_compression_schemes compression_level_current_default =
@@ -722,8 +748,9 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
   /* Write LXMF file descriptor */
   xmf_write_outputfooter(xmfFile, e->snapshot_output_count, e->time);
 
-  /* Close the file for now */
+  /* Close the file */
   H5Fclose(h_file);
+  H5Pclose(h_props);
 
 #else
   error(
@@ -738,6 +765,7 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
  * @param e The engine containing all the system.
  * @param internal_units The #unit_system used internally
  * @param snapshot_units The #unit_system used in the snapshots
+ * @param fof Is this a snapshot related to a stand-alone FOF call?
  * @param mpi_rank The rank number of the calling MPI rank.
  * @param mpi_size the number of MPI ranks.
  * @param comm The communicator used by the MPI ranks.
@@ -751,8 +779,9 @@ void write_virtual_file(struct engine* e, const char* fileName_base,
 void write_output_distributed(struct engine* e,
                               const struct unit_system* internal_units,
                               const struct unit_system* snapshot_units,
-                              const int mpi_rank, const int mpi_size,
-                              MPI_Comm comm, MPI_Info info) {
+                              const int fof, const int mpi_rank,
+                              const int mpi_size, MPI_Comm comm,
+                              MPI_Info info) {
 
   hid_t h_file = 0, h_grp = 0;
   int numFiles = mpi_size;
@@ -940,7 +969,7 @@ void write_output_distributed(struct engine* e,
   }
 
   /* Compute offset in the file and total number of particles */
-  const long long N[swift_type_count] = {
+  long long N[swift_type_count] = {
       Ngas_written,   Ndm_written,         Ndm_background, Nsinks_written,
       Nstars_written, Nblackholes_written, Ndm_neutrino};
 
@@ -964,26 +993,59 @@ void write_output_distributed(struct engine* e,
   };
 
   /* Use a single Lustre stripe with a rank-based OST offset? */
-  if (e->snapshot_lustre_OST_count != 0) {
-
-    /* Use a random offset to avoid placing things in the same OSTs. We do
-     * this to keep the use of OSTs balanced, much like using -1 for the
-     * stripe. */
-    int offset = rand() % e->snapshot_lustre_OST_count;
-    MPI_Bcast(&offset, 1, MPI_INT, 0, MPI_COMM_WORLD);
-
-    char string[1200];
-    sprintf(string, "lfs setstripe -c 1 -i %d %s",
-            ((e->nodeID + offset) % e->snapshot_lustre_OST_count), fileName);
-    const int result = system(string);
-    if (result != 0) {
-      message("lfs setstripe command returned error code %d", result);
+  if (e->snapshot_lustre_OST_checks != 0) {
+
+    /* Gather information about the current state of the OSTs. */
+    struct swift_ost_store ost_infos;
+
+    /* Select good OSTs sorted by free space. */
+    if (e->nodeID == 0) {
+      swift_ost_select(&ost_infos, fileName, e->snapshot_lustre_OST_free,
+                       e->snapshot_lustre_OST_test, e->verbose);
+    }
+
+    /* Distribute the OST information. */
+    MPI_Bcast(&ost_infos, sizeof(struct swift_ost_store), MPI_BYTE, 0,
+              MPI_COMM_WORLD);
+
+    /* Need to make space for the OSTs and copy those locally. If the count is
+     * zero this is probably not a lustre mount. */
+    if (ost_infos.count > 0) {
+      if (e->nodeID != 0) swift_ost_store_alloc(&ost_infos, ost_infos.size);
+      MPI_Bcast(ost_infos.infos, sizeof(struct swift_ost_info) * ost_infos.size,
+                MPI_BYTE, 0, MPI_COMM_WORLD);
+
+      /* We now know how many OSTs are available, each rank should attempt to
+       * use a different one, but overtime we should try not to use the same
+       * ones. Culling will order things by free space so we should get some
+       * reordering of those if we do this process each time. */
+      int dummy = e->nodeID;
+      int offset = swift_ost_next(&ost_infos, &dummy, 1);
+
+      /* And create the file with a stripe of 1 on the OST. */
+      const int result = swift_create_striped_file(fileName, offset, 1, &dummy);
+      if (result != 0) message("failed to set stripe of snapshot");
+
+      /* Finished with this. */
+      swift_ost_store_free(&ost_infos);
+    } else if (e->nodeID == 0) {
+      swift_ost_store_free(&ost_infos);
+
+      /* Don't try this again until next launch. */
+      e->snapshot_lustre_OST_checks = 0;
+      message("Disabling further lustre OST checks");
     }
   }
 
+  /* Set the minimal API version to avoid issues with advanced features */
+  hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+  herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                    HDF5_HIGHEST_FILE_FORMAT_VERSION);
+  if (err < 0) error("Error setting the hdf5 API version");
+
   /* Open file */
   /* message("Opening file '%s'.", fileName); */
-  h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+  h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
   if (h_file < 0) error("Error while opening file '%s'.", fileName);
 
   /* Open header to write simulation properties */
@@ -1097,7 +1159,7 @@ void write_output_distributed(struct engine* e,
   ic_info_write_hdf5(e->ics_metadata, h_file);
 
   /* Write all the meta-data */
-  io_write_meta_data(h_file, e, internal_units, snapshot_units);
+  io_write_meta_data(h_file, e, internal_units, snapshot_units, fof);
 
   /* Now write the top-level cell structure
    * We use a global offset of 0 here. This means that the cells will write
@@ -1144,8 +1206,8 @@ void write_output_distributed(struct engine* e,
     io_write_attribute_ll(h_grp, "TotalNumberOfParticles", N_total[ptype]);
 
     int num_fields = 0;
-    struct io_props list[100];
-    bzero(list, 100 * sizeof(struct io_props));
+    struct io_props list[io_max_size_output_list];
+    bzero(list, io_max_size_output_list * sizeof(struct io_props));
     size_t Nparticles = 0;
 
     struct part* parts_written = NULL;
@@ -1413,6 +1475,15 @@ void write_output_distributed(struct engine* e,
         error("Particle Type %d not yet supported. Aborting", ptype);
     }
 
+    /* Verify we are not going to crash when writing below */
+    if (num_fields >= io_max_size_output_list)
+      error("Too many fields to write for particle type %d", ptype);
+    for (int i = 0; i < num_fields; ++i) {
+      if (!list[i].is_used) error("List of field contains an empty entry!");
+      if (!list[i].dimension)
+        error("Dimension of field '%s' is <= 1!", list[i].name);
+    }
+
     /* Did the user specify a non-standard default for the entire particle
      * type? */
     const enum lossy_compression_schemes compression_level_current_default =
@@ -1460,6 +1531,7 @@ void write_output_distributed(struct engine* e,
 
   /* Close file */
   H5Fclose(h_file);
+  H5Pclose(h_props);
 
 #if H5_VERSION_GE(1, 10, 0)
 
@@ -1467,7 +1539,7 @@ void write_output_distributed(struct engine* e,
   if (mpi_rank == 0)
     write_virtual_file(e, fileName_base, xmfFileName, N_total, N_counts,
                        mpi_size, to_write, numFields, current_selection_name,
-                       internal_units, snapshot_units, subsample_any,
+                       internal_units, snapshot_units, fof, subsample_any,
                        subsample_fraction);
 
   /* Make sure nobody is allowed to progress until rank 0 is done. */
@@ -1482,8 +1554,13 @@ void write_output_distributed(struct engine* e,
     char fileName_virtual[1030];
     sprintf(fileName_virtual, "%s.hdf5", fileName_base);
 
+    h_props = H5Pcreate(H5P_FILE_ACCESS);
+    err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                               HDF5_HIGHEST_FILE_FORMAT_VERSION);
+    if (err < 0) error("Error setting the hdf5 API version");
+
     /* Open the snapshot on rank 0 */
-    h_file_cells = H5Fopen(fileName_virtual, H5F_ACC_RDWR, H5P_DEFAULT);
+    h_file_cells = H5Fopen(fileName_virtual, H5F_ACC_RDWR, h_props);
     if (h_file_cells < 0)
       error("Error while opening file '%s' on rank %d.", fileName_virtual,
             mpi_rank);
@@ -1511,6 +1588,7 @@ void write_output_distributed(struct engine* e,
   if (mpi_rank == 0) {
     H5Gclose(h_grp_cells);
     H5Fclose(h_file_cells);
+    H5Pclose(h_props);
   }
 
 #endif
diff --git a/src/distributed_io.h b/src/distributed_io.h
index 0a73129ab7ab6c5a4502b4dcfeab9b72b47558f8..0fbb66cfe80e5a8fccf876a95983844334e6c9d0 100644
--- a/src/distributed_io.h
+++ b/src/distributed_io.h
@@ -35,8 +35,8 @@ struct unit_system;
 void write_output_distributed(struct engine* e,
                               const struct unit_system* internal_units,
                               const struct unit_system* snapshot_units,
-                              int mpi_rank, int mpi_size, MPI_Comm comm,
-                              MPI_Info info);
+                              const int fof, int mpi_rank, int mpi_size,
+                              MPI_Comm comm, MPI_Info info);
 
 #endif /* HAVE_HDF5 && WITH_MPI */
 
diff --git a/src/drift.h b/src/drift.h
index 1e5764d9004c886af045413e04f692765a80f4a9..213781d82f25f5c33c1868d079d29d6c44227572 100644
--- a/src/drift.h
+++ b/src/drift.h
@@ -23,6 +23,7 @@
 #include <config.h>
 
 /* Local headers. */
+#include "adaptive_softening.h"
 #include "black_holes.h"
 #include "const.h"
 #include "debug.h"
@@ -205,6 +206,7 @@ __attribute__((always_inline)) INLINE static void drift_part(
   mhd_predict_extra(p, xp, dt_drift, dt_therm, cosmo, hydro_props,
                     entropy_floor);
   rt_predict_extra(p, xp, dt_drift);
+  if (p->gpart) gravity_update_softening(p->gpart, p, e->gravity_properties);
 
   /* Compute offsets since last cell construction */
   for (int k = 0; k < 3; k++) {
@@ -390,6 +392,25 @@ __attribute__((always_inline)) INLINE static void drift_sink(
   sink->ti_drift = ti_current;
 #endif
 
+#ifdef SWIFT_FIXED_BOUNDARY_PARTICLES
+
+  /* Get the ID of the gpart */
+  const long long id = sink->id;
+
+  /* Cancel the velocity of the particles */
+  if (id < SWIFT_FIXED_BOUNDARY_PARTICLES) {
+
+    /* Don't move! */
+    sink->v[0] = 0.f;
+    sink->v[1] = 0.f;
+    sink->v[2] = 0.f;
+  }
+#endif
+
+#ifdef WITH_LIGHTCONE
+  error("Lightcone treatment of sinks needs implementing");
+#endif
+
   /* Drift... */
   sink->x[0] += sink->v[0] * dt_drift;
   sink->x[1] += sink->v[1] * dt_drift;
diff --git a/src/engine.c b/src/engine.c
index 474f0a5b0e0d263eb84723111a3bc2b06afaeb36..effbe577ac2cb94bde8e9676f14b6f5cb38c2e1c 100644
--- a/src/engine.c
+++ b/src/engine.c
@@ -70,6 +70,7 @@
 #include "extra_io.h"
 #include "feedback.h"
 #include "fof.h"
+#include "forcing.h"
 #include "gravity.h"
 #include "gravity_cache.h"
 #include "hydro.h"
@@ -133,7 +134,9 @@ const char *engine_policy_names[] = {"none",
                                      "line of sight",
                                      "sink",
                                      "rt",
-                                     "power spectra"};
+                                     "power spectra",
+                                     "moving mesh",
+                                     "moving mesh hydro"};
 
 const int engine_default_snapshot_subsample[swift_type_count] = {0};
 
@@ -321,7 +324,7 @@ void engine_repartition_trigger(struct engine *e) {
           e->usertime_last_step, e->systime_last_step, (double)resident,
           e->local_deadtime / (e->nr_threads * e->wallclock_time)};
       double timemems[e->nr_nodes * 4];
-      MPI_Gather(&timemem, 4, MPI_DOUBLE, timemems, 4, MPI_DOUBLE, 0,
+      MPI_Gather(timemem, 4, MPI_DOUBLE, timemems, 4, MPI_DOUBLE, 0,
                  MPI_COMM_WORLD);
       if (e->nodeID == 0) {
 
@@ -490,6 +493,34 @@ void engine_exchange_cells(struct engine *e) {
 #endif
 }
 
+/**
+ * @brief Exchange extra information for the grid construction with other nodes.
+ *
+ * @param e The #engine.
+ */
+void engine_exchange_grid_extra(struct engine *e) {
+
+#ifdef WITH_MPI
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (!(e->policy & engine_policy_grid))
+    error("Not running with grid, but trying to exchange grid information!");
+#endif
+
+  const ticks tic = getticks();
+
+  /* Exchange the grid info with neighbouring ranks. */
+  proxy_grid_extra_exchange(e->proxies, e->nr_proxies, e->s);
+
+  if (e->verbose)
+    message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
+            clocks_getunit());
+
+#else
+  error("SWIFT was not compiled with MPI support.");
+#endif
+}
+
 /**
  * @brief Exchanges the top-level multipoles between all the nodes
  * such that every node has a multipole for each top-level cell.
@@ -746,16 +777,18 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
 #ifdef WITH_MPI
 
   const int nr_proxies = e->nr_proxies;
-  const int with_hydro = e->policy & engine_policy_hydro;
+  const int with_hydro =
+      e->policy & (engine_policy_hydro | engine_policy_grid_hydro);
   const int with_stars = e->policy & engine_policy_stars;
   const int with_black_holes = e->policy & engine_policy_black_holes;
+  const int with_sinks = e->policy & engine_policy_sinks;
   struct space *s = e->s;
   ticks tic = getticks();
 
   /* Count the number of particles we need to import and re-allocate
      the buffer if needed. */
   size_t count_parts_in = 0, count_gparts_in = 0, count_sparts_in = 0,
-         count_bparts_in = 0;
+         count_bparts_in = 0, count_sinks_in = 0;
   for (int k = 0; k < nr_proxies; k++) {
     for (int j = 0; j < e->proxies[k].nr_cells_in; j++) {
 
@@ -774,6 +807,11 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
 
       /* For black holes, we just use the numbers in the top-level cells */
       count_bparts_in += e->proxies[k].cells_in[j]->black_holes.count;
+
+      /* For sinks, we just use the numbers in the top-level cells + some
+         extra space */
+      count_sinks_in +=
+          e->proxies[k].cells_in[j]->sinks.count + space_extra_sinks;
     }
   }
 
@@ -781,12 +819,6 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
     error(
         "Not running with hydro but about to receive gas particles in "
         "proxies!");
-  if (!with_stars && count_sparts_in)
-    error("Not running with stars but about to receive stars in proxies!");
-  if (!with_black_holes && count_bparts_in)
-    error(
-        "Not running with black holes but about to receive black holes in "
-        "proxies!");
 
   if (e->verbose)
     message("Counting number of foreign particles took %.3f %s.",
@@ -826,11 +858,15 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
                        spart_align,
                        sizeof(struct spart) * s->size_sparts_foreign) != 0)
       error("Failed to allocate foreign spart data.");
+
+#ifdef SWIFT_DEBUG_CHECKS
     bzero(s->sparts_foreign, s->size_sparts_foreign * sizeof(struct spart));
+
     for (size_t i = 0; i < s->size_sparts_foreign; ++i) {
       s->sparts_foreign[i].time_bin = time_bin_not_created;
       s->sparts_foreign[i].id = -43;
     }
+#endif
   }
 
   /* Allocate space for the foreign particles we will receive */
@@ -845,28 +881,52 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
       error("Failed to allocate foreign bpart data.");
   }
 
+  /* Allocate space for the foreign particles we will receive */
+  size_t old_size_sinks_foreign = s->size_sinks_foreign;
+  if (!fof && count_sinks_in > s->size_sinks_foreign) {
+    if (s->sinks_foreign != NULL) swift_free("sinks_foreign", s->sinks_foreign);
+    s->size_sinks_foreign = engine_foreign_alloc_margin * count_sinks_in;
+    if (swift_memalign("sinks_foreign", (void **)&s->sinks_foreign, sink_align,
+                       sizeof(struct sink) * s->size_sinks_foreign) != 0)
+      error("Failed to allocate foreign sink data.");
+
+#ifdef SWIFT_DEBUG_CHECKS
+    bzero(s->sinks_foreign, s->size_sinks_foreign * sizeof(struct sink));
+
+    /* Note: If you ever see a sink particle with id = -666, the following
+       lines is the ones that sets the ID to this value. */
+    for (size_t i = 0; i < s->size_sinks_foreign; ++i) {
+      s->sinks_foreign[i].time_bin = time_bin_not_created;
+      s->sinks_foreign[i].id = -666;
+    }
+#endif
+  }
+
   if (e->verbose) {
     message(
-        "Allocating %zd/%zd/%zd/%zd foreign part/gpart/spart/bpart "
-        "(%zd/%zd/%zd/%zd MB)",
+        "Allocating %zd/%zd/%zd/%zd/%zd foreign part/gpart/spart/bpart/sink "
+        "(%zd/%zd/%zd/%zd/%zd MB)",
         s->size_parts_foreign, s->size_gparts_foreign, s->size_sparts_foreign,
-        s->size_bparts_foreign,
+        s->size_bparts_foreign, s->size_sinks_foreign,
         s->size_parts_foreign * sizeof(struct part) / (1024 * 1024),
         s->size_gparts_foreign * sizeof(struct gpart) / (1024 * 1024),
         s->size_sparts_foreign * sizeof(struct spart) / (1024 * 1024),
-        s->size_bparts_foreign * sizeof(struct bpart) / (1024 * 1024));
+        s->size_bparts_foreign * sizeof(struct bpart) / (1024 * 1024),
+        s->size_sinks_foreign * sizeof(struct sink) / (1024 * 1024));
 
     if ((s->size_parts_foreign - old_size_parts_foreign) > 0 ||
         (s->size_gparts_foreign - old_size_gparts_foreign) > 0 ||
         (s->size_sparts_foreign - old_size_sparts_foreign) > 0 ||
-        (s->size_bparts_foreign - old_size_bparts_foreign) > 0) {
+        (s->size_bparts_foreign - old_size_bparts_foreign) > 0 ||
+        (s->size_sinks_foreign - old_size_sinks_foreign) > 0) {
       message(
-          "Re-allocations %zd/%zd/%zd/%zd part/gpart/spart/bpart "
-          "(%zd/%zd/%zd/%zd MB)",
+          "Re-allocations %zd/%zd/%zd/%zd/%zd part/gpart/spart/bpart/sink "
+          "(%zd/%zd/%zd/%zd/%zd MB)",
           (s->size_parts_foreign - old_size_parts_foreign),
           (s->size_gparts_foreign - old_size_gparts_foreign),
           (s->size_sparts_foreign - old_size_sparts_foreign),
           (s->size_bparts_foreign - old_size_bparts_foreign),
+          (s->size_sinks_foreign - old_size_sinks_foreign),
           (s->size_parts_foreign - old_size_parts_foreign) *
               sizeof(struct part) / (1024 * 1024),
           (s->size_gparts_foreign - old_size_gparts_foreign) *
@@ -874,15 +934,24 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
           (s->size_sparts_foreign - old_size_sparts_foreign) *
               sizeof(struct spart) / (1024 * 1024),
           (s->size_bparts_foreign - old_size_bparts_foreign) *
-              sizeof(struct bpart) / (1024 * 1024));
+              sizeof(struct bpart) / (1024 * 1024),
+          (s->size_sinks_foreign - old_size_sinks_foreign) *
+              sizeof(struct sink) / (1024 * 1024));
     }
   }
 
+  if (e->verbose)
+    message("Allocating and zeroing arrays took %.3f %s.",
+            clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  tic = getticks();
+
   /* Unpack the cells and link to the particle data. */
   struct part *parts = s->parts_foreign;
   struct gpart *gparts = s->gparts_foreign;
   struct spart *sparts = s->sparts_foreign;
   struct bpart *bparts = s->bparts_foreign;
+  struct sink *sinks = s->sinks_foreign;
   for (int k = 0; k < nr_proxies; k++) {
     for (int j = 0; j < e->proxies[k].nr_cells_in; j++) {
 
@@ -914,6 +983,14 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
         cell_link_bparts(e->proxies[k].cells_in[j], bparts);
         bparts = &bparts[e->proxies[k].cells_in[j]->black_holes.count];
       }
+
+      if (!fof && with_sinks) {
+
+        /* For sinks, we just use the numbers in the top-level cells */
+        cell_link_sinks(e->proxies[k].cells_in[j], sinks);
+        sinks =
+            &sinks[e->proxies[k].cells_in[j]->sinks.count + space_extra_sinks];
+      }
     }
   }
 
@@ -922,6 +999,7 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof) {
   s->nr_gparts_foreign = gparts - s->gparts_foreign;
   s->nr_sparts_foreign = sparts - s->sparts_foreign;
   s->nr_bparts_foreign = bparts - s->bparts_foreign;
+  s->nr_sinks_foreign = sinks - s->sinks_foreign;
 
   if (e->verbose)
     message("Recursively linking foreign arrays took %.3f %s.",
@@ -1021,6 +1099,19 @@ void engine_print_task_counts(const struct engine *e) {
   message("nr_sparts = %zu.", e->s->nr_sparts);
   message("nr_bparts = %zu.", e->s->nr_bparts);
 
+#if defined(SWIFT_DEBUG_CHECKS) && defined(WITH_MPI)
+  if (e->verbose == 2) {
+    /* check that the global number of sends matches the global number of
+       recvs */
+    int global_counts[2] = {counts[task_type_send], counts[task_type_recv]};
+    MPI_Allreduce(MPI_IN_PLACE, global_counts, 2, MPI_INT, MPI_SUM,
+                  MPI_COMM_WORLD);
+    if (global_counts[0] != global_counts[1])
+      error("Missing communications (%i sends, %i recvs)!", global_counts[0],
+            global_counts[1]);
+  }
+#endif
+
   if (e->verbose)
     message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
             clocks_getunit());
@@ -1109,8 +1200,15 @@ int engine_estimate_nr_tasks(const struct engine *e) {
 #endif
   }
   if (e->policy & engine_policy_sinks) {
-    /* 1 drift, 2 kicks, 1 time-step, 1 sink formation */
-    n1 += 5;
+    /* 1 drift, 2 kicks, 1 time-step, 1 sink formation     | 5
+       density: 1 self + 13 pairs                          | 14
+       swallow: 1 self + 13 pairs                          | 14
+       do_gas_swallow: 1 self + 13 pairs                   | 14
+       do_sink_swallow: 1 self + 13 pairs                  | 14
+       ghosts: density_ghost, sink_ghost_1, sink_ghost_2   | 3
+       implicit: sink_in,  sink_out                        | 2 */
+    n1 += 66;
+    n2 += 3;
     if (e->policy & engine_policy_stars) {
       /* 1 star formation */
       n1 += 1;
@@ -1135,6 +1233,35 @@ int engine_estimate_nr_tasks(const struct engine *e) {
     n1 += 39;
 #ifdef WITH_MPI
     n1 += 2;
+#endif
+  }
+  if (e->policy & engine_policy_grid) {
+    /* Grid construction: 1 self + 26 (asymmetric) pairs + 1 ghost + 1 sort */
+    n1 += 29;
+    n2 += 3;
+#ifdef WITH_MPI
+    n1 += 3;
+#endif
+  }
+  if (e->policy & engine_policy_grid_hydro) {
+    /* slope estimate: 1 self + 13 pairs (on average)       |   14
+     * others: 1 ghosts, 2 kicks, 1 drift, 1 timestep       | +  7
+     * Total:                                               =   21 */
+    n1 += 21;
+    n2 += 2;
+#ifdef EXTRA_HYDRO_LOOP
+    /* slope limiter: 1 self + 13 pairs                     | + 14
+     * flux: 1 self + 13 pairs                              | + 14
+     * others: 2 ghost.                                     | +  2
+     * Total:                                               =   30  */
+    n1 += 30;
+    n2 += 3;
+#endif
+#ifdef WITH_MPI
+    n1 += 1;
+#ifdef EXTRA_HYDRO_LOOP
+    n1 += 1;
+#endif
 #endif
   }
 
@@ -1150,7 +1277,8 @@ int engine_estimate_nr_tasks(const struct engine *e) {
     struct cell *c = &e->s->cells_top[k];
 
     /* Any cells with particles will have tasks (local & foreign). */
-    int nparts = c->hydro.count + c->grav.count + c->stars.count;
+    int nparts = c->hydro.count + c->grav.count + c->stars.count +
+                 c->black_holes.count + c->sinks.count;
     if (nparts > 0) {
       ntop++;
       ncells++;
@@ -1286,7 +1414,7 @@ void engine_rebuild(struct engine *e, const int repartitioned,
                 MPI_COMM_WORLD);
   MPI_Allreduce(MPI_IN_PLACE, &e->s->max_softening, 1, MPI_FLOAT, MPI_MAX,
                 MPI_COMM_WORLD);
-  MPI_Allreduce(MPI_IN_PLACE, &e->s->max_mpole_power,
+  MPI_Allreduce(MPI_IN_PLACE, e->s->max_mpole_power,
                 SELF_GRAVITY_MULTIPOLE_ORDER + 1, MPI_FLOAT, MPI_MAX,
                 MPI_COMM_WORLD);
 #endif
@@ -1312,6 +1440,12 @@ void engine_rebuild(struct engine *e, const int repartitioned,
   /* Initial cleaning up session ? */
   if (clean_smoothing_length_values) space_sanitize(e->s);
 
+  /* Set the initial completeness flag for the moving mesh (before exchange) */
+  if (e->policy & engine_policy_grid) {
+    cell_grid_set_self_completeness_mapper(e->s->cells_top, e->s->nr_cells,
+                                           NULL);
+  }
+
 /* If in parallel, exchange the cell structure, top-level and neighbouring
  * multipoles. To achieve this, free the foreign particle buffers first. */
 #ifdef WITH_MPI
@@ -1335,7 +1469,26 @@ void engine_rebuild(struct engine *e, const int repartitioned,
     if (counter != e->total_nr_gparts)
       error("Total particles in multipoles inconsistent with engine");
   }
+  if (e->policy & engine_policy_grid) {
+    for (int i = 0; i < e->s->nr_cells; i++) {
+      const struct cell *ci = &e->s->cells_top[i];
+      if (ci->hydro.count > 0 && !(ci->grid.self_completeness == grid_complete))
+        error("Encountered incomplete top level cell!");
+    }
+  }
+#endif
+
+  /* Set the grid construction level, is needed before splitting the tasks */
+  if (e->policy & engine_policy_grid) {
+    /* Set the completeness and construction level */
+    threadpool_map(&e->threadpool, cell_set_grid_completeness_mapper, NULL,
+                   e->s->nr_cells, 1, threadpool_auto_chunk_size, e);
+    threadpool_map(&e->threadpool, cell_set_grid_construction_level_mapper,
+                   NULL, e->s->nr_cells, 1, threadpool_auto_chunk_size, e);
+#ifdef WITH_MPI
+    engine_exchange_grid_extra(e);
 #endif
+  }
 
   /* Re-build the tasks. */
   engine_maketasks(e);
@@ -1367,9 +1520,9 @@ void engine_rebuild(struct engine *e, const int repartitioned,
   space_check_unskip_flags(e->s);
 #endif
 
-  /* Run through the tasks and mark as skip or not. */
-  if (engine_marktasks(e))
-    error("engine_marktasks failed after space_rebuild.");
+  /* Run through the cells, and their tasks to mark as unskipped. */
+  engine_unskip(e);
+  if (e->forcerebuild) error("engine_unskip faled after a rebuild!");
 
   /* Print the status of the system */
   if (e->verbose) engine_print_task_counts(e);
@@ -1648,6 +1801,7 @@ void engine_skip_force_and_kick(struct engine *e) {
         t->subtype == task_subtype_part_prep1 ||
         t->subtype == task_subtype_spart_prep2 ||
         t->subtype == task_subtype_sf_counts ||
+        t->subtype == task_subtype_grav_counts ||
         t->subtype == task_subtype_rt_gradient ||
         t->subtype == task_subtype_rt_transport)
       t->skip = 1;
@@ -1731,6 +1885,10 @@ void engine_launch(struct engine *e, const char *call) {
   e->sched.deadtime.active_ticks += active_time;
   e->sched.deadtime.waiting_ticks += getticks() - tic;
 
+#ifdef SWIFT_DEBUG_CHECKS
+  e->sched.last_successful_task_fetch = 0LL;
+#endif
+
   if (e->verbose)
     message("(%s) took %.3f %s.", call, clocks_from_ticks(getticks() - tic),
             clocks_getunit());
@@ -1772,6 +1930,47 @@ void engine_get_max_ids(struct engine *e) {
 #endif
 }
 
+/**
+ * @brief Gather the information about the top-level cells whose time-step has
+ * changed and activate the communications required to synchonize the
+ * time-steps.
+ *
+ * @param e The #engine.
+ */
+void engine_synchronize_times(struct engine *e) {
+
+#ifdef WITH_MPI
+
+  const ticks tic = getticks();
+
+  /* Collect which top-level cells have been updated */
+  MPI_Allreduce(MPI_IN_PLACE, e->s->cells_top_updated, e->s->nr_cells, MPI_CHAR,
+                MPI_SUM, MPI_COMM_WORLD);
+
+  /* Activate tend communications involving the cells that have changed. */
+  for (int i = 0; i < e->s->nr_cells; ++i) {
+
+    if (e->s->cells_top_updated[i]) {
+
+      struct cell *c = &e->s->cells_top[i];
+      scheduler_activate_all_subtype(&e->sched, c->mpi.send, task_subtype_tend);
+      scheduler_activate_all_subtype(&e->sched, c->mpi.recv, task_subtype_tend);
+    }
+  }
+
+  if (e->verbose)
+    message("Gathering and activating tend took %.3f %s.",
+            clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  TIMER_TIC;
+  engine_launch(e, "tend");
+  TIMER_TOC(timer_runners);
+
+#else
+  error("SWIFT was not compiled with MPI support.");
+#endif
+}
+
 /**
  * @brief Run the radiative transfer sub-cycles outside the
  * regular time-steps.
@@ -1782,6 +1981,11 @@ void engine_run_rt_sub_cycles(struct engine *e) {
 
   /* Do we have work to do? */
   if (!(e->policy & engine_policy_rt)) return;
+
+  /* Note that if running without sub-cycles, no RT-specific timestep data will
+   * be written to screen or to the RT subcycles timestep data file. It's
+   * meaningless to do so, as all data will already be contained in the normal
+   * timesteps file. */
   if (e->max_nr_rt_subcycles <= 1) return;
 
   /* Get the subcycling step */
@@ -1816,20 +2020,47 @@ void engine_run_rt_sub_cycles(struct engine *e) {
   /* Get some time variables for printouts. Don't update the ones in the
    * engine like in the regular step, or the outputs in the regular steps
    * will be wrong. */
-  /* think cosmology one day: needs adapting here */
-  if (e->policy & engine_policy_cosmology)
-    error("Can't run RT subcycling with cosmology yet");
-  const double dt_subcycle = rt_step_size * e->time_base;
-  double time = e->ti_current_subcycle * e->time_base + e->time_begin;
+  double dt_subcycle;
+  if (e->policy & engine_policy_cosmology) {
+    dt_subcycle =
+        cosmology_get_delta_time(e->cosmology, e->ti_current, e->ti_rt_end_min);
+  } else {
+    dt_subcycle = rt_step_size * e->time_base;
+  }
+  double time = e->time;
+
+  /* Keep track and accumulate the deadtime over all sub-cycles. */
+  /* We need to manually put this back in the engine struct when
+   * the sub-cycling is completed. */
+  double global_deadtime_acc = e->global_deadtime;
 
   /* Collect and print info before it's gone */
   engine_collect_end_of_sub_cycle(e);
+
   if (e->nodeID == 0) {
+
     printf(
-        "  %6d cycle   0 (during regular tasks) dt=%14e "
-        "min/max active bin=%2d/%2d rt_updates=%18lld\n",
-        e->step, dt_subcycle, e->min_active_bin_subcycle,
-        e->max_active_bin_subcycle, e->rt_updates);
+        " [rt-sc] %-4d %12e %11.6f %11.6f %13e %4d %4d %12lld %12s %12s "
+        "%12s %12s %21s %6s %17s\n",
+        0, e->time, e->cosmology->a, e->cosmology->z, dt_subcycle,
+        e->min_active_bin_subcycle, e->max_active_bin_subcycle, e->rt_updates,
+        /*g, s, sink, bh updates=*/"-", "-", "-", "-", /*wallclock_time=*/"-",
+        /*props=*/"-", /*dead_time=*/"-");
+#ifdef SWIFT_DEBUG_CHECKS
+    fflush(stdout);
+#endif
+
+    if (!e->restarting) {
+      fprintf(
+          e->file_rt_subcycles,
+          "  %6d %9d %14e %12.7f %12.7f %14e %4d %4d %12lld %21.3f %17.3f\n",
+          e->step, 0, time, e->cosmology->a, e->cosmology->z, dt_subcycle,
+          e->min_active_bin_subcycle, e->max_active_bin_subcycle, e->rt_updates,
+          /*wall-clock time=*/-1.f, /*deadtime=*/-1.f);
+    }
+#ifdef SWIFT_DEBUG_CHECKS
+    fflush(e->file_rt_subcycles);
+#endif
   }
 
   /* Take note of the (integer) time until which the radiative transfer
@@ -1841,32 +2072,87 @@ void engine_run_rt_sub_cycles(struct engine *e) {
 
   for (int sub_cycle = 1; sub_cycle < nr_rt_cycles; ++sub_cycle) {
 
+    /* Keep track of the wall-clock time of each additional sub-cycle. */
+    struct clocks_time time1, time2;
+    clocks_gettime(&time1);
+
+    /* reset the deadtime information in the scheduler */
+    e->sched.deadtime.active_ticks = 0;
+    e->sched.deadtime.waiting_ticks = 0;
+
+    /* Set and re-set times, bins, etc. */
     e->rt_updates = 0ll;
     integertime_t ti_subcycle_old = e->ti_current_subcycle;
     e->ti_current_subcycle = e->ti_current + sub_cycle * rt_step_size;
     e->max_active_bin_subcycle = get_max_active_bin(e->ti_current_subcycle);
     e->min_active_bin_subcycle =
         get_min_active_bin(e->ti_current_subcycle, ti_subcycle_old);
-    /* think cosmology one day: needs adapting here */
-    if (e->policy & engine_policy_cosmology)
-      error("Can't run RT subcycling with cosmology yet");
-    time = e->ti_current_subcycle * e->time_base + e->time_begin;
+
+    /* Update rt properties */
+    rt_props_update(e->rt_props, e->internal_units, e->cosmology);
+
+    if (e->policy & engine_policy_cosmology) {
+      double time_old = time;
+      cosmology_update(
+          e->cosmology, e->physical_constants,
+          e->ti_current_subcycle);  // Update cosmological parameters
+      time = e->cosmology->time;    // Grab new cosmology time
+      dt_subcycle = time - time_old;
+    } else {
+      time = e->ti_current_subcycle * e->time_base + e->time_begin;
+    }
 
     /* Do the actual work now. */
     engine_unskip_rt_sub_cycle(e);
+    TIMER_TIC;
     engine_launch(e, "cycles");
+    TIMER_TOC(timer_runners);
+
+    /* Compute the local accumulated deadtime. */
+    const ticks deadticks = (e->nr_threads * e->sched.deadtime.waiting_ticks) -
+                            e->sched.deadtime.active_ticks;
+    e->local_deadtime = clocks_from_ticks(deadticks);
 
     /* Collect number of updates and print */
     engine_collect_end_of_sub_cycle(e);
 
+    /* Add our sub-cycling deadtime. */
+    global_deadtime_acc += e->global_deadtime;
+
+    /* Keep track how far we have integrated over. */
     rt_integration_end += rt_step_size;
 
     if (e->nodeID == 0) {
+
+      const double dead_time =
+          e->global_deadtime / (e->nr_nodes * e->nr_threads);
+
+      /* engine_step() stores the wallclock time in the engine struct.
+       * Don't do that here - we want the full step to include the full
+       * duration of the step, which includes all sub-cycles. (Also it
+       * would be overwritten anyway.) */
+      clocks_gettime(&time2);
+      const float wallclock_time = (float)clocks_diff(&time1, &time2);
+
       printf(
-          "  %6d cycle %3d time=%13.6e     dt=%14e "
-          "min/max active bin=%2d/%2d rt_updates=%18lld\n",
-          e->step, sub_cycle, time, dt_subcycle, e->min_active_bin_subcycle,
-          e->max_active_bin_subcycle, e->rt_updates);
+          " [rt-sc] %-4d %12e %11.6f %11.6f %13e %4d %4d %12lld %12s %12s "
+          "%12s %12s %21.3f %6s %17.3f\n",
+          sub_cycle, time, e->cosmology->a, e->cosmology->z, dt_subcycle,
+          e->min_active_bin_subcycle, e->max_active_bin_subcycle, e->rt_updates,
+          /*g, s, sink, bh updates=*/"-", "-", "-", "-", wallclock_time,
+          /*props=*/"-", dead_time);
+#ifdef SWIFT_DEBUG_CHECKS
+      fflush(stdout);
+#endif
+      fprintf(
+          e->file_rt_subcycles,
+          "  %6d %9d %14e %12.7f %12.7f %14e %4d %4d %12lld %21.3f %17.3f\n",
+          e->step, sub_cycle, time, e->cosmology->a, e->cosmology->z,
+          dt_subcycle, e->min_active_bin_subcycle, e->max_active_bin_subcycle,
+          e->rt_updates, wallclock_time, dead_time);
+#ifdef SWIFT_DEBUG_CHECKS
+      fflush(e->file_rt_subcycles);
+#endif
     }
   }
 
@@ -1879,6 +2165,7 @@ void engine_run_rt_sub_cycles(struct engine *e) {
 
   /* Once we're done, clean up after ourselves */
   e->rt_updates = 0ll;
+  e->global_deadtime = global_deadtime_acc;
 }
 
 /**
@@ -1915,6 +2202,10 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
   if (e->nodeID == 0) message("Setting particles to a valid state...");
   engine_first_init_particles(e);
 
+  /* Initialise the particle splitting mechanism */
+  if (e->hydro_properties->particle_splitting)
+    engine_init_split_gas_particles(e);
+
   if (e->nodeID == 0)
     message("Computing initial gas densities and approximate gravity.");
 
@@ -1949,8 +2240,11 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
   /* Update the cooling function */
   if ((e->policy & engine_policy_cooling) ||
       (e->policy & engine_policy_temperature))
-    cooling_update(e->cosmology, e->pressure_floor_props, e->cooling_func,
-                   e->s);
+    cooling_update(e->physical_constants, e->cosmology, e->pressure_floor_props,
+                   e->cooling_func, e->s, e->time);
+
+  if (e->policy & engine_policy_rt)
+    rt_props_update(e->rt_props, e->internal_units, e->cosmology);
 
 #ifdef WITH_CSDS
   if (e->policy & engine_policy_csds) {
@@ -1969,6 +2263,9 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
   }
 #endif
 
+  /* Zero the list of cells that have had their time-step updated */
+  bzero(e->s->cells_top_updated, e->s->nr_cells * sizeof(char));
+
   /* Now, launch the calculation */
   TIMER_TIC;
   engine_launch(e, "tasks");
@@ -1993,12 +2290,15 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
 
     /* Correct what we did (e.g. in PE-SPH, need to recompute rho_bar) */
     if (hydro_need_extra_init_loop) {
-      engine_marktasks(e);
+      engine_unskip(e);
       engine_skip_force_and_kick(e);
       engine_launch(e, "tasks");
     }
   }
 
+  /* Do some post initialisations */
+  space_post_init_parts(e->s, e->verbose);
+
   /* Apply some RT conversions (e.g. energy -> energy density) */
   if (e->policy & engine_policy_rt)
     space_convert_rt_quantities(e->s, e->verbose);
@@ -2054,11 +2354,19 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
   scheduler_write_cell_dependencies(&e->sched, e->verbose, e->step);
   if (e->nodeID == 0) scheduler_write_task_level(&e->sched, e->step);
 
+  /* Zero the list of cells that have had their time-step updated */
+  bzero(e->s->cells_top_updated, e->s->nr_cells * sizeof(char));
+
   /* Run the 0th time-step */
   TIMER_TIC2;
   engine_launch(e, "tasks");
   TIMER_TOC2(timer_runners);
 
+  /* When running over MPI, synchronize top-level cells */
+#ifdef WITH_MPI
+  engine_synchronize_times(e);
+#endif
+
 #ifdef SWIFT_HYDRO_DENSITY_CHECKS
   /* Run the brute-force hydro calculation for some parts */
   if (e->policy & engine_policy_hydro)
@@ -2078,6 +2386,15 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
     stars_exact_density_check(e->s, e, /*rel_tol=*/1e-3);
 #endif
 
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  /* Run the brute-force sink calculation for some sinks */
+  if (e->policy & engine_policy_sinks) sink_exact_density_compute(e->s, e);
+
+  /* Check the accuracy of the sink calculation */
+  if (e->policy & engine_policy_sinks)
+    sink_exact_density_check(e->s, e, /*rel_tol=*/1e-3);
+#endif
+
 #ifdef SWIFT_GRAVITY_FORCE_CHECKS
   /* Check the accuracy of the gravity calculation */
   if (e->policy & engine_policy_self_gravity)
@@ -2190,12 +2507,12 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
     for (int i = 0; i < s->nr_cells; i++) {
       struct cell *c = &s->cells_top[i];
       if (c->nodeID == engine_rank && c->sinks.count > 0) {
-        float sink_h_max = c->sinks.parts[0].r_cut;
+        float sink_h_max = c->sinks.parts[0].h;
         for (int k = 1; k < c->sinks.count; k++) {
-          if (c->sinks.parts[k].r_cut > sink_h_max)
-            sink_h_max = c->sinks.parts[k].r_cut;
+          if (c->sinks.parts[k].h > sink_h_max)
+            sink_h_max = c->sinks.parts[k].h;
         }
-        c->sinks.r_cut_max = max(sink_h_max, c->sinks.r_cut_max);
+        c->sinks.h_max = max(sink_h_max, c->sinks.h_max);
       }
     }
   }
@@ -2370,8 +2687,8 @@ int engine_step(struct engine *e) {
   /* Update the cooling function */
   if ((e->policy & engine_policy_cooling) ||
       (e->policy & engine_policy_temperature))
-    cooling_update(e->cosmology, e->pressure_floor_props, e->cooling_func,
-                   e->s);
+    cooling_update(e->physical_constants, e->cosmology, e->pressure_floor_props,
+                   e->cooling_func, e->s, e->time);
 
   /* Update the softening lengths */
   if (e->policy & engine_policy_self_gravity)
@@ -2382,6 +2699,10 @@ int engine_step(struct engine *e) {
     hydro_props_update(e->hydro_properties, e->gravity_properties,
                        e->cosmology);
 
+  /* Update the rt properties */
+  if (e->policy & engine_policy_rt)
+    rt_props_update(e->rt_props, e->internal_units, e->cosmology);
+
   /* Check for any snapshot triggers */
   engine_io_check_snapshot_triggers(e);
 
@@ -2579,11 +2900,19 @@ int engine_step(struct engine *e) {
      want to lose the data from the tasks) */
   space_reset_ghost_histograms(e->s);
 
+  /* Zero the list of cells that have had their time-step updated */
+  bzero(e->s->cells_top_updated, e->s->nr_cells * sizeof(char));
+
   /* Start all the tasks. */
   TIMER_TIC;
   engine_launch(e, "tasks");
   TIMER_TOC(timer_runners);
 
+  /* When running over MPI, synchronize top-level cells */
+#ifdef WITH_MPI
+  engine_synchronize_times(e);
+#endif
+
   /* Now record the CPU times used by the tasks. */
 #ifdef WITH_MPI
   double end_usertime = 0.0;
@@ -2612,6 +2941,15 @@ int engine_step(struct engine *e) {
     stars_exact_density_check(e->s, e, /*rel_tol=*/1e-2);
 #endif
 
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  /* Run the brute-force sink calculation for some sinks */
+  if (e->policy & engine_policy_sinks) sink_exact_density_compute(e->s, e);
+
+  /* Check the accuracy of the sink calculation */
+  if (e->policy & engine_policy_sinks)
+    sink_exact_density_check(e->s, e, /*rel_tol=*/1e-2);
+#endif
+
 #ifdef SWIFT_GRAVITY_FORCE_CHECKS
   /* Check if we want to run force checks this timestep. */
   if (e->policy & engine_policy_self_gravity) {
@@ -2920,8 +3258,9 @@ void engine_pin(void) {
   threadpool_set_affinity_mask(entry_affinity);
 
   int pin;
-  for (pin = 0; pin < CPU_SETSIZE && !CPU_ISSET(pin, entry_affinity); ++pin)
-    ;
+  for (pin = 0; pin < CPU_SETSIZE && !CPU_ISSET(pin, entry_affinity); ++pin) {
+    /* Nothing to do here */
+  }
 
   cpu_set_t affinity;
   CPU_ZERO(&affinity);
@@ -3058,6 +3397,7 @@ void engine_init(
     struct pressure_floor_props *pressure_floor, struct rt_props *rt,
     struct pm_mesh *mesh, struct power_spectrum_data *pow_data,
     const struct external_potential *potential,
+    const struct forcing_terms *forcing_terms,
     struct cooling_function_data *cooling_func,
     const struct star_formation *starform,
     const struct chemistry_global_data *chemistry,
@@ -3137,8 +3477,12 @@ void engine_init(
       parser_get_opt_param_int(params, "Snapshots:compression", 0);
   e->snapshot_distributed =
       parser_get_opt_param_int(params, "Snapshots:distributed", 0);
-  e->snapshot_lustre_OST_count =
-      parser_get_opt_param_int(params, "Snapshots:lustre_OST_count", 0);
+  e->snapshot_lustre_OST_checks =
+      parser_get_opt_param_int(params, "Snapshots:lustre_OST_checks", 0);
+  e->snapshot_lustre_OST_free =
+      parser_get_opt_param_int(params, "Snapshots:lustre_OST_free", 0);
+  e->snapshot_lustre_OST_test =
+      parser_get_opt_param_int(params, "Snapshots:lustre_OST_test", 0);
   e->snapshot_invoke_stf =
       parser_get_opt_param_int(params, "Snapshots:invoke_stf", 0);
   e->snapshot_invoke_fof =
@@ -3198,6 +3542,7 @@ void engine_init(
   e->mesh = mesh;
   e->power_data = pow_data;
   e->external_potential = potential;
+  e->forcing_terms = forcing_terms;
   e->cooling_func = cooling_func;
   e->star_formation = starform;
   e->feedback_props = feedback;
@@ -3506,6 +3851,9 @@ void engine_recompute_displacement_constraint(struct engine *e) {
 
     /* Apply the dimensionless factor */
     e->dt_max_RMS_displacement = dt * e->max_RMS_displacement_factor;
+    if (e->dt_max_RMS_displacement == 0.f) {
+      error("Setting dt_max_RMS_displacement to 0!");
+    }
 
     if (e->verbose)
       message("max_dt_RMS_displacement = %e", e->dt_max_RMS_displacement);
@@ -3618,7 +3966,7 @@ void engine_clean(struct engine *e, const int fof, const int restart) {
   stats_free_mpi_type();
   proxy_free_mpi_type();
   task_free_mpi_comms();
-  mpicollect_free_MPI_type();
+  if (!fof) mpicollect_free_MPI_type();
 #endif
 
   /* Close files */
@@ -3629,6 +3977,10 @@ void engine_clean(struct engine *e, const int fof, const int restart) {
     if (e->policy & engine_policy_star_formation) {
       fclose(e->sfh_logger);
     }
+
+#ifndef RT_NONE
+    fclose(e->file_rt_subcycles);
+#endif
   }
 
   /* If the run was restarted, we should also free the memory allocated
@@ -3637,6 +3989,7 @@ void engine_clean(struct engine *e, const int fof, const int restart) {
     free((void *)e->parameter_file);
     free((void *)e->output_options);
     free((void *)e->external_potential);
+    free((void *)e->forcing_terms);
     free((void *)e->black_holes_properties);
     free((void *)e->pressure_floor_props);
     free((void *)e->rt_props);
@@ -3711,6 +4064,7 @@ void engine_struct_dump(struct engine *e, FILE *stream) {
   pm_mesh_struct_dump(e->mesh, stream);
   power_spectrum_struct_dump(e->power_data, stream);
   potential_struct_dump(e->external_potential, stream);
+  forcing_terms_struct_dump(e->forcing_terms, stream);
   cooling_struct_dump(e->cooling_func, stream);
   starformation_struct_dump(e->star_formation, stream);
   feedback_struct_dump(e->feedback_props, stream);
@@ -3826,6 +4180,11 @@ void engine_struct_restore(struct engine *e, FILE *stream) {
   potential_struct_restore(external_potential, stream);
   e->external_potential = external_potential;
 
+  struct forcing_terms *forcing_terms =
+      (struct forcing_terms *)malloc(sizeof(struct forcing_terms));
+  forcing_terms_struct_restore(forcing_terms, stream);
+  e->forcing_terms = forcing_terms;
+
   struct cooling_function_data *cooling_func =
       (struct cooling_function_data *)malloc(
           sizeof(struct cooling_function_data));
@@ -3851,7 +4210,7 @@ void engine_struct_restore(struct engine *e, FILE *stream) {
   struct rt_props *rt_properties =
       (struct rt_props *)malloc(sizeof(struct rt_props));
   rt_struct_restore(rt_properties, stream, e->physical_constants,
-                    e->internal_units);
+                    e->internal_units, cosmo);
   e->rt_props = rt_properties;
 
   struct black_holes_props *black_holes_properties =
diff --git a/src/engine.h b/src/engine.h
index 1aa0c304829908b2a70c9c53c7970da3931ff721..b0143a7d1ab541a1583d4b01e6ab16d7073466bf 100644
--- a/src/engine.h
+++ b/src/engine.h
@@ -54,6 +54,7 @@
 struct black_holes_properties;
 struct extra_io_properties;
 struct external_potential;
+struct forcing_terms;
 
 /**
  * @brief The different policies the #engine can follow.
@@ -88,8 +89,10 @@ enum engine_policy {
   engine_policy_sinks = (1 << 25),
   engine_policy_rt = (1 << 26),
   engine_policy_power_spectra = (1 << 27),
+  engine_policy_grid = (1 << 28),
+  engine_policy_grid_hydro = (1 << 29),
 };
-#define engine_maxpolicy 28
+#define engine_maxpolicy 30
 extern const char *engine_policy_names[engine_maxpolicy + 1];
 
 /**
@@ -119,6 +122,7 @@ enum engine_step_properties {
 #define engine_foreign_alloc_margin_default 1.05
 #define engine_default_energy_file_name "statistics"
 #define engine_default_timesteps_file_name "timesteps"
+#define engine_default_rt_subcycles_file_name "rtsubcycles"
 #define engine_max_parts_per_ghost_default 1000
 #define engine_max_sparts_per_ghost_default 1000
 #define engine_max_parts_per_cooling_default 10000
@@ -346,7 +350,9 @@ struct engine {
   float snapshot_subsample_fraction[swift_type_count];
   int snapshot_run_on_dump;
   int snapshot_distributed;
-  int snapshot_lustre_OST_count;
+  int snapshot_lustre_OST_checks;
+  int snapshot_lustre_OST_free;
+  int snapshot_lustre_OST_test;
   int snapshot_compression;
   int snapshot_invoke_stf;
   int snapshot_invoke_fof;
@@ -429,6 +435,9 @@ struct engine {
   /* File handle for the timesteps information */
   FILE *file_timesteps;
 
+  /* File handle for the Radiative Transfer sub-cycling information */
+  FILE *file_rt_subcycles;
+
   /* File handle for the SFH logger file */
   FILE *sfh_logger;
 
@@ -536,6 +545,9 @@ struct engine {
   /* Properties of external gravitational potential */
   const struct external_potential *external_potential;
 
+  /* Properties of the hydrodynamics forcing terms */
+  const struct forcing_terms *forcing_terms;
+
   /* Properties of the cooling scheme */
   struct cooling_function_data *cooling_func;
 
@@ -579,8 +591,16 @@ struct engine {
   /* Whether to dump restart files after the last step. */
   int restart_onexit;
 
-  /* Number of Lustre OSTs on the system to use as rank-based striping offset */
-  int restart_lustre_OST_count;
+  /* Perform OST checks and assign each restart file a stripe on the basis of
+   * most free space first. */
+  int restart_lustre_OST_checks;
+
+  /* Free space that an OST should have to be used, -1 makes this
+   * the rss size. In MiB so we can use an int and human sized. */
+  int restart_lustre_OST_free;
+
+  /* Whether to check is OSTs are writable, if not then they are not used. */
+  int restart_lustre_OST_test;
 
   /* Do we free the foreign data before writing restart files? */
   int free_foreign_when_dumping_restart;
@@ -674,7 +694,7 @@ struct engine {
 /* Function prototypes, engine.c. */
 void engine_addlink(struct engine *e, struct link **l, struct task *t);
 void engine_barrier(struct engine *e);
-void engine_compute_next_snapshot_time(struct engine *e);
+void engine_compute_next_snapshot_time(struct engine *e, const int restart);
 void engine_compute_next_stf_time(struct engine *e);
 void engine_compute_next_fof_time(struct engine *e);
 void engine_compute_next_statistics_time(struct engine *e);
@@ -692,7 +712,7 @@ void engine_io(struct engine *e);
 void engine_io_check_snapshot_triggers(struct engine *e);
 void engine_collect_end_of_step(struct engine *e, int apply);
 void engine_collect_end_of_sub_cycle(struct engine *e);
-void engine_dump_snapshot(struct engine *e);
+void engine_dump_snapshot(struct engine *e, const int fof);
 void engine_run_on_dump(struct engine *e);
 void engine_init_output_lists(struct engine *e, struct swift_params *params,
                               const struct output_options *output_options);
@@ -713,6 +733,7 @@ void engine_init(
     struct pressure_floor_props *pressure_floor, struct rt_props *rt,
     struct pm_mesh *mesh, struct power_spectrum_data *pow_data,
     const struct external_potential *potential,
+    const struct forcing_terms *forcing_terms,
     struct cooling_function_data *cooling_func,
     const struct star_formation *starform,
     const struct chemistry_global_data *chemistry,
@@ -738,7 +759,8 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
                             size_t *Ngpart, const size_t offset_sparts,
                             const int *ind_spart, size_t *Nspart,
                             const size_t offset_bparts, const int *ind_bpart,
-                            size_t *Nbpart);
+                            size_t *Nbpart, const size_t offset_sinks,
+                            const int *ind_sink, size_t *Nsink);
 void engine_rebuild(struct engine *e, int redistributed, int clean_h_values);
 void engine_repartition(struct engine *e);
 void engine_repartition_trigger(struct engine *e);
@@ -755,6 +777,7 @@ void engine_fof(struct engine *e, const int dump_results,
                 const int dump_debug_results, const int seed_black_holes,
                 const int foreign_buffers_allocated);
 void engine_activate_gpart_comms(struct engine *e);
+void engine_activate_fof_attach_tasks(struct engine *e);
 
 /* Function prototypes, engine_maketasks.c. */
 void engine_maketasks(struct engine *e);
@@ -762,11 +785,9 @@ void engine_maketasks(struct engine *e);
 /* Function prototypes, engine_maketasks.c. */
 void engine_make_fof_tasks(struct engine *e);
 
-/* Function prototypes, engine_marktasks.c. */
-int engine_marktasks(struct engine *e);
-
 /* Function prototypes, engine_split_particles.c. */
 void engine_split_gas_particles(struct engine *e);
+void engine_init_split_gas_particles(struct engine *e);
 
 #ifdef HAVE_SETAFFINITY
 cpu_set_t *engine_entry_affinity(void);
@@ -778,4 +799,7 @@ void engine_struct_dump(struct engine *e, FILE *stream);
 void engine_struct_restore(struct engine *e, FILE *stream);
 int engine_dump_restarts(struct engine *e, int drifted_all, int force);
 
+/* dev/debug */
+void engine_dump_diagnostic_data(struct engine *e);
+
 #endif /* SWIFT_ENGINE_H */
diff --git a/src/engine_collect_end_of_step.c b/src/engine_collect_end_of_step.c
index edca57333c939646e7e04fd22bf2ff2798e544b3..b97d87a73c329f895ea89103586ec57de8b64e22 100644
--- a/src/engine_collect_end_of_step.c
+++ b/src/engine_collect_end_of_step.c
@@ -405,14 +405,30 @@ void engine_collect_end_of_sub_cycle(struct engine *e) {
                  s->local_cells_top, s->nr_local_cells, sizeof(int),
                  threadpool_auto_chunk_size, e);
 
-  /* Aggregate collective data from the different nodes for this step. */
 #ifdef WITH_MPI
+
+  /* Aggregate collective data from the different nodes for this step. */
+  int test;
   long long rt_updates_tot = 0ll;
-  int test = MPI_Reduce(&e->rt_updates, &rt_updates_tot, 1, MPI_LONG_LONG,
-                        MPI_SUM, 0, MPI_COMM_WORLD);
+  test = MPI_Reduce(&e->rt_updates, &rt_updates_tot, 1, MPI_LONG_LONG, MPI_SUM,
+                    0, MPI_COMM_WORLD);
+  if (test != MPI_SUCCESS) error("MPI reduce failed");
+
+  double global_deadtime = 0.;
+  test = MPI_Reduce(&e->local_deadtime, &global_deadtime, 1, MPI_DOUBLE,
+                    MPI_SUM, 0, MPI_COMM_WORLD);
   if (test != MPI_SUCCESS) error("MPI reduce failed");
+
   /* Overwrite only on rank 0. */
-  if (e->nodeID == 0) e->rt_updates = rt_updates_tot;
+  if (e->nodeID == 0) {
+    e->rt_updates = rt_updates_tot;
+    e->global_deadtime = global_deadtime;
+  }
+
+#else
+
+  e->global_deadtime = e->local_deadtime;
+
 #endif
 
   if (e->verbose)
diff --git a/src/engine_config.c b/src/engine_config.c
index b34fe40da55669457df8d7ff6e61c7d74d58c4a8..bf5bef510ebe1ddaab3be36e0b4c18d4d6c4dd37 100644
--- a/src/engine_config.c
+++ b/src/engine_config.c
@@ -37,6 +37,7 @@
 
 /* Local headers. */
 #include "fof.h"
+#include "line_of_sight.h"
 #include "mpiuse.h"
 #include "part.h"
 #include "pressure_floor.h"
@@ -52,6 +53,31 @@ extern int engine_max_parts_per_ghost;
 extern int engine_max_sparts_per_ghost;
 extern int engine_max_parts_per_cooling;
 
+/**
+ * @brief dump diagnostic data on tasks, memuse, mpiuse, queues.
+ *
+ * @param e the #engine
+ */
+void engine_dump_diagnostic_data(struct engine *e) {
+  /* OK, do our work. */
+  message("Dumping engine tasks in step: %d", e->step);
+  task_dump_active(e);
+
+#ifdef SWIFT_MEMUSE_REPORTS
+  /* Dump the currently logged memory. */
+  message("Dumping memory use report");
+  memuse_log_dump_error(e->nodeID);
+#endif
+
+#if defined(SWIFT_MPIUSE_REPORTS) && defined(WITH_MPI)
+  /* Dump the MPI interactions in the step. */
+  mpiuse_log_dump_error(e->nodeID);
+#endif
+
+  /* Add more interesting diagnostics. */
+  scheduler_dump_queues(e);
+}
+
 /* Particle cache size. */
 #define CACHE_SIZE 512
 
@@ -75,23 +101,7 @@ static void *engine_dumper_poll(void *p) {
   while (1) {
     if (access(dumpfile, F_OK) == 0) {
 
-      /* OK, do our work. */
-      message("Dumping engine tasks in step: %d", e->step);
-      task_dump_active(e);
-
-#ifdef SWIFT_MEMUSE_REPORTS
-      /* Dump the currently logged memory. */
-      message("Dumping memory use report");
-      memuse_log_dump_error(e->nodeID);
-#endif
-
-#if defined(SWIFT_MPIUSE_REPORTS) && defined(WITH_MPI)
-      /* Dump the MPI interactions in the step. */
-      mpiuse_log_dump_error(e->nodeID);
-#endif
-
-      /* Add more interesting diagnostics. */
-      scheduler_dump_queues(e);
+      engine_dump_diagnostic_data(e);
 
       /* Delete the file. */
       unlink(dumpfile);
@@ -186,6 +196,7 @@ void engine_config(int restart, int fof, struct engine *e,
   e->nr_links = 0;
   e->file_stats = NULL;
   e->file_timesteps = NULL;
+  e->file_rt_subcycles = NULL;
   e->sfh_logger = NULL;
   e->verbose = verbose;
   e->wallclock_time = 0.f;
@@ -263,6 +274,13 @@ void engine_config(int restart, int fof, struct engine *e,
     error("Scheduler:task_level_output_frequency should be >= 0");
   }
 
+#if defined(SWIFT_DEBUG_CHECKS)
+  e->sched.deadlock_waiting_time_ms = parser_get_opt_param_float(
+      params, "Scheduler:deadlock_waiting_time_s", -1.f);
+  /* User provides parameter in s. We want it in ms. */
+  e->sched.deadlock_waiting_time_ms *= 1000.f;
+#endif
+
 /* Deal with affinity. For now, just figure out the number of cores. */
 #if defined(HAVE_SETAFFINITY)
   const int nr_cores = sysconf(_SC_NPROCESSORS_ONLN);
@@ -294,8 +312,9 @@ void engine_config(int restart, int fof, struct engine *e,
     int skip = 0;
     for (int k = 0; k < nr_affinity_cores; k++) {
       int c;
-      for (c = skip; c < CPU_SETSIZE && !CPU_ISSET(c, entry_affinity); ++c)
-        ;
+      for (c = skip; c < CPU_SETSIZE && !CPU_ISSET(c, entry_affinity); ++c) {
+        /* Nothing to do here */
+      }
       cpuid[k] = c;
       skip = c + 1;
     }
@@ -390,8 +409,8 @@ void engine_config(int restart, int fof, struct engine *e,
     /* Make sure the corresponding policy is set and make space for the proxies
      */
     e->policy |= engine_policy_mpi;
-    if ((e->proxies = (struct proxy *)calloc(sizeof(struct proxy),
-                                             engine_maxproxies)) == NULL)
+    if ((e->proxies = (struct proxy *)calloc(engine_maxproxies,
+                                             sizeof(struct proxy))) == NULL)
       error("Failed to allocate memory for proxies.");
     e->nr_proxies = 0;
 
@@ -477,6 +496,18 @@ void engine_config(int restart, int fof, struct engine *e,
       error("Could not open the file '%s' with mode '%s'.", timestepsfileName,
             mode);
 
+#ifndef RT_NONE
+    char rtSubcyclesFileName[200] = "";
+    parser_get_opt_param_string(params, "Statistics:rt_subcycles_file_name",
+                                rtSubcyclesFileName,
+                                engine_default_rt_subcycles_file_name);
+    sprintf(rtSubcyclesFileName + strlen(rtSubcyclesFileName), ".txt");
+    e->file_rt_subcycles = fopen(rtSubcyclesFileName, mode);
+    if (e->file_rt_subcycles == NULL)
+      error("Could not open the file '%s' with mode '%s'.", rtSubcyclesFileName,
+            mode);
+#endif
+
     if (!restart) {
       fprintf(
           e->file_timesteps,
@@ -509,6 +540,47 @@ void engine_config(int restart, int fof, struct engine *e,
               "b-Updates", "Wall-clock time", clocks_getunit(), "Props",
               "Dead time", clocks_getunit());
       fflush(e->file_timesteps);
+
+#ifndef RT_NONE
+      fprintf(
+          e->file_rt_subcycles,
+          "# Host: %s\n# Branch: %s\n# Revision: %s\n# Compiler: %s, "
+          "Version: %s \n# "
+          "Number of threads: %d\n# Number of MPI ranks: %d\n# Hydrodynamic "
+          "scheme: %s\n# Hydrodynamic kernel: %s\n# No. of neighbours: %.2f "
+          "+/- %.4f\n# Eta: %f\n# Radiative Transfer Scheme: %s\n# Max Number "
+          "RT sub-cycles: %d\n# Config: %s\n# CFLAGS: %s\n",
+          hostname(), git_branch(), git_revision(), compiler_name(),
+          compiler_version(), e->nr_threads, e->nr_nodes, SPH_IMPLEMENTATION,
+          kernel_name, e->hydro_properties->target_neighbours,
+          e->hydro_properties->delta_neighbours,
+          e->hydro_properties->eta_neighbours, RT_IMPLEMENTATION,
+          e->max_nr_rt_subcycles, configuration_options(),
+          compilation_cflags());
+
+      fprintf(
+          e->file_rt_subcycles,
+          "# Step Properties: Rebuild=%d, Redistribute=%d, Repartition=%d, "
+          "Statistics=%d, Snapshot=%d, Restarts=%d STF=%d, FOF=%d, mesh=%d\n",
+          engine_step_prop_rebuild, engine_step_prop_redistribute,
+          engine_step_prop_repartition, engine_step_prop_statistics,
+          engine_step_prop_snapshot, engine_step_prop_restarts,
+          engine_step_prop_stf, engine_step_prop_fof, engine_step_prop_mesh);
+
+      fprintf(e->file_rt_subcycles,
+              "# Note: Sub-cycle=0 is performed during the regular SWIFT step, "
+              "alongside hydro, gravity etc.\n");
+      fprintf(e->file_rt_subcycles,
+              "#       For this reason, the wall-clock time and dead time is "
+              "not available for it, and is written as -1.\n");
+
+      fprintf(e->file_rt_subcycles,
+              "# %6s %9s %14s %12s %12s %14s %9s %12s %16s [%s] %12s [%s]\n",
+              "Step", "Sub-cycle", "Time", "Scale-factor", "Redshift",
+              "Time-step", "Time-bins", "RT-Updates", "Wall-clock time",
+              clocks_getunit(), "Dead time", clocks_getunit());
+      fflush(e->file_rt_subcycles);
+#endif  // compiled with RT
     }
 
     /* Initialize the SFH logger if running with star formation */
@@ -717,14 +789,16 @@ void engine_config(int restart, int fof, struct engine *e,
 #endif
 
     /* Find the time of the first snapshot output */
-    engine_compute_next_snapshot_time(e);
+    engine_compute_next_snapshot_time(e, restart);
 
     /* Find the time of the first statistics output */
     engine_compute_next_statistics_time(e);
 
-    /* Find the time of the first line of sight output */
+    /* Find the time of the first line of sight output
+     * and verify the outputs */
     if (e->policy & engine_policy_line_of_sight) {
       engine_compute_next_los_time(e);
+      los_io_output_check(e);
     }
 
     /* Find the time of the first stf output */
@@ -757,9 +831,13 @@ void engine_config(int restart, int fof, struct engine *e,
      * on restart. */
     e->restart_onexit = parser_get_opt_param_int(params, "Restarts:onexit", 0);
 
-    /* Read the number of Lustre OSTs to distribute the restart files over */
-    e->restart_lustre_OST_count =
-        parser_get_opt_param_int(params, "Restarts:lustre_OST_count", 0);
+    /* Lustre OST options. Disabled by default. */
+    e->restart_lustre_OST_checks =
+        parser_get_opt_param_int(params, "Restarts:lustre_OST_checks", 0);
+    e->restart_lustre_OST_free =
+        parser_get_opt_param_int(params, "Restarts:lustre_OST_free", 0);
+    e->restart_lustre_OST_test =
+        parser_get_opt_param_int(params, "Restarts:lustre_OST_test", 0);
 
     /* Hours between restart dumps. Can be changed on restart. */
     float dhours =
@@ -876,6 +954,8 @@ void engine_config(int restart, int fof, struct engine *e,
         params, "Scheduler:cell_extra_gparts", space_extra_gparts_default);
     space_extra_bparts = parser_get_opt_param_int(
         params, "Scheduler:cell_extra_bparts", space_extra_bparts_default);
+    space_extra_sinks = parser_get_opt_param_int(
+        params, "Scheduler:cell_extra_sinks", space_extra_sinks_default);
 
     /* Do we want any spare particles for on the fly creation?
        This condition should be the same than in space.c */
diff --git a/src/engine_fof.c b/src/engine_fof.c
index 7cda72195d7cd10fee4b4102bb4c8569cbf9f87f..bab1715dc5c4eb7db7149ef6e3c8d8611421d17f 100644
--- a/src/engine_fof.c
+++ b/src/engine_fof.c
@@ -68,7 +68,7 @@ void engine_activate_gpart_comms(struct engine *e) {
 }
 
 /**
- * @brief Activate all the FOF tasks.
+ * @brief Activate all the FOF linking tasks.
  *
  * Marks all the other task types to be skipped.
  *
@@ -97,6 +97,37 @@ void engine_activate_fof_tasks(struct engine *e) {
             clocks_getunit());
 }
 
+/**
+ * @brief Activate all the FOF attaching tasks.
+ *
+ * Marks all the other task types to be skipped.
+ *
+ * @param e The #engine to act on.
+ */
+void engine_activate_fof_attach_tasks(struct engine *e) {
+
+  const ticks tic = getticks();
+
+  struct scheduler *s = &e->sched;
+  const int nr_tasks = s->nr_tasks;
+  struct task *tasks = s->tasks;
+
+  for (int k = 0; k < nr_tasks; k++) {
+
+    struct task *t = &tasks[k];
+
+    if (t->type == task_type_fof_attach_self ||
+        t->type == task_type_fof_attach_pair)
+      scheduler_activate(s, t);
+    else
+      t->skip = 1;
+  }
+
+  if (e->verbose)
+    message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
+            clocks_getunit());
+}
+
 /**
  * @brief Run a FOF search.
  *
@@ -123,15 +154,8 @@ void engine_fof(struct engine *e, const int dump_results,
 #endif
   }
 
-  /* Compute number of DM particles */
-  const long long total_nr_baryons =
-      e->total_nr_parts + e->total_nr_sparts + e->total_nr_bparts;
-  const long long total_nr_dmparts =
-      e->total_nr_gparts - e->total_nr_DM_background_gparts -
-      e->total_nr_neutrino_gparts - total_nr_baryons;
-
   /* Initialise FOF parameters and allocate FOF arrays. */
-  fof_allocate(e->s, total_nr_dmparts, e->fof_properties);
+  fof_allocate(e->s, e->fof_properties);
 
   /* Make FOF tasks */
   engine_make_fof_tasks(e);
@@ -142,14 +166,48 @@ void engine_fof(struct engine *e, const int dump_results,
   /* Print the number of active tasks ? */
   if (e->verbose) engine_print_task_counts(e);
 
-  /* Perform local FOF tasks. */
+  /* Perform local FOF tasks for linkable particles. */
   engine_launch(e, "fof");
 
-  /* Perform FOF search over foreign particles and
-   * find groups which require black hole seeding.  */
-  fof_search_tree(e->fof_properties, e->black_holes_properties,
-                  e->physical_constants, e->cosmology, e->s, dump_results,
-                  dump_debug_results, seed_black_holes);
+  /* Compute group sizes (only of local fragments with MPI) */
+  fof_compute_local_sizes(e->fof_properties, e->s);
+
+#ifdef WITH_MPI
+
+  /* Allocate buffers to receive the gpart fof information */
+  engine_allocate_foreign_particles(e, /*fof=*/1);
+
+  /* Compute the local<->foreign group links (nothing to do without MPI)*/
+  fof_search_foreign_cells(e->fof_properties, e->s);
+#endif
+
+  /* Compute the attachable->linkable links */
+  fof_link_attachable_particles(e->fof_properties, e->s);
+
+#ifdef WITH_MPI
+
+  /* Free the foreign particles */
+  space_free_foreign_parts(e->s, /*clear pointers=*/1);
+
+  /* Make a list of purely local groups to speed up the attaching */
+  fof_build_list_of_purely_local_groups(e->fof_properties, e->s);
+#endif
+
+  /* Finish the operations attaching the attachables to their groups */
+  fof_finalise_attachables(e->fof_properties, e->s);
+
+#ifdef WITH_MPI
+
+  /* Link the foreign fragments and finalise global group list (nothing to do
+   * without MPI) */
+  fof_link_foreign_fragments(e->fof_properties, e->s);
+#endif
+
+  /* Compute group properties and act on the results
+   * (seed BHs, dump catalogues..) */
+  fof_compute_group_props(e->fof_properties, e->black_holes_properties,
+                          e->physical_constants, e->cosmology, e->s,
+                          dump_results, dump_debug_results, seed_black_holes);
 
   /* Reset flag. */
   e->run_fof = 0;
diff --git a/src/engine_io.c b/src/engine_io.c
index 279e341153d85c0aa856b9ba6a59e719990a8044..006ba50b8be734808d580ae347b65211579abd08 100644
--- a/src/engine_io.c
+++ b/src/engine_io.c
@@ -268,8 +268,9 @@ int engine_dump_restarts(struct engine *e, const int drifted_all,
  * @brief Writes a snapshot with the current state of the engine
  *
  * @param e The #engine.
+ * @param fof Is this a stand-alone FOF call?
  */
-void engine_dump_snapshot(struct engine *e) {
+void engine_dump_snapshot(struct engine *e, const int fof) {
 
   struct clocks_time time1, time2;
   clocks_gettime(&time1);
@@ -324,24 +325,29 @@ void engine_dump_snapshot(struct engine *e) {
 #if defined(HAVE_HDF5)
 #if defined(WITH_MPI)
 
+  MPI_Info info;
+  MPI_Info_create(&info);
+
   if (e->snapshot_distributed) {
 
-    write_output_distributed(e, e->internal_units, e->snapshot_units, e->nodeID,
-                             e->nr_nodes, MPI_COMM_WORLD, MPI_INFO_NULL);
+    write_output_distributed(e, e->internal_units, e->snapshot_units, fof,
+                             e->nodeID, e->nr_nodes, MPI_COMM_WORLD, info);
+
   } else {
 
 #if defined(HAVE_PARALLEL_HDF5)
-    write_output_parallel(e, e->internal_units, e->snapshot_units, e->nodeID,
-                          e->nr_nodes, MPI_COMM_WORLD, MPI_INFO_NULL);
+    write_output_parallel(e, e->internal_units, e->snapshot_units, fof,
+                          e->nodeID, e->nr_nodes, MPI_COMM_WORLD, info);
 #else
-    write_output_serial(e, e->internal_units, e->snapshot_units, e->nodeID,
-                        e->nr_nodes, MPI_COMM_WORLD, MPI_INFO_NULL);
+    write_output_serial(e, e->internal_units, e->snapshot_units, fof, e->nodeID,
+                        e->nr_nodes, MPI_COMM_WORLD, info);
 #endif
   }
+  MPI_Info_free(&info);
 #else
-  write_output_single(e, e->internal_units, e->snapshot_units);
-#endif
-#endif
+  write_output_single(e, e->internal_units, e->snapshot_units, fof);
+#endif /* WITH_MPI */
+#endif /* WITH_HDF5 */
 
   /* Cancel any triggers that are switched on */
   if (num_snapshot_triggers_part > 0 || num_snapshot_triggers_spart > 0 ||
@@ -541,7 +547,7 @@ void engine_io(struct engine *e) {
         }
 
         /* Dump... */
-        engine_dump_snapshot(e);
+        engine_dump_snapshot(e, /*fof=*/0);
 
         /* Free the memory allocated for VELOCIraptor i/o. */
         if (with_stf && e->snapshot_invoke_stf && e->s->gpart_group_data) {
@@ -559,7 +565,7 @@ void engine_io(struct engine *e) {
 #endif
 
         /* ... and find the next output time */
-        engine_compute_next_snapshot_time(e);
+        engine_compute_next_snapshot_time(e, /*restart=*/0);
         break;
 
       case output_statistics:
@@ -688,17 +694,77 @@ void engine_io(struct engine *e) {
   e->time = time;
 }
 
+/**
+ * @brief Set the value of the recording trigger windows based
+ * on the user's desires and the time to the next snapshot.
+ *
+ * @param e The #engine.
+ */
+void engine_set_and_verify_snapshot_triggers(struct engine *e) {
+
+  integertime_t ti_next_snap = e->ti_next_snapshot;
+  if (ti_next_snap == -1) ti_next_snap = max_nr_timesteps;
+
+  /* Time until the next snapshot */
+  double time_to_next_snap;
+  if (e->policy & engine_policy_cosmology) {
+    time_to_next_snap =
+        cosmology_get_delta_time(e->cosmology, e->ti_current, ti_next_snap);
+  } else {
+    time_to_next_snap = (ti_next_snap - e->ti_current) * e->time_base;
+  }
+
+  /* Do we need to reduce any of the recording trigger times?
+   * Or can we set them with the user's desired range? */
+  for (int k = 0; k < num_snapshot_triggers_part; ++k) {
+    if (e->snapshot_recording_triggers_desired_part[k] > 0) {
+      if (e->snapshot_recording_triggers_desired_part[k] > time_to_next_snap) {
+        e->snapshot_recording_triggers_part[k] = time_to_next_snap;
+      } else {
+        e->snapshot_recording_triggers_part[k] =
+            e->snapshot_recording_triggers_desired_part[k];
+      }
+    }
+  }
+  for (int k = 0; k < num_snapshot_triggers_spart; ++k) {
+    if (e->snapshot_recording_triggers_desired_spart[k] > 0) {
+      if (e->snapshot_recording_triggers_desired_spart[k] > time_to_next_snap) {
+        e->snapshot_recording_triggers_spart[k] = time_to_next_snap;
+      } else {
+        e->snapshot_recording_triggers_spart[k] =
+            e->snapshot_recording_triggers_desired_spart[k];
+      }
+    }
+  }
+  for (int k = 0; k < num_snapshot_triggers_bpart; ++k) {
+    if (e->snapshot_recording_triggers_desired_bpart[k] > 0) {
+      if (e->snapshot_recording_triggers_desired_bpart[k] > time_to_next_snap) {
+        e->snapshot_recording_triggers_bpart[k] = time_to_next_snap;
+      } else {
+        e->snapshot_recording_triggers_bpart[k] =
+            e->snapshot_recording_triggers_desired_bpart[k];
+      }
+    }
+  }
+}
+
 /**
  * @brief Computes the next time (on the time line) for a dump
  *
  * @param e The #engine.
+ * @param restart Are we calling this upon a restart event?
  */
-void engine_compute_next_snapshot_time(struct engine *e) {
+void engine_compute_next_snapshot_time(struct engine *e, const int restart) {
 
   /* Do output_list file case */
   if (e->output_list_snapshots) {
     output_list_read_next_time(e->output_list_snapshots, e, "snapshots",
                                &e->ti_next_snapshot);
+
+    /* Unless we are restarting, check the allowed recording trigger time */
+    if (!restart) engine_set_and_verify_snapshot_triggers(e);
+
+    /* All done in the list case */
     return;
   }
 
@@ -756,49 +822,9 @@ void engine_compute_next_snapshot_time(struct engine *e) {
         message("Next snapshot time set to t=%e.", next_snapshot_time);
     }
 
-    /* Time until the next snapshot */
-    double time_to_next_snap;
-    if (e->policy & engine_policy_cosmology) {
-      time_to_next_snap = cosmology_get_delta_time(e->cosmology, e->ti_current,
-                                                   e->ti_next_snapshot);
-    } else {
-      time_to_next_snap = (e->ti_next_snapshot - e->ti_current) * e->time_base;
-    }
-
-    /* Do we need to reduce any of the recording trigger times? */
-    for (int k = 0; k < num_snapshot_triggers_part; ++k) {
-      if (e->snapshot_recording_triggers_desired_part[k] > 0) {
-        if (e->snapshot_recording_triggers_desired_part[k] >
-            time_to_next_snap) {
-          e->snapshot_recording_triggers_part[k] = time_to_next_snap;
-        } else {
-          e->snapshot_recording_triggers_part[k] =
-              e->snapshot_recording_triggers_desired_part[k];
-        }
-      }
-    }
-    for (int k = 0; k < num_snapshot_triggers_spart; ++k) {
-      if (e->snapshot_recording_triggers_desired_spart[k] > 0) {
-        if (e->snapshot_recording_triggers_desired_spart[k] >
-            time_to_next_snap) {
-          e->snapshot_recording_triggers_spart[k] = time_to_next_snap;
-        } else {
-          e->snapshot_recording_triggers_spart[k] =
-              e->snapshot_recording_triggers_desired_spart[k];
-        }
-      }
-    }
-    for (int k = 0; k < num_snapshot_triggers_bpart; ++k) {
-      if (e->snapshot_recording_triggers_desired_bpart[k] > 0) {
-        if (e->snapshot_recording_triggers_desired_bpart[k] >
-            time_to_next_snap) {
-          e->snapshot_recording_triggers_bpart[k] = time_to_next_snap;
-        } else {
-          e->snapshot_recording_triggers_bpart[k] =
-              e->snapshot_recording_triggers_desired_bpart[k];
-        }
-      }
-    }
+    /* Unless we are restarting, set the recording triggers accordingly for the
+     * next output */
+    if (!restart) engine_set_and_verify_snapshot_triggers(e);
   }
 }
 
@@ -1163,7 +1189,7 @@ void engine_init_output_lists(struct engine *e, struct swift_params *params,
     if (e->output_list_snapshots->select_output_on)
       output_list_check_selection(e->output_list_snapshots, output_options);
 
-    engine_compute_next_snapshot_time(e);
+    engine_compute_next_snapshot_time(e, /*restart=*/0);
 
     if (e->policy & engine_policy_cosmology)
       e->a_first_snapshot =
@@ -1259,12 +1285,15 @@ void engine_io_check_snapshot_triggers(struct engine *e) {
   const int with_cosmology = (e->policy & engine_policy_cosmology);
 
   /* Time until the next snapshot */
+  integertime_t ti_next_snap = e->ti_next_snapshot;
+  if (ti_next_snap == -1) ti_next_snap = max_nr_timesteps;
+
   double time_to_next_snap;
   if (e->policy & engine_policy_cosmology) {
-    time_to_next_snap = cosmology_get_delta_time(e->cosmology, e->ti_current,
-                                                 e->ti_next_snapshot);
+    time_to_next_snap =
+        cosmology_get_delta_time(e->cosmology, e->ti_current, ti_next_snap);
   } else {
-    time_to_next_snap = (e->ti_next_snapshot - e->ti_current) * e->time_base;
+    time_to_next_snap = (ti_next_snap - e->ti_current) * e->time_base;
   }
 
   /* Should any not yet switched on trigger be activated? (part version) */
@@ -1299,10 +1328,10 @@ void engine_io_check_snapshot_triggers(struct engine *e) {
         /* Time from the start of the particle's step to the snapshot */
         double total_time;
         if (with_cosmology) {
-          total_time = cosmology_get_delta_time(e->cosmology, ti_begin,
-                                                e->ti_next_snapshot);
+          total_time =
+              cosmology_get_delta_time(e->cosmology, ti_begin, ti_next_snap);
         } else {
-          total_time = (e->ti_next_snapshot - ti_begin) * e->time_base;
+          total_time = (ti_next_snap - ti_begin) * e->time_base;
         }
 
         /* Time to deduct = time since the start of the step - trigger time */
@@ -1360,10 +1389,10 @@ void engine_io_check_snapshot_triggers(struct engine *e) {
         /* Time from the start of the particle's step to the snapshot */
         double total_time;
         if (with_cosmology) {
-          total_time = cosmology_get_delta_time(e->cosmology, ti_begin,
-                                                e->ti_next_snapshot);
+          total_time =
+              cosmology_get_delta_time(e->cosmology, ti_begin, ti_next_snap);
         } else {
-          total_time = (e->ti_next_snapshot - ti_begin) * e->time_base;
+          total_time = (ti_next_snap - ti_begin) * e->time_base;
         }
 
         /* Time to deduct = time since the start of the step - trigger time */
@@ -1420,10 +1449,10 @@ void engine_io_check_snapshot_triggers(struct engine *e) {
         /* Time from the start of the particle's step to the snapshot */
         double total_time;
         if (with_cosmology) {
-          total_time = cosmology_get_delta_time(e->cosmology, ti_begin,
-                                                e->ti_next_snapshot);
+          total_time =
+              cosmology_get_delta_time(e->cosmology, ti_begin, ti_next_snap);
         } else {
-          total_time = (e->ti_next_snapshot - ti_begin) * e->time_base;
+          total_time = (ti_next_snap - ti_begin) * e->time_base;
         }
 
         /* Time to deduct = time since the start of the step - trigger time */
diff --git a/src/engine_maketasks.c b/src/engine_maketasks.c
index e205cf1f1a74371d7b117868095328b071bbb51f..ad49905d54fea12f8ca94b3cf810fb880e473736 100644
--- a/src/engine_maketasks.c
+++ b/src/engine_maketasks.c
@@ -43,6 +43,7 @@
 #include "engine.h"
 
 /* Local headers. */
+#include "adaptive_softening.h"
 #include "atomic.h"
 #include "cell.h"
 #include "clocks.h"
@@ -69,7 +70,9 @@ extern int engine_max_parts_per_cooling;
  * @param t_grav The send_grav #task, if it has already been created.
  */
 void engine_addtasks_send_gravity(struct engine *e, struct cell *ci,
-                                  struct cell *cj, struct task *t_grav) {
+                                  struct cell *cj, struct task *t_grav_counts,
+                                  struct task *t_grav,
+                                  const int with_star_formation) {
 
 #ifdef WITH_MPI
   struct link *l = NULL;
@@ -79,6 +82,19 @@ void engine_addtasks_send_gravity(struct engine *e, struct cell *ci,
   /* Early abort (are we below the level where tasks are)? */
   if (!cell_get_flag(ci, cell_flag_has_tasks)) return;
 
+  if (t_grav_counts == NULL && with_star_formation && ci->hydro.count > 0) {
+#ifdef SWIFT_DEBUG_CHECKS
+    if (ci->depth != 0)
+      error(
+          "Attaching a grav_count task at a non-top level c->depth=%d "
+          "c->count=%d",
+          ci->depth, ci->hydro.count);
+#endif
+    t_grav_counts = scheduler_addtask(
+        s, task_type_send, task_subtype_grav_counts, ci->mpi.tag, 0, ci, cj);
+    scheduler_addunlock(s, ci->hydro.star_formation, t_grav_counts);
+  }
+
   /* Check if any of the gravity tasks are for the target node. */
   for (l = ci->grav.grav; l != NULL; l = l->next)
     if (l->t->ci->nodeID == nodeID ||
@@ -100,19 +116,30 @@ void engine_addtasks_send_gravity(struct engine *e, struct cell *ci,
       /* The sends should unlock the down pass. */
       scheduler_addunlock(s, t_grav, ci->grav.super->grav.down);
 
+      if (with_star_formation && ci->top->hydro.count > 0)
+        scheduler_addunlock(s, t_grav, ci->top->hydro.star_formation);
+
       /* Drift before you send */
       scheduler_addunlock(s, ci->grav.super->grav.drift, t_grav);
+
+      if (gravity_after_hydro_density)
+        scheduler_addunlock(s, ci->grav.super->grav.init_out, t_grav);
     }
 
     /* Add them to the local cell. */
     engine_addlink(e, &ci->mpi.send, t_grav);
+
+    if (with_star_formation && ci->hydro.count > 0) {
+      engine_addlink(e, &ci->mpi.send, t_grav_counts);
+    }
   }
 
   /* Recurse? */
   if (ci->split)
     for (int k = 0; k < 8; k++)
       if (ci->progeny[k] != NULL)
-        engine_addtasks_send_gravity(e, ci->progeny[k], cj, t_grav);
+        engine_addtasks_send_gravity(e, ci->progeny[k], cj, t_grav_counts,
+                                     t_grav, with_star_formation);
 
 #else
   error("SWIFT was not compiled with MPI support.");
@@ -338,14 +365,13 @@ void engine_addtasks_send_stars(struct engine *e, struct cell *ci,
                                 struct cell *cj, struct task *t_density,
                                 struct task *t_prep2, struct task *t_sf_counts,
                                 const int with_star_formation) {
-#ifdef SWIFT_DEBUG_CHECKS
+#ifdef WITH_MPI
+#if !defined(SWIFT_DEBUG_CHECKS)
   if (e->policy & engine_policy_sinks && e->policy & engine_policy_stars) {
-    error("TODO");
+    error("TODO: Star formation sink over MPI");
   }
 #endif
 
-#ifdef WITH_MPI
-
   struct link *l = NULL;
   struct scheduler *s = &e->sched;
   const int nodeID = cj->nodeID;
@@ -501,24 +527,24 @@ void engine_addtasks_send_black_holes(struct engine *e, struct cell *ci,
       scheduler_addunlock(s, t_feedback,
                           ci->hydro.super->black_holes.black_holes_out);
 
-      scheduler_addunlock(s, ci->hydro.super->black_holes.swallow_ghost_2,
+      scheduler_addunlock(s, ci->hydro.super->black_holes.swallow_ghost_3,
                           t_feedback);
 
       /* Ghost before you send */
       scheduler_addunlock(s, ci->hydro.super->black_holes.drift, t_rho);
       scheduler_addunlock(s, ci->hydro.super->black_holes.density_ghost, t_rho);
       scheduler_addunlock(s, t_rho,
-                          ci->hydro.super->black_holes.swallow_ghost_0);
+                          ci->hydro.super->black_holes.swallow_ghost_1);
 
-      scheduler_addunlock(s, ci->hydro.super->black_holes.swallow_ghost_0,
+      scheduler_addunlock(s, ci->hydro.super->black_holes.swallow_ghost_1,
                           t_bh_merger);
       scheduler_addunlock(s, t_bh_merger,
-                          ci->hydro.super->black_holes.swallow_ghost_2);
+                          ci->hydro.super->black_holes.swallow_ghost_3);
 
-      scheduler_addunlock(s, ci->hydro.super->black_holes.swallow_ghost_0,
+      scheduler_addunlock(s, ci->hydro.super->black_holes.swallow_ghost_1,
                           t_gas_swallow);
       scheduler_addunlock(s, t_gas_swallow,
-                          ci->hydro.super->black_holes.swallow_ghost_1);
+                          ci->hydro.super->black_holes.swallow_ghost_2);
     }
 
     engine_addlink(e, &ci->mpi.send, t_rho);
@@ -889,13 +915,13 @@ void engine_addtasks_recv_stars(struct engine *e, struct cell *c,
                                 struct task *t_sf_counts,
                                 struct task *const tend,
                                 const int with_star_formation) {
-#ifdef SWIFT_DEBUG_CHECKS
+#ifdef WITH_MPI
+#if !defined(SWIFT_DEBUG_CHECKS)
   if (e->policy & engine_policy_sinks && e->policy & engine_policy_stars) {
-    error("TODO");
+    error("TODO: Star formation sink over MPI");
   }
 #endif
 
-#ifdef WITH_MPI
   struct scheduler *s = &e->sched;
 
   /* Early abort (are we below the level where tasks are)? */
@@ -1108,8 +1134,9 @@ void engine_addtasks_recv_black_holes(struct engine *e, struct cell *c,
  * @param tend The top-level time-step communication #task.
  */
 void engine_addtasks_recv_gravity(struct engine *e, struct cell *c,
-                                  struct task *t_grav,
-                                  struct task *const tend) {
+                                  struct task *t_grav_counts,
+                                  struct task *t_grav, struct task *const tend,
+                                  const int with_star_formation) {
 
 #ifdef WITH_MPI
   struct scheduler *s = &e->sched;
@@ -1117,6 +1144,19 @@ void engine_addtasks_recv_gravity(struct engine *e, struct cell *c,
   /* Early abort (are we below the level where tasks are)? */
   if (!cell_get_flag(c, cell_flag_has_tasks)) return;
 
+  if (t_grav_counts == NULL && with_star_formation && c->hydro.count > 0) {
+#ifdef SWIFT_DEBUG_CHECKS
+    if (c->depth != 0)
+      error(
+          "Attaching a grav_count task at a non-top level c->depth=%d "
+          "c->count=%d",
+          c->depth, c->hydro.count);
+#endif
+
+    t_grav_counts = scheduler_addtask(
+        s, task_type_recv, task_subtype_grav_counts, c->mpi.tag, 0, c, NULL);
+  }
+
   /* Have we reached a level where there are any gravity tasks ? */
   if (t_grav == NULL && c->grav.grav != NULL) {
 
@@ -1128,12 +1168,18 @@ void engine_addtasks_recv_gravity(struct engine *e, struct cell *c,
     /* Create the tasks. */
     t_grav = scheduler_addtask(s, task_type_recv, task_subtype_gpart,
                                c->mpi.tag, 0, c, NULL);
+
+    if (t_grav_counts != NULL) scheduler_addunlock(s, t_grav, t_grav_counts);
   }
 
   /* If we have tasks, link them. */
   if (t_grav != NULL) {
     engine_addlink(e, &c->mpi.recv, t_grav);
 
+    if (with_star_formation && c->hydro.count > 0) {
+      engine_addlink(e, &c->mpi.recv, t_grav_counts);
+    }
+
     for (struct link *l = c->grav.grav; l != NULL; l = l->next) {
       scheduler_addunlock(s, t_grav, l->t);
       scheduler_addunlock(s, l->t, tend);
@@ -1144,7 +1190,8 @@ void engine_addtasks_recv_gravity(struct engine *e, struct cell *c,
   if (c->split)
     for (int k = 0; k < 8; k++)
       if (c->progeny[k] != NULL)
-        engine_addtasks_recv_gravity(e, c->progeny[k], t_grav, tend);
+        engine_addtasks_recv_gravity(e, c->progeny[k], t_grav_counts, t_grav,
+                                     tend, with_star_formation);
 
 #else
   error("SWIFT was not compiled with MPI support.");
@@ -1272,6 +1319,11 @@ void engine_make_hierarchical_tasks_common(struct engine *e, struct cell *c) {
         scheduler_addunlock(s, c->top->sinks.star_formation_sink, c->timestep);
       }
 
+      /* Subgrid tasks: sinks formation */
+      if (with_sinks) {
+        scheduler_addunlock(s, c->kick2, c->top->sinks.sink_formation);
+      }
+
       /* Time-step limiter */
       if (with_timestep_limiter) {
 
@@ -1379,6 +1431,11 @@ void engine_make_hierarchical_tasks_gravity(struct engine *e, struct cell *c) {
         scheduler_addunlock(s, c->grav.long_range, c->grav.down);
         scheduler_addunlock(s, c->grav.down, c->grav.super->grav.end_force);
 
+        /* With adaptive softening, force the hydro density to complete first */
+        if (gravity_after_hydro_density && c->hydro.super == c) {
+          scheduler_addunlock(s, c->hydro.ghost_out, c->grav.init_out);
+        }
+
         /* Link in the implicit tasks */
         scheduler_addunlock(s, c->grav.init, c->grav.init_out);
         scheduler_addunlock(s, c->grav.drift, c->grav.drift_out);
@@ -1499,6 +1556,7 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
   const int with_star_formation_sink = (with_sinks && with_stars);
   const int with_black_holes = (e->policy & engine_policy_black_holes);
   const int with_rt = (e->policy & engine_policy_rt);
+  const int with_timestep_sync = (e->policy & engine_policy_timestep_sync);
 #ifdef WITH_CSDS
   const int with_csds = (e->policy & engine_policy_csds);
 #endif
@@ -1509,22 +1567,10 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
   if ((c->nodeID == e->nodeID) && (star_resort_cell == NULL) &&
       (c->depth == engine_star_resort_task_depth || c->hydro.super == c)) {
 
-    /* Star formation */
-    if (with_feedback && c->hydro.count > 0 && with_star_formation) {
-
-      /* Record this is the level where we re-sort */
-      star_resort_cell = c;
-
-      c->hydro.stars_resort = scheduler_addtask(
-          s, task_type_stars_resort, task_subtype_none, 0, 0, c, NULL);
-
-      scheduler_addunlock(s, c->top->hydro.star_formation,
-                          c->hydro.stars_resort);
-    }
-
-    /* Star formation from sinks */
-    if (with_feedback && with_star_formation_sink &&
-        (c->hydro.count > 0 || c->sinks.count > 0)) {
+    /* If star formation from gas or sinks has happened, we need to resort */
+    if (with_feedback && ((c->hydro.count > 0 && with_star_formation) ||
+                          ((c->hydro.count > 0 || c->sinks.count > 0) &&
+                           with_star_formation_sink))) {
 
       /* Record this is the level where we re-sort */
       star_resort_cell = c;
@@ -1532,8 +1578,20 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
       c->hydro.stars_resort = scheduler_addtask(
           s, task_type_stars_resort, task_subtype_none, 0, 0, c, NULL);
 
-      scheduler_addunlock(s, c->top->sinks.star_formation_sink,
-                          c->hydro.stars_resort);
+      /* Now add the relevant unlocks */
+      /* If we can make stars, we should wait until SF is done before resorting
+       */
+      if (with_star_formation && c->hydro.count > 0) {
+        scheduler_addunlock(s, c->top->hydro.star_formation,
+                            c->hydro.stars_resort);
+      }
+      /* If we can make sinks or spawn from existing ones, we should wait until
+      SF is done before resorting */
+      if (with_star_formation_sink &&
+          (c->hydro.count > 0 || c->sinks.count > 0)) {
+        scheduler_addunlock(s, c->top->sinks.star_formation_sink,
+                            c->hydro.stars_resort);
+      }
     }
   }
 
@@ -1550,7 +1608,7 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
     }
 
     if (with_black_holes) {
-      c->black_holes.swallow_ghost_0 =
+      c->black_holes.swallow_ghost_1 =
           scheduler_addtask(s, task_type_bh_swallow_ghost1, task_subtype_none,
                             0, /* implicit =*/1, c, NULL);
     }
@@ -1601,6 +1659,9 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
             scheduler_addtask(s, task_type_sink_in, task_subtype_none, 0,
                               /* implicit = */ 1, c, NULL);
 
+        c->sinks.density_ghost = scheduler_addtask(
+            s, task_type_sink_density_ghost, task_subtype_none, 0, 0, c, NULL);
+
         c->sinks.sink_ghost1 =
             scheduler_addtask(s, task_type_sink_ghost1, task_subtype_none, 0,
                               /* implicit = */ 1, c, NULL);
@@ -1616,6 +1677,9 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
         /* Link to the main tasks */
         scheduler_addunlock(s, c->super->kick2, c->sinks.sink_in);
         scheduler_addunlock(s, c->sinks.sink_out, c->super->timestep);
+        scheduler_addunlock(s, c->top->sinks.sink_formation, c->sinks.sink_in);
+        if (with_timestep_sync)
+          scheduler_addunlock(s, c->sinks.sink_out, c->super->timestep_sync);
 
         if (with_stars &&
             (c->top->hydro.count > 0 || c->top->sinks.count > 0)) {
@@ -1690,13 +1754,9 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
         scheduler_addunlock(s, c->stars.stars_out, c->super->timestep);
 
         /* Star formation*/
-        if (with_feedback && c->hydro.count > 0 && with_star_formation) {
-          scheduler_addunlock(s, star_resort_cell->hydro.stars_resort,
-                              c->stars.stars_in);
-        }
-        /* Star formation from sinks */
-        if (with_feedback && with_star_formation_sink &&
-            (c->hydro.count > 0 || c->sinks.count > 0)) {
+        if (with_feedback && ((c->hydro.count > 0 && with_star_formation) ||
+                              ((c->hydro.count > 0 || c->sinks.count > 0) &&
+                               with_star_formation_sink))) {
           scheduler_addunlock(s, star_resort_cell->hydro.stars_resort,
                               c->stars.stars_in);
         }
@@ -1785,11 +1845,11 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
         c->black_holes.density_ghost = scheduler_addtask(
             s, task_type_bh_density_ghost, task_subtype_none, 0, 0, c, NULL);
 
-        c->black_holes.swallow_ghost_1 =
+        c->black_holes.swallow_ghost_2 =
             scheduler_addtask(s, task_type_bh_swallow_ghost2, task_subtype_none,
                               0, /* implicit =*/1, c, NULL);
 
-        c->black_holes.swallow_ghost_2 = scheduler_addtask(
+        c->black_holes.swallow_ghost_3 = scheduler_addtask(
             s, task_type_bh_swallow_ghost3, task_subtype_none, 0, 0, c, NULL);
 
 #ifdef WITH_CSDS
@@ -1811,7 +1871,7 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
         /* Make sure we don't start swallowing gas particles before the stars
            have converged on their smoothing lengths. */
         scheduler_addunlock(s, c->stars.density_ghost,
-                            c->black_holes.swallow_ghost_0);
+                            c->black_holes.swallow_ghost_1);
       }
     }
   } else { /* We are above the super-cell so need to go deeper */
@@ -2098,7 +2158,9 @@ void engine_count_and_link_tasks_mapper(void *map_data, int num_elements,
 
       /* Link self tasks to cells. */
     } else if (t_type == task_type_self) {
+#ifdef SWIFT_DEBUG_CHECKS
       atomic_inc(&ci->nr_tasks);
+#endif
 
       if (t_subtype == task_subtype_density) {
         engine_addlink(e, &ci->hydro.density, t);
@@ -2110,8 +2172,10 @@ void engine_count_and_link_tasks_mapper(void *map_data, int num_elements,
 
       /* Link pair tasks to cells. */
     } else if (t_type == task_type_pair) {
+#ifdef SWIFT_DEBUG_CHECKS
       atomic_inc(&ci->nr_tasks);
       atomic_inc(&cj->nr_tasks);
+#endif
 
       if (t_subtype == task_subtype_density) {
         engine_addlink(e, &ci->hydro.density, t);
@@ -2128,7 +2192,9 @@ void engine_count_and_link_tasks_mapper(void *map_data, int num_elements,
 
       /* Link sub-self tasks to cells. */
     } else if (t_type == task_type_sub_self) {
+#ifdef SWIFT_DEBUG_CHECKS
       atomic_inc(&ci->nr_tasks);
+#endif
 
       if (t_subtype == task_subtype_density) {
         engine_addlink(e, &ci->hydro.density, t);
@@ -2140,8 +2206,10 @@ void engine_count_and_link_tasks_mapper(void *map_data, int num_elements,
 
       /* Link sub-pair tasks to cells. */
     } else if (t_type == task_type_sub_pair) {
+#ifdef SWIFT_DEBUG_CHECKS
       atomic_inc(&ci->nr_tasks);
       atomic_inc(&cj->nr_tasks);
+#endif
 
       if (t_subtype == task_subtype_density) {
         engine_addlink(e, &ci->hydro.density, t);
@@ -2170,24 +2238,28 @@ void engine_count_and_link_tasks_mapper(void *map_data, int num_elements,
 /**
  * @brief Creates all the task dependencies for the gravity
  *
- * @param e The #engine
+ * @param map_data The task array passed to this pool thread.
+ * @param num_elements The number of tasks in this pool thread.
+ * @param extra_data Pointer to the #engine.
  */
-void engine_link_gravity_tasks(struct engine *e) {
+void engine_link_gravity_tasks_mapper(void *map_data, int num_elements,
+                                      void *extra_data) {
 
+  struct task *tasks = (struct task *)map_data;
+  struct engine *e = (struct engine *)extra_data;
   struct scheduler *sched = &e->sched;
   const int nodeID = e->nodeID;
-  const int nr_tasks = sched->nr_tasks;
 
-  for (int k = 0; k < nr_tasks; k++) {
+  for (int k = 0; k < num_elements; k++) {
 
     /* Get a pointer to the task. */
-    struct task *t = &sched->tasks[k];
+    struct task *t = &tasks[k];
 
     if (t->type == task_type_none) continue;
 
     /* Get the cells we act on */
-    struct cell *ci = t->ci;
-    struct cell *cj = t->cj;
+    struct cell *restrict ci = t->ci;
+    struct cell *restrict cj = t->cj;
     const enum task_types t_type = t->type;
     const enum task_subtypes t_subtype = t->subtype;
 
@@ -2229,7 +2301,8 @@ void engine_link_gravity_tasks(struct engine *e) {
     }
 
     /* Self-interaction for external gravity ? */
-    if (t_type == task_type_self && t_subtype == task_subtype_external_grav) {
+    else if (t_type == task_type_self &&
+             t_subtype == task_subtype_external_grav) {
 
 #ifdef SWIFT_DEBUG_CHECKS
       if (ci_nodeID != nodeID) error("Non-local self task");
@@ -2425,6 +2498,7 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
   struct task *t_do_gas_swallow = NULL;
   struct task *t_do_bh_swallow = NULL;
   struct task *t_bh_feedback = NULL;
+  struct task *t_sink_density = NULL;
   struct task *t_sink_swallow = NULL;
   struct task *t_rt_gradient = NULL;
   struct task *t_rt_transport = NULL;
@@ -2496,6 +2570,9 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
       /* The sink tasks */
       if (with_sink) {
+        t_sink_density =
+            scheduler_addtask(sched, task_type_self, task_subtype_sink_density,
+                              flags, 0, ci, NULL);
         t_sink_swallow =
             scheduler_addtask(sched, task_type_self, task_subtype_sink_swallow,
                               flags, 0, ci, NULL);
@@ -2547,6 +2624,7 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 #endif
       }
       if (with_sink) {
+        engine_addlink(e, &ci->sinks.density, t_sink_density);
         engine_addlink(e, &ci->sinks.swallow, t_sink_swallow);
         engine_addlink(e, &ci->sinks.do_sink_swallow, t_sink_do_sink_swallow);
         engine_addlink(e, &ci->sinks.do_gas_swallow, t_sink_do_gas_swallow);
@@ -2626,17 +2704,20 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
       /* The sink's tasks. */
       if (with_sink) {
 
-        /* Do the sink_formation */
+        /* Sink density */
         scheduler_addunlock(sched, ci->hydro.super->sinks.drift,
-                            ci->top->sinks.sink_formation);
+                            t_sink_density);
         scheduler_addunlock(sched, ci->hydro.super->hydro.drift,
-                            ci->top->sinks.sink_formation);
+                            t_sink_density);
         scheduler_addunlock(sched, ci->hydro.super->sinks.sink_in,
-                            ci->top->sinks.sink_formation);
-        scheduler_addunlock(sched, ci->top->sinks.sink_formation,
-                            t_sink_swallow);
+                            t_sink_density);
+        scheduler_addunlock(sched, t_sink_density,
+                            ci->hydro.super->sinks.density_ghost);
 
         /* Do the sink_swallow */
+        scheduler_addunlock(sched, ci->hydro.super->sinks.density_ghost,
+                            t_sink_swallow);
+
         scheduler_addunlock(sched, t_sink_swallow,
                             ci->hydro.super->sinks.sink_ghost1);
 
@@ -2670,19 +2751,19 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         scheduler_addunlock(sched, ci->hydro.super->black_holes.density_ghost,
                             t_bh_swallow);
         scheduler_addunlock(sched, t_bh_swallow,
-                            ci->hydro.super->black_holes.swallow_ghost_0);
+                            ci->hydro.super->black_holes.swallow_ghost_1);
 
-        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_0,
+        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_1,
                             t_do_gas_swallow);
         scheduler_addunlock(sched, t_do_gas_swallow,
-                            ci->hydro.super->black_holes.swallow_ghost_1);
+                            ci->hydro.super->black_holes.swallow_ghost_2);
 
-        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_1,
+        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_2,
                             t_do_bh_swallow);
         scheduler_addunlock(sched, t_do_bh_swallow,
-                            ci->hydro.super->black_holes.swallow_ghost_2);
+                            ci->hydro.super->black_holes.swallow_ghost_3);
 
-        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_2,
+        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_3,
                             t_bh_feedback);
         scheduler_addunlock(sched, t_bh_feedback,
                             ci->hydro.super->black_holes.black_holes_out);
@@ -2776,6 +2857,8 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
       /* The sink tasks */
       if (with_sink) {
+        t_sink_density = scheduler_addtask(
+            sched, task_type_pair, task_subtype_sink_density, flags, 0, ci, cj);
         t_sink_swallow = scheduler_addtask(
             sched, task_type_pair, task_subtype_sink_swallow, flags, 0, ci, cj);
         t_sink_do_sink_swallow = scheduler_addtask(
@@ -2854,13 +2937,12 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 #endif
       }
       if (with_sink) {
-        /* Formation */
+        engine_addlink(e, &ci->sinks.density, t_sink_density);
+        engine_addlink(e, &cj->sinks.density, t_sink_density);
         engine_addlink(e, &ci->sinks.swallow, t_sink_swallow);
         engine_addlink(e, &cj->sinks.swallow, t_sink_swallow);
-        /* Merger */
         engine_addlink(e, &ci->sinks.do_sink_swallow, t_sink_do_sink_swallow);
         engine_addlink(e, &cj->sinks.do_sink_swallow, t_sink_do_sink_swallow);
-        /* Accretion */
         engine_addlink(e, &ci->sinks.do_gas_swallow, t_sink_do_gas_swallow);
         engine_addlink(e, &cj->sinks.do_gas_swallow, t_sink_do_gas_swallow);
       }
@@ -2983,17 +3065,19 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
         if (with_sink) {
 
-          /* Do the sink_formation */
+          /* Sink density */
           scheduler_addunlock(sched, ci->hydro.super->sinks.drift,
-                              ci->top->sinks.sink_formation);
+                              t_sink_density);
           scheduler_addunlock(sched, ci->hydro.super->hydro.drift,
-                              ci->top->sinks.sink_formation);
+                              t_sink_density);
           scheduler_addunlock(sched, ci->hydro.super->sinks.sink_in,
-                              ci->top->sinks.sink_formation);
-          scheduler_addunlock(sched, ci->top->sinks.sink_formation,
-                              t_sink_swallow);
+                              t_sink_density);
+          scheduler_addunlock(sched, t_sink_density,
+                              ci->hydro.super->sinks.density_ghost);
 
           /* Do the sink_swallow */
+          scheduler_addunlock(sched, ci->hydro.super->sinks.density_ghost,
+                              t_sink_swallow);
           scheduler_addunlock(sched, t_sink_swallow,
                               ci->hydro.super->sinks.sink_ghost1);
 
@@ -3028,22 +3112,22 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
           scheduler_addunlock(sched, ci->hydro.super->black_holes.density_ghost,
                               t_bh_swallow);
           scheduler_addunlock(sched, t_bh_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_0);
+                              ci->hydro.super->black_holes.swallow_ghost_1);
 
           scheduler_addunlock(sched,
-                              ci->hydro.super->black_holes.swallow_ghost_0,
+                              ci->hydro.super->black_holes.swallow_ghost_1,
                               t_do_gas_swallow);
           scheduler_addunlock(sched, t_do_gas_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_1);
+                              ci->hydro.super->black_holes.swallow_ghost_2);
 
           scheduler_addunlock(sched,
-                              ci->hydro.super->black_holes.swallow_ghost_1,
+                              ci->hydro.super->black_holes.swallow_ghost_2,
                               t_do_bh_swallow);
           scheduler_addunlock(sched, t_do_bh_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_2);
+                              ci->hydro.super->black_holes.swallow_ghost_3);
 
           scheduler_addunlock(sched,
-                              ci->hydro.super->black_holes.swallow_ghost_2,
+                              ci->hydro.super->black_holes.swallow_ghost_3,
                               t_bh_feedback);
           scheduler_addunlock(sched, t_bh_feedback,
                               ci->hydro.super->black_holes.black_holes_out);
@@ -3089,7 +3173,7 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
         if (with_black_holes && (bcount_i > 0 || bcount_j > 0)) {
           scheduler_addunlock(sched, t_bh_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_0);
+                              ci->hydro.super->black_holes.swallow_ghost_1);
         }
       }
 
@@ -3140,17 +3224,19 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
           if (with_sink) {
 
-            /* Do the sink_formation */
+            /* Sink density */
             scheduler_addunlock(sched, cj->hydro.super->sinks.drift,
-                                cj->top->sinks.sink_formation);
+                                t_sink_density);
             scheduler_addunlock(sched, cj->hydro.super->hydro.drift,
-                                cj->top->sinks.sink_formation);
+                                t_sink_density);
             scheduler_addunlock(sched, cj->hydro.super->sinks.sink_in,
-                                cj->top->sinks.sink_formation);
-            scheduler_addunlock(sched, cj->top->sinks.sink_formation,
-                                t_sink_swallow);
+                                t_sink_density);
+            scheduler_addunlock(sched, t_sink_density,
+                                cj->hydro.super->sinks.density_ghost);
 
             /* Do the sink_swallow */
+            scheduler_addunlock(sched, cj->hydro.super->sinks.density_ghost,
+                                t_sink_swallow);
             scheduler_addunlock(sched, t_sink_swallow,
                                 cj->hydro.super->sinks.sink_ghost1);
 
@@ -3187,22 +3273,22 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
                                 cj->hydro.super->black_holes.density_ghost,
                                 t_bh_swallow);
             scheduler_addunlock(sched, t_bh_swallow,
-                                cj->hydro.super->black_holes.swallow_ghost_0);
+                                cj->hydro.super->black_holes.swallow_ghost_1);
 
             scheduler_addunlock(sched,
-                                cj->hydro.super->black_holes.swallow_ghost_0,
+                                cj->hydro.super->black_holes.swallow_ghost_1,
                                 t_do_gas_swallow);
             scheduler_addunlock(sched, t_do_gas_swallow,
-                                cj->hydro.super->black_holes.swallow_ghost_1);
+                                cj->hydro.super->black_holes.swallow_ghost_2);
 
             scheduler_addunlock(sched,
-                                cj->hydro.super->black_holes.swallow_ghost_1,
+                                cj->hydro.super->black_holes.swallow_ghost_2,
                                 t_do_bh_swallow);
             scheduler_addunlock(sched, t_do_bh_swallow,
-                                cj->hydro.super->black_holes.swallow_ghost_2);
+                                cj->hydro.super->black_holes.swallow_ghost_3);
 
             scheduler_addunlock(sched,
-                                cj->hydro.super->black_holes.swallow_ghost_2,
+                                cj->hydro.super->black_holes.swallow_ghost_3,
                                 t_bh_feedback);
             scheduler_addunlock(sched, t_bh_feedback,
                                 cj->hydro.super->black_holes.black_holes_out);
@@ -3257,7 +3343,7 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         if (with_black_holes && (bcount_i > 0 || bcount_j > 0)) {
 
           scheduler_addunlock(sched, t_bh_swallow,
-                              cj->hydro.super->black_holes.swallow_ghost_0);
+                              cj->hydro.super->black_holes.swallow_ghost_1);
         }
       }
     }
@@ -3303,6 +3389,9 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
       /* The sink tasks */
       if (with_sink) {
+        t_sink_density =
+            scheduler_addtask(sched, task_type_sub_self,
+                              task_subtype_sink_density, flags, 0, ci, NULL);
         t_sink_swallow =
             scheduler_addtask(sched, task_type_sub_self,
                               task_subtype_sink_swallow, flags, 0, ci, NULL);
@@ -3359,6 +3448,7 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 #endif
       }
       if (with_sink) {
+        engine_addlink(e, &ci->sinks.density, t_sink_density);
         engine_addlink(e, &ci->sinks.swallow, t_sink_swallow);
         engine_addlink(e, &ci->sinks.do_sink_swallow, t_sink_do_sink_swallow);
         engine_addlink(e, &ci->sinks.do_gas_swallow, t_sink_do_gas_swallow);
@@ -3443,17 +3533,19 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
       if (with_sink) {
 
-        /* Do the sink_formation */
+        /* Sink density */
         scheduler_addunlock(sched, ci->hydro.super->sinks.drift,
-                            ci->top->sinks.sink_formation);
+                            t_sink_density);
         scheduler_addunlock(sched, ci->hydro.super->hydro.drift,
-                            ci->top->sinks.sink_formation);
+                            t_sink_density);
         scheduler_addunlock(sched, ci->hydro.super->sinks.sink_in,
-                            ci->top->sinks.sink_formation);
-        scheduler_addunlock(sched, ci->top->sinks.sink_formation,
-                            t_sink_swallow);
+                            t_sink_density);
+        scheduler_addunlock(sched, t_sink_density,
+                            ci->hydro.super->sinks.density_ghost);
 
         /* Do the sink_swallow */
+        scheduler_addunlock(sched, ci->hydro.super->sinks.density_ghost,
+                            t_sink_swallow);
         scheduler_addunlock(sched, t_sink_swallow,
                             ci->hydro.super->sinks.sink_ghost1);
 
@@ -3487,19 +3579,19 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         scheduler_addunlock(sched, ci->hydro.super->black_holes.density_ghost,
                             t_bh_swallow);
         scheduler_addunlock(sched, t_bh_swallow,
-                            ci->hydro.super->black_holes.swallow_ghost_0);
+                            ci->hydro.super->black_holes.swallow_ghost_1);
 
-        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_0,
+        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_1,
                             t_do_gas_swallow);
         scheduler_addunlock(sched, t_do_gas_swallow,
-                            ci->hydro.super->black_holes.swallow_ghost_1);
+                            ci->hydro.super->black_holes.swallow_ghost_2);
 
-        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_1,
+        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_2,
                             t_do_bh_swallow);
         scheduler_addunlock(sched, t_do_bh_swallow,
-                            ci->hydro.super->black_holes.swallow_ghost_2);
+                            ci->hydro.super->black_holes.swallow_ghost_3);
 
-        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_2,
+        scheduler_addunlock(sched, ci->hydro.super->black_holes.swallow_ghost_3,
                             t_bh_feedback);
         scheduler_addunlock(sched, t_bh_feedback,
                             ci->hydro.super->black_holes.black_holes_out);
@@ -3598,6 +3690,9 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
       /* The sink tasks */
       if (with_sink) {
+        t_sink_density =
+            scheduler_addtask(sched, task_type_sub_pair,
+                              task_subtype_sink_density, flags, 0, ci, cj);
         t_sink_swallow =
             scheduler_addtask(sched, task_type_sub_pair,
                               task_subtype_sink_swallow, flags, 0, ci, cj);
@@ -3682,15 +3777,12 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 #endif
       }
       if (with_sink) {
-        /* Formation */
+        engine_addlink(e, &ci->sinks.density, t_sink_density);
+        engine_addlink(e, &cj->sinks.density, t_sink_density);
         engine_addlink(e, &ci->sinks.swallow, t_sink_swallow);
         engine_addlink(e, &cj->sinks.swallow, t_sink_swallow);
-
-        /* Merger */
         engine_addlink(e, &ci->sinks.do_sink_swallow, t_sink_do_sink_swallow);
         engine_addlink(e, &cj->sinks.do_sink_swallow, t_sink_do_sink_swallow);
-
-        /* Accretion */
         engine_addlink(e, &ci->sinks.do_gas_swallow, t_sink_do_gas_swallow);
         engine_addlink(e, &cj->sinks.do_gas_swallow, t_sink_do_gas_swallow);
       }
@@ -3812,17 +3904,19 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
         if (with_sink) {
 
-          /* Do the sink_formation */
+          /* Sink density */
           scheduler_addunlock(sched, ci->hydro.super->sinks.drift,
-                              ci->top->sinks.sink_formation);
+                              t_sink_density);
           scheduler_addunlock(sched, ci->hydro.super->hydro.drift,
-                              ci->top->sinks.sink_formation);
+                              t_sink_density);
           scheduler_addunlock(sched, ci->hydro.super->sinks.sink_in,
-                              ci->top->sinks.sink_formation);
-          scheduler_addunlock(sched, ci->top->sinks.sink_formation,
-                              t_sink_swallow);
+                              t_sink_density);
+          scheduler_addunlock(sched, t_sink_density,
+                              ci->hydro.super->sinks.density_ghost);
 
           /* Do the sink_swallow */
+          scheduler_addunlock(sched, ci->hydro.super->sinks.density_ghost,
+                              t_sink_swallow);
           scheduler_addunlock(sched, t_sink_swallow,
                               ci->hydro.super->sinks.sink_ghost1);
 
@@ -3857,22 +3951,22 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
           scheduler_addunlock(sched, ci->hydro.super->black_holes.density_ghost,
                               t_bh_swallow);
           scheduler_addunlock(sched, t_bh_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_0);
+                              ci->hydro.super->black_holes.swallow_ghost_1);
 
           scheduler_addunlock(sched,
-                              ci->hydro.super->black_holes.swallow_ghost_0,
+                              ci->hydro.super->black_holes.swallow_ghost_1,
                               t_do_gas_swallow);
           scheduler_addunlock(sched, t_do_gas_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_1);
+                              ci->hydro.super->black_holes.swallow_ghost_2);
 
           scheduler_addunlock(sched,
-                              ci->hydro.super->black_holes.swallow_ghost_1,
+                              ci->hydro.super->black_holes.swallow_ghost_2,
                               t_do_bh_swallow);
           scheduler_addunlock(sched, t_do_bh_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_2);
+                              ci->hydro.super->black_holes.swallow_ghost_3);
 
           scheduler_addunlock(sched,
-                              ci->hydro.super->black_holes.swallow_ghost_2,
+                              ci->hydro.super->black_holes.swallow_ghost_3,
                               t_bh_feedback);
           scheduler_addunlock(sched, t_bh_feedback,
                               ci->hydro.super->black_holes.black_holes_out);
@@ -3918,7 +4012,7 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         if (with_black_holes && (bcount_i > 0 || bcount_j > 0)) {
 
           scheduler_addunlock(sched, t_bh_swallow,
-                              ci->hydro.super->black_holes.swallow_ghost_0);
+                              ci->hydro.super->black_holes.swallow_ghost_1);
         }
       }
 
@@ -3966,19 +4060,22 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
             scheduler_addunlock(sched, t_star_feedback,
                                 cj->hydro.super->stars.stars_out);
           }
+
           if (with_sink) {
 
-            /* Do the sink_formation */
+            /* Sink density */
             scheduler_addunlock(sched, cj->hydro.super->sinks.drift,
-                                cj->top->sinks.sink_formation);
+                                t_sink_density);
             scheduler_addunlock(sched, cj->hydro.super->hydro.drift,
-                                cj->top->sinks.sink_formation);
+                                t_sink_density);
             scheduler_addunlock(sched, cj->hydro.super->sinks.sink_in,
-                                cj->top->sinks.sink_formation);
-            scheduler_addunlock(sched, cj->top->sinks.sink_formation,
-                                t_sink_swallow);
+                                t_sink_density);
+            scheduler_addunlock(sched, t_sink_density,
+                                cj->hydro.super->sinks.density_ghost);
 
             /* Do the sink_swallow */
+            scheduler_addunlock(sched, cj->hydro.super->sinks.density_ghost,
+                                t_sink_swallow);
             scheduler_addunlock(sched, t_sink_swallow,
                                 cj->hydro.super->sinks.sink_ghost1);
 
@@ -4015,22 +4112,22 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
                                 cj->hydro.super->black_holes.density_ghost,
                                 t_bh_swallow);
             scheduler_addunlock(sched, t_bh_swallow,
-                                cj->hydro.super->black_holes.swallow_ghost_0);
+                                cj->hydro.super->black_holes.swallow_ghost_1);
 
             scheduler_addunlock(sched,
-                                cj->hydro.super->black_holes.swallow_ghost_0,
+                                cj->hydro.super->black_holes.swallow_ghost_1,
                                 t_do_gas_swallow);
             scheduler_addunlock(sched, t_do_gas_swallow,
-                                cj->hydro.super->black_holes.swallow_ghost_1);
+                                cj->hydro.super->black_holes.swallow_ghost_2);
 
             scheduler_addunlock(sched,
-                                cj->hydro.super->black_holes.swallow_ghost_1,
+                                cj->hydro.super->black_holes.swallow_ghost_2,
                                 t_do_bh_swallow);
             scheduler_addunlock(sched, t_do_bh_swallow,
-                                cj->hydro.super->black_holes.swallow_ghost_2);
+                                cj->hydro.super->black_holes.swallow_ghost_3);
 
             scheduler_addunlock(sched,
-                                cj->hydro.super->black_holes.swallow_ghost_2,
+                                cj->hydro.super->black_holes.swallow_ghost_3,
                                 t_bh_feedback);
             scheduler_addunlock(sched, t_bh_feedback,
                                 cj->hydro.super->black_holes.black_holes_out);
@@ -4082,7 +4179,7 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
         if (with_black_holes && (bcount_i > 0 || bcount_j > 0)) {
           scheduler_addunlock(sched, t_bh_swallow,
-                              cj->hydro.super->black_holes.swallow_ghost_0);
+                              cj->hydro.super->black_holes.swallow_ghost_1);
         }
       }
     }
@@ -4293,9 +4390,9 @@ void engine_addtasks_send_mapper(void *map_data, int num_elements,
   const int with_rt = (e->policy & engine_policy_rt);
   struct cell_type_pair *cell_type_pairs = (struct cell_type_pair *)map_data;
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(WITH_MPI) && !defined(SWIFT_DEBUG_CHECKS)
   if (e->policy & engine_policy_sinks) {
-    error("TODO");
+    error("TODO: Sink MPI tasks are not implemented yet!");
   }
 #endif
 
@@ -4349,7 +4446,8 @@ void engine_addtasks_send_mapper(void *map_data, int num_elements,
      * connection. */
     if ((e->policy & engine_policy_self_gravity) &&
         (type & proxy_cell_type_gravity))
-      engine_addtasks_send_gravity(e, ci, cj, /*t_grav=*/NULL);
+      engine_addtasks_send_gravity(e, ci, cj, /*t_grav_counts=*/NULL,
+                                   /*t_grav=*/NULL, with_star_formation);
   }
 }
 
@@ -4365,9 +4463,9 @@ void engine_addtasks_recv_mapper(void *map_data, int num_elements,
   const int with_rt = (e->policy & engine_policy_rt);
   struct cell_type_pair *cell_type_pairs = (struct cell_type_pair *)map_data;
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(WITH_MPI) && !defined(SWIFT_DEBUG_CHECKS)
   if (e->policy & engine_policy_sinks) {
-    error("TODO");
+    error("TODO: Sink MPI tasks are not implemented yet!");
   }
 #endif
 
@@ -4445,7 +4543,8 @@ void engine_addtasks_recv_mapper(void *map_data, int num_elements,
      * connection. */
     if ((e->policy & engine_policy_self_gravity) &&
         (type & proxy_cell_type_gravity))
-      engine_addtasks_recv_gravity(e, ci, /*t_grav=*/NULL, tend);
+      engine_addtasks_recv_gravity(e, ci, /*t_grav_counts*/ NULL,
+                                   /*t_grav=*/NULL, tend, with_star_formation);
   }
 }
 
@@ -4490,11 +4589,12 @@ void engine_make_fofloop_tasks_mapper(void *map_data, int num_elements,
     if (ci->grav.count == 0) continue;
 
     /* If the cells is local build a self-interaction */
-    if (ci->nodeID == nodeID)
+    if (ci->nodeID == nodeID) {
       scheduler_addtask(sched, task_type_fof_self, task_subtype_none, 0, 0, ci,
                         NULL);
-    else
-      continue;
+      scheduler_addtask(sched, task_type_fof_attach_self, task_subtype_none, 0,
+                        0, ci, NULL);
+    }
 
     /* Now loop over all the neighbours of this cell */
     for (int ii = -1; ii < 2; ii++) {
@@ -4514,13 +4614,19 @@ void engine_make_fofloop_tasks_mapper(void *map_data, int num_elements,
           const int cjd = cell_getid(cdim, iii, jjj, kkk);
           struct cell *cj = &cells[cjd];
 
-          /* Is that neighbour local and does it have particles ? */
-          if (cid >= cjd || cj->grav.count == 0 || (ci->nodeID != cj->nodeID))
-            continue;
+          /* Does that neighbour have particles ? */
+          if (cid >= cjd || cj->grav.count == 0) continue;
 
-          /* Construct the pair task */
-          scheduler_addtask(sched, task_type_fof_pair, task_subtype_none, 0, 0,
-                            ci, cj);
+          /* Construct the pair search task only for fully local pairs */
+          if (ci->nodeID == nodeID && cj->nodeID == nodeID)
+            scheduler_addtask(sched, task_type_fof_pair, task_subtype_none, 0,
+                              0, ci, cj);
+
+          /* Construct the pair search task for pairs overlapping with the node
+           */
+          if (ci->nodeID == nodeID || cj->nodeID == nodeID)
+            scheduler_addtask(sched, task_type_fof_attach_pair,
+                              task_subtype_none, 0, 0, ci, cj);
         }
       }
     }
@@ -4538,7 +4644,10 @@ void engine_make_fof_tasks(struct engine *e) {
   struct scheduler *sched = &e->sched;
   ticks tic = getticks();
 
-  if (e->restarting) error("Running FOF on a restart step!");
+  if (e->restarting) {
+    /* Re-set the scheduler. */
+    scheduler_reset(sched, engine_estimate_nr_tasks(e));
+  }
 
   /* Construct a FOF loop over neighbours */
   if (e->policy & engine_policy_fof)
@@ -4714,8 +4823,13 @@ void engine_maketasks(struct engine *e) {
   tic2 = getticks();
 
   /* Add the dependencies for the gravity stuff */
-  if (e->policy & (engine_policy_self_gravity | engine_policy_external_gravity))
-    engine_link_gravity_tasks(e);
+  if (e->policy &
+      (engine_policy_self_gravity | engine_policy_external_gravity)) {
+
+    threadpool_map(&e->threadpool, engine_link_gravity_tasks_mapper,
+                   e->sched.tasks, e->sched.nr_tasks, sizeof(struct task),
+                   threadpool_auto_chunk_size, e);
+  }
 
   if (e->verbose)
     message("Linking gravity tasks took %.3f %s.",
@@ -4833,7 +4947,7 @@ void engine_maketasks(struct engine *e) {
   tic2 = getticks();
 
   /* Set the unlocks per task. */
-  scheduler_set_unlocks(sched);
+  scheduler_set_unlocks(sched, &e->threadpool);
 
   if (e->verbose)
     message("Setting unlocks took %.3f %s.",
diff --git a/src/engine_marktasks.c b/src/engine_marktasks.c
deleted file mode 100644
index a5e6ad5b2ef6697938340b5b4c3c833d28876694..0000000000000000000000000000000000000000
--- a/src/engine_marktasks.c
+++ /dev/null
@@ -1,1684 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2012 Pedro Gonnet (pedro.gonnet@durham.ac.uk)
- *                    Matthieu Schaller (schaller@strw.leidenuniv.nl)
- *               2015 Peter W. Draper (p.w.draper@durham.ac.uk)
- *                    Angus Lepper (angus.lepper@ed.ac.uk)
- *               2016 John A. Regan (john.a.regan@durham.ac.uk)
- *                    Tom Theuns (tom.theuns@durham.ac.uk)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-/* Config parameters. */
-#include <config.h>
-
-/* Some standard headers. */
-#include <stdlib.h>
-#include <unistd.h>
-
-/* MPI headers. */
-#ifdef WITH_MPI
-#include <mpi.h>
-#endif
-
-/* Load the profiler header, if needed. */
-#ifdef WITH_PROFILER
-#include <gperftools/profiler.h>
-#endif
-
-/* This object's header. */
-#include "engine.h"
-
-/* Local headers. */
-#include "active.h"
-#include "atomic.h"
-#include "cell.h"
-#include "clocks.h"
-#include "cycle.h"
-#include "debug.h"
-#include "error.h"
-#include "feedback.h"
-#include "proxy.h"
-#include "timers.h"
-
-/**
- * @brief Mark tasks to be un-skipped and set the sort flags accordingly.
- *        Threadpool mapper function.
- *
- * @param map_data pointer to the tasks
- * @param num_elements number of tasks
- * @param extra_data pointer to int that will define if a rebuild is needed.
- */
-void engine_marktasks_mapper(void *map_data, int num_elements,
-                             void *extra_data) {
-  /* Unpack the arguments. */
-  struct task *tasks = (struct task *)map_data;
-  size_t *rebuild_space = &((size_t *)extra_data)[1];
-  struct scheduler *s = (struct scheduler *)(((size_t *)extra_data)[2]);
-  struct engine *e = (struct engine *)((size_t *)extra_data)[0];
-  const int nodeID = e->nodeID;
-  const int with_timestep_limiter = e->policy & engine_policy_timestep_limiter;
-  const int with_timestep_sync = e->policy & engine_policy_timestep_sync;
-  const int with_sinks = e->policy & engine_policy_sinks;
-  const int with_stars = e->policy & engine_policy_stars;
-  const int with_star_formation = (e->policy & engine_policy_star_formation);
-  const int with_star_formation_sink = with_sinks && with_stars;
-  const int with_feedback = e->policy & engine_policy_feedback;
-
-  for (int ind = 0; ind < num_elements; ind++) {
-
-    /* Get basic task information */
-    struct task *t = &tasks[ind];
-    const enum task_types t_type = t->type;
-    const enum task_subtypes t_subtype = t->subtype;
-
-    /* Single-cell task? */
-    if (t_type == task_type_self || t_type == task_type_sub_self) {
-
-      /* Local pointer. */
-      struct cell *ci = t->ci;
-
-#ifdef SWIFT_DEBUG_CHECKS
-      if (ci->nodeID != nodeID) error("Non-local self task found");
-#endif
-
-      const int ci_active_hydro = cell_is_active_hydro(ci, e);
-      const int ci_active_gravity = cell_is_active_gravity(ci, e);
-      const int ci_active_black_holes = cell_is_active_black_holes(ci, e);
-      const int ci_active_sinks =
-          cell_is_active_sinks(ci, e) || ci_active_hydro;
-      const int ci_active_stars = cell_need_activating_stars(
-          ci, e, with_star_formation, with_star_formation_sink);
-      const int ci_active_rt = cell_is_rt_active(ci, e);
-
-      /* Activate the hydro drift */
-      if (t_type == task_type_self && t_subtype == task_subtype_density) {
-        if (ci_active_hydro) {
-          scheduler_activate(s, t);
-          cell_activate_drift_part(ci, s);
-          if (with_timestep_limiter) cell_activate_limiter(ci, s);
-        }
-      }
-
-      /* Store current values of dx_max and h_max. */
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_density) {
-        if (ci_active_hydro) {
-          scheduler_activate(s, t);
-          cell_activate_subcell_hydro_tasks(ci, NULL, s, with_timestep_limiter);
-          if (with_timestep_limiter) cell_activate_limiter(ci, s);
-        }
-      }
-
-      else if (t_type == task_type_self && t_subtype == task_subtype_force) {
-        if (ci_active_hydro) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_force) {
-        if (ci_active_hydro) scheduler_activate(s, t);
-      }
-
-      else if (t->type == task_type_self &&
-               t->subtype == task_subtype_limiter) {
-        if (ci_active_hydro) scheduler_activate(s, t);
-      }
-
-      else if (t->type == task_type_sub_self &&
-               t->subtype == task_subtype_limiter) {
-        if (ci_active_hydro) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_self && t_subtype == task_subtype_gradient) {
-        if (ci_active_hydro) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_gradient) {
-        if (ci_active_hydro) scheduler_activate(s, t);
-      }
-
-      /* Activate the star density */
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_stars_density) {
-        if (ci_active_stars) {
-          scheduler_activate(s, t);
-          cell_activate_drift_part(ci, s);
-          cell_activate_drift_spart(ci, s);
-          if (with_timestep_sync) cell_activate_sync_part(ci, s);
-        }
-      }
-
-      /* Store current values of dx_max and h_max. */
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_stars_density) {
-        if (ci_active_stars) {
-          scheduler_activate(s, t);
-          cell_activate_subcell_stars_tasks(ci, NULL, s, with_star_formation,
-                                            with_star_formation_sink,
-                                            with_timestep_sync);
-        }
-      }
-
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_stars_prep1) {
-        if (ci_active_stars) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_stars_prep1) {
-        if (ci_active_stars) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_stars_prep2) {
-        if (ci_active_stars) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_stars_prep2) {
-        if (ci_active_stars) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_stars_feedback) {
-        if (ci_active_stars) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_stars_feedback) {
-        if (ci_active_stars) scheduler_activate(s, t);
-      }
-
-      /* Activate the sink formation */
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_sink_swallow) {
-        if (ci_active_sinks) {
-          scheduler_activate(s, t);
-          cell_activate_drift_part(ci, s);
-          cell_activate_drift_sink(ci, s);
-          cell_activate_sink_formation_tasks(ci->top, s);
-          if (with_timestep_sync) cell_activate_sync_part(ci, s);
-        }
-      }
-
-      /* Store current values of dx_max and h_max. */
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_sink_swallow) {
-        if (ci_active_sinks) {
-          scheduler_activate(s, t);
-          cell_activate_subcell_sinks_tasks(ci, NULL, s, with_timestep_sync);
-        }
-      }
-
-      /* Activate the sink merger */
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_sink_do_sink_swallow) {
-        if (ci_active_sinks) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_sink_do_sink_swallow) {
-        if (ci_active_sinks) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      /* Activate the sink accretion */
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_sink_do_gas_swallow) {
-        if (ci_active_sinks) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_sink_do_gas_swallow) {
-        if (ci_active_sinks) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      /* Activate the black hole density */
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_bh_density) {
-        if (ci_active_black_holes) {
-          scheduler_activate(s, t);
-          cell_activate_drift_part(ci, s);
-          cell_activate_drift_bpart(ci, s);
-          if (with_timestep_sync) cell_activate_sync_part(ci, s);
-        }
-      }
-
-      /* Store current values of dx_max and h_max. */
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_bh_density) {
-        if (ci_active_black_holes) {
-          scheduler_activate(s, t);
-          cell_activate_subcell_black_holes_tasks(ci, NULL, s,
-                                                  with_timestep_sync);
-        }
-      }
-
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_bh_swallow) {
-        if (ci_active_black_holes) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_bh_swallow) {
-        if (ci_active_black_holes) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_do_gas_swallow) {
-        if (ci_active_black_holes) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_do_gas_swallow) {
-        if (ci_active_black_holes) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_do_bh_swallow) {
-        if (ci_active_black_holes) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_do_bh_swallow) {
-        if (ci_active_black_holes) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_bh_feedback) {
-        if (ci_active_black_holes) {
-          scheduler_activate(s, t);
-        }
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_bh_feedback) {
-        if (ci_active_black_holes) scheduler_activate(s, t);
-      }
-
-      /* Activate the gravity drift */
-      else if (t_type == task_type_self && t_subtype == task_subtype_grav) {
-        if (ci_active_gravity) {
-          scheduler_activate(s, t);
-          cell_activate_subcell_grav_tasks(t->ci, NULL, s);
-        }
-      }
-
-      /* Activate the gravity drift */
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_external_grav) {
-        if (ci_active_gravity) {
-          scheduler_activate(s, t);
-          cell_activate_drift_gpart(t->ci, s);
-        }
-      }
-
-      /* Activate RT tasks */
-      else if (t_type == task_type_self &&
-               t_subtype == task_subtype_rt_gradient) {
-        if (ci_active_rt) scheduler_activate(s, t);
-      }
-
-      else if (t_type == task_type_sub_self &&
-               t_subtype == task_subtype_rt_gradient) {
-        if (ci_active_rt) {
-          scheduler_activate(s, t);
-          cell_activate_subcell_rt_tasks(ci, NULL, s, /*sub_cycle=*/0);
-        }
-      }
-
-      else if (t_subtype == task_subtype_rt_transport) {
-        if (ci_active_rt) scheduler_activate(s, t);
-      }
-
-#ifdef SWIFT_DEBUG_CHECKS
-      else {
-        error("Invalid task type / sub-type encountered");
-      }
-#endif
-    }
-
-    /* Pair? */
-    else if (t_type == task_type_pair || t_type == task_type_sub_pair) {
-
-      /* Local pointers. */
-      struct cell *ci = t->ci;
-      struct cell *cj = t->cj;
-#ifdef WITH_MPI
-      const int ci_nodeID = ci->nodeID;
-      const int cj_nodeID = cj->nodeID;
-#else
-      const int ci_nodeID = nodeID;
-      const int cj_nodeID = nodeID;
-#endif
-      const int ci_active_hydro = cell_is_active_hydro(ci, e);
-      const int cj_active_hydro = cell_is_active_hydro(cj, e);
-
-      const int ci_active_gravity = cell_is_active_gravity(ci, e);
-      const int cj_active_gravity = cell_is_active_gravity(cj, e);
-
-      const int ci_active_black_holes = cell_is_active_black_holes(ci, e);
-      const int cj_active_black_holes = cell_is_active_black_holes(cj, e);
-
-      const int ci_active_sinks =
-          cell_is_active_sinks(ci, e) || ci_active_hydro;
-      const int cj_active_sinks =
-          cell_is_active_sinks(cj, e) || cj_active_hydro;
-
-      const int ci_active_stars = cell_need_activating_stars(
-          ci, e, with_star_formation, with_star_formation_sink);
-      const int cj_active_stars = cell_need_activating_stars(
-          cj, e, with_star_formation, with_star_formation_sink);
-
-      const int ci_active_rt = cell_is_rt_active(ci, e);
-      const int cj_active_rt = cell_is_rt_active(cj, e);
-
-      /* Only activate tasks that involve a local active cell. */
-      if ((t_subtype == task_subtype_density ||
-           t_subtype == task_subtype_gradient ||
-           t_subtype == task_subtype_limiter ||
-           t_subtype == task_subtype_force) &&
-          ((ci_active_hydro && ci_nodeID == nodeID) ||
-           (cj_active_hydro && cj_nodeID == nodeID))) {
-
-        scheduler_activate(s, t);
-
-        /* Set the correct sorting flags */
-        if (t_type == task_type_pair && t_subtype == task_subtype_density) {
-
-          /* Store some values. */
-          atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-          atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-          ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
-          cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
-
-          /* Activate the hydro drift tasks. */
-          if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-          if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
-
-          /* And the limiter */
-          if (ci_nodeID == nodeID && with_timestep_limiter)
-            cell_activate_limiter(ci, s);
-          if (cj_nodeID == nodeID && with_timestep_limiter)
-            cell_activate_limiter(cj, s);
-
-          /* Check the sorts and activate them if needed. */
-          cell_activate_hydro_sorts(ci, t->flags, s);
-          cell_activate_hydro_sorts(cj, t->flags, s);
-
-        }
-
-        /* Store current values of dx_max and h_max. */
-        else if (t_type == task_type_sub_pair &&
-                 t_subtype == task_subtype_density) {
-          cell_activate_subcell_hydro_tasks(t->ci, t->cj, s,
-                                            with_timestep_limiter);
-        }
-      }
-
-      /* Stars density */
-      else if ((t_subtype == task_subtype_stars_density) &&
-               (ci_active_stars || cj_active_stars) &&
-               (ci_nodeID == nodeID || cj_nodeID == nodeID)) {
-
-        scheduler_activate(s, t);
-
-        /* Set the correct sorting flags */
-        if (t_type == task_type_pair) {
-
-          /* Add stars_in dependencies for each cell that is part of
-           * a pair task as to not miss any dependencies */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->stars.stars_in);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->stars.stars_in);
-
-          /* Do ci */
-          if (ci_active_stars) {
-
-            /* stars for ci */
-            atomic_or(&ci->stars.requires_sorts, 1 << t->flags);
-            ci->stars.dx_max_sort_old = ci->stars.dx_max_sort;
-
-            /* hydro for cj */
-            atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-            cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
-
-            /* Activate the drift tasks. */
-            if (ci_nodeID == nodeID) cell_activate_drift_spart(ci, s);
-            if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
-            if (cj_nodeID == nodeID && with_timestep_sync)
-              cell_activate_sync_part(cj, s);
-
-            /* Check the sorts and activate them if needed. */
-            cell_activate_hydro_sorts(cj, t->flags, s);
-            cell_activate_stars_sorts(ci, t->flags, s);
-          }
-
-          /* Do cj */
-          if (cj_active_stars) {
-
-            /* hydro for ci */
-            atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-            ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
-
-            /* stars for cj */
-            atomic_or(&cj->stars.requires_sorts, 1 << t->flags);
-            cj->stars.dx_max_sort_old = cj->stars.dx_max_sort;
-
-            /* Activate the drift tasks. */
-            if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-            if (cj_nodeID == nodeID) cell_activate_drift_spart(cj, s);
-            if (ci_nodeID == nodeID && with_timestep_sync)
-              cell_activate_sync_part(ci, s);
-
-            /* Check the sorts and activate them if needed. */
-            cell_activate_hydro_sorts(ci, t->flags, s);
-            cell_activate_stars_sorts(cj, t->flags, s);
-          }
-        }
-
-        /* Store current values of dx_max and h_max. */
-        else if (t_type == task_type_sub_pair &&
-                 t_subtype == task_subtype_stars_density) {
-
-          /* Add stars_in dependencies for each cell that is part of
-           * a pair/sub_pair task as to not miss any dependencies */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->stars.stars_in);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->stars.stars_in);
-
-          cell_activate_subcell_stars_tasks(ci, cj, s, with_star_formation,
-                                            with_star_formation_sink,
-                                            with_timestep_sync);
-        }
-      }
-
-      /* Stars prep1 */
-      else if (t_subtype == task_subtype_stars_prep1) {
-
-        /* We only want to activate the task if the cell is active and is
-           going to update some gas on the *local* node */
-        if ((ci_nodeID == nodeID && cj_nodeID == nodeID) &&
-            (ci_active_stars || cj_active_stars)) {
-
-          scheduler_activate(s, t);
-
-          /* If there are active sparts in ci, activate hydro ghost in cj */
-          if (ci_active_stars)
-            scheduler_activate(s, cj->hydro.super->hydro.prep1_ghost);
-          /* If there are active sparts in cj, activate hydro ghost in ci */
-          if (cj_active_stars)
-            scheduler_activate(s, ci->hydro.super->hydro.prep1_ghost);
-
-        } else if ((ci_nodeID == nodeID && cj_nodeID != nodeID) &&
-                   (cj_active_stars)) {
-
-          scheduler_activate(s, t);
-          /* If there are active sparts in cj, activate hydro ghost in ci */
-          scheduler_activate(s, ci->hydro.super->hydro.prep1_ghost);
-
-        } else if ((ci_nodeID != nodeID && cj_nodeID == nodeID) &&
-                   (ci_active_stars)) {
-
-          scheduler_activate(s, t);
-          /* If there are active sparts in ci, activate hydro ghost in cj */
-          scheduler_activate(s, cj->hydro.super->hydro.prep1_ghost);
-        }
-      }
-
-      /* Stars prep2 */
-      else if (t_subtype == task_subtype_stars_prep2) {
-
-        /* We only want to activate the task if the cell is active and is
-           going to update some sparts on the *local* node */
-        if ((ci_nodeID == nodeID && cj_nodeID == nodeID) &&
-            (ci_active_stars || cj_active_stars)) {
-
-          scheduler_activate(s, t);
-
-        } else if ((ci_nodeID == nodeID && cj_nodeID != nodeID) &&
-                   (ci_active_stars)) {
-
-          scheduler_activate(s, t);
-
-        } else if ((ci_nodeID != nodeID && cj_nodeID == nodeID) &&
-                   (cj_active_stars)) {
-
-          scheduler_activate(s, t);
-        }
-      }
-
-      /* Stars feedback */
-      else if (t_subtype == task_subtype_stars_feedback) {
-
-        /* We only want to activate the task if the cell is active and is
-           going to update some gas on the *local* node */
-        if ((ci_nodeID == nodeID && cj_nodeID == nodeID) &&
-            (ci_active_stars || cj_active_stars)) {
-
-          scheduler_activate(s, t);
-
-        } else if ((ci_nodeID == nodeID && cj_nodeID != nodeID) &&
-                   (cj_active_stars)) {
-
-          scheduler_activate(s, t);
-
-        } else if ((ci_nodeID != nodeID && cj_nodeID == nodeID) &&
-                   (ci_active_stars)) {
-
-          scheduler_activate(s, t);
-        }
-
-        if (t->type == task_type_pair || t->type == task_type_sub_pair) {
-          /* Add stars_out dependencies for each cell that is part of
-           * a pair/sub_pair task as to not miss any dependencies */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->stars.stars_out);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->stars.stars_out);
-        }
-      }
-
-      /* Black_Holes density */
-      else if ((t_subtype == task_subtype_bh_density ||
-                t_subtype == task_subtype_bh_swallow ||
-                t_subtype == task_subtype_do_gas_swallow ||
-                t_subtype == task_subtype_do_bh_swallow ||
-                t_subtype == task_subtype_bh_feedback) &&
-               (ci_active_black_holes || cj_active_black_holes) &&
-               (ci_nodeID == nodeID || cj_nodeID == nodeID)) {
-
-        scheduler_activate(s, t);
-
-        /* Set the correct drifting flags */
-        if (t_type == task_type_pair && t_subtype == task_subtype_bh_density) {
-          if (ci_nodeID == nodeID) cell_activate_drift_bpart(ci, s);
-          if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-
-          if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
-          if (cj_nodeID == nodeID) cell_activate_drift_bpart(cj, s);
-
-          /* Activate bh_in for each cell that is part of
-           * a pair task as to not miss any dependencies */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->black_holes.black_holes_in);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->black_holes.black_holes_in);
-        }
-
-        if ((t_type == task_type_pair || t_type == task_type_sub_pair) &&
-            t_subtype == task_subtype_bh_feedback) {
-          /* Add bh_out dependencies for each cell that is part of
-           * a pair/sub_pair task as to not miss any dependencies */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->black_holes.black_holes_out);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->black_holes.black_holes_out);
-        }
-
-        /* Store current values of dx_max and h_max. */
-        else if (t_type == task_type_sub_pair &&
-                 t_subtype == task_subtype_bh_density) {
-          cell_activate_subcell_black_holes_tasks(ci, cj, s,
-                                                  with_timestep_sync);
-          /* Activate bh_in for each cell that is part of
-           * a sub_pair task as to not miss any dependencies */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->black_holes.black_holes_in);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->black_holes.black_holes_in);
-        }
-      }
-
-      /* Gravity */
-      else if ((t_subtype == task_subtype_grav) &&
-               ((ci_active_gravity && ci_nodeID == nodeID) ||
-                (cj_active_gravity && cj_nodeID == nodeID))) {
-
-        scheduler_activate(s, t);
-
-        if (t_type == task_type_pair && t_subtype == task_subtype_grav) {
-          /* Activate the gravity drift */
-          cell_activate_subcell_grav_tasks(t->ci, t->cj, s);
-        }
-
-#ifdef SWIFT_DEBUG_CHECKS
-        else if (t_type == task_type_sub_pair &&
-                 t_subtype == task_subtype_grav) {
-          error("Invalid task sub-type encountered");
-        }
-#endif
-      }
-
-      /* Sink formation */
-      else if ((t_subtype == task_subtype_sink_swallow ||
-                t_subtype == task_subtype_sink_do_sink_swallow ||
-                t_subtype == task_subtype_sink_do_gas_swallow) &&
-               (ci_active_sinks || cj_active_sinks) &&
-               (ci_nodeID == nodeID || cj_nodeID == nodeID)) {
-
-        scheduler_activate(s, t);
-
-        /* Set the correct sorting flags */
-        if (t_type == task_type_pair &&
-            t_subtype == task_subtype_sink_swallow) {
-
-          /* Activate the sink drift for the sink merger */
-          if (ci_nodeID == nodeID) {
-            cell_activate_drift_sink(ci, s);
-            cell_activate_sink_formation_tasks(ci->top, s);
-            /* Activate all sink_in tasks for each cell involved
-             * in pair type tasks */
-            scheduler_activate(s, ci->hydro.super->sinks.sink_in);
-          }
-
-          if (cj_nodeID == nodeID) {
-            cell_activate_drift_sink(cj, s);
-            if (ci->top != cj->top) {
-              cell_activate_sink_formation_tasks(cj->top, s);
-            }
-            /* Activate all sink_in tasks for each cell involved
-             * in pair type tasks */
-            scheduler_activate(s, cj->hydro.super->sinks.sink_in);
-          }
-
-          /* Do ci */
-          if (ci_active_sinks) {
-
-            /* hydro for cj */
-            atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-            cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
-
-            /* Activate the drift tasks. */
-            if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
-            if (cj_nodeID == nodeID && with_timestep_sync)
-              cell_activate_sync_part(cj, s);
-
-            /* Check the sorts and activate them if needed. */
-            cell_activate_hydro_sorts(cj, t->flags, s);
-          }
-
-          /* Do cj */
-          if (cj_active_sinks) {
-
-            /* hydro for ci */
-            atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-            ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
-
-            /* Activate the drift tasks. */
-            /* Activate the sink drift for the merger */
-            if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-            if (ci_nodeID == nodeID && with_timestep_sync)
-              cell_activate_sync_part(ci, s);
-
-            /* Check the sorts and activate them if needed. */
-            cell_activate_hydro_sorts(ci, t->flags, s);
-          }
-        }
-
-        else if (t_type == task_type_sub_pair &&
-                 t_subtype == task_subtype_sink_swallow) {
-          /* Activate all sink_in tasks for each cell involved
-           * in sub_pair type tasks */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->sinks.sink_in);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->sinks.sink_in);
-
-          /* Store current values of dx_max and h_max. */
-          cell_activate_subcell_sinks_tasks(ci, cj, s, with_timestep_sync);
-        }
-
-        else if ((t_type == task_type_pair || t_type == task_type_sub_pair) &&
-                 t_subtype == task_subtype_sink_do_gas_swallow) {
-          /* Activate sinks_out for each cell that is part of
-           * a pair/sub_pair task as to not miss any dependencies */
-          if (ci_nodeID == nodeID)
-            scheduler_activate(s, ci->hydro.super->sinks.sink_out);
-          if (cj_nodeID == nodeID)
-            scheduler_activate(s, cj->hydro.super->sinks.sink_out);
-        }
-      }
-
-      /* RT gradient and transport tasks */
-      else if (t_subtype == task_subtype_rt_gradient) {
-
-        /* We only want to activate the task if the cell is active and is
-           going to update some gas on the *local* node */
-
-        if ((ci_nodeID == nodeID && ci_active_rt) ||
-            (cj_nodeID == nodeID && cj_active_rt)) {
-
-          scheduler_activate(s, t);
-
-          /* Set the correct sorting flags */
-          if (t_type == task_type_pair) {
-
-            /* Store some values. */
-            atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-            atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-            ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
-            cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
-
-            /* Check the sorts and activate them if needed. */
-            cell_activate_rt_sorts(ci, t->flags, s);
-            cell_activate_rt_sorts(cj, t->flags, s);
-          }
-
-          /* Store current values of dx_max and h_max. */
-          else if (t_type == task_type_sub_pair) {
-            cell_activate_subcell_rt_tasks(ci, cj, s, /*sub_cycle=*/0);
-          }
-        }
-      }
-
-      else if (t_subtype == task_subtype_rt_transport) {
-        /* We only want to activate the task if the cell is active and is
-           going to update some gas on the *local* node */
-
-        if ((ci_nodeID == nodeID && ci_active_rt) ||
-            (cj_nodeID == nodeID && cj_active_rt)) {
-
-          /* The gradient and transport task subtypes mirror the hydro tasks.
-           * Therefore all the (subcell) sorts and drifts should already have
-           * been activated properly in the hydro part of the activation. */
-          scheduler_activate(s, t);
-
-          if (t_type == task_type_pair || t_type == task_type_sub_pair) {
-            /* Activate transport_out for each cell that is part of
-             * a pair/sub_pair task as to not miss any dependencies */
-            if (ci_nodeID == nodeID)
-              scheduler_activate(s, ci->hydro.super->rt.rt_transport_out);
-            if (cj_nodeID == nodeID)
-              scheduler_activate(s, cj->hydro.super->rt.rt_transport_out);
-          }
-        }
-      }
-
-      /* Pair tasks between inactive local cells and active remote cells. */
-      if ((ci_nodeID != nodeID && cj_nodeID == nodeID && ci_active_hydro &&
-           !cj_active_hydro) ||
-          (ci_nodeID == nodeID && cj_nodeID != nodeID && !ci_active_hydro &&
-           cj_active_hydro)) {
-
-#if defined(WITH_MPI) && defined(MPI_SYMMETRIC_FORCE_INTERACTION)
-        if (t_subtype == task_subtype_force) {
-
-          scheduler_activate(s, t);
-
-          /* Set the correct sorting flags */
-          if (t_type == task_type_pair) {
-            /* Store some values. */
-            atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-            atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-            ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
-            cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
-
-            /* Activate the hydro drift tasks. */
-            if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-            if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
-
-            /* And the limiter */
-            if (ci_nodeID == nodeID && with_timestep_limiter)
-              cell_activate_limiter(ci, s);
-            if (cj_nodeID == nodeID && with_timestep_limiter)
-              cell_activate_limiter(cj, s);
-
-            /* Check the sorts and activate them if needed. */
-            cell_activate_hydro_sorts(ci, t->flags, s);
-            cell_activate_hydro_sorts(cj, t->flags, s);
-
-          }
-
-          /* Store current values of dx_max and h_max. */
-          else if (t_type == task_type_sub_pair) {
-            cell_activate_subcell_hydro_tasks(t->ci, t->cj, s,
-                                              with_timestep_limiter);
-          }
-        }
-      }
-
-      /* Pair tasks between inactive local cells and active remote cells. */
-      if ((ci_nodeID != nodeID && cj_nodeID == nodeID && ci_active_rt &&
-           !cj_active_rt) ||
-          (ci_nodeID == nodeID && cj_nodeID != nodeID && !ci_active_rt &&
-           cj_active_rt)) {
-
-        if (t_subtype == task_subtype_rt_transport) {
-
-          scheduler_activate(s, t);
-
-          /* Set the correct sorting flags */
-          if (t_type == task_type_pair) {
-
-            /* Store some values. */
-            atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-            atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-            ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
-            cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
-
-            /* Check the sorts and activate them if needed. */
-            cell_activate_rt_sorts(ci, t->flags, s);
-            cell_activate_rt_sorts(cj, t->flags, s);
-          }
-
-          /* Store current values of dx_max and h_max. */
-          else if (t_type == task_type_sub_pair) {
-            cell_activate_subcell_rt_tasks(ci, cj, s, /*sub_cycle=*/0);
-          }
-        }
-#endif
-      }
-
-      /* Only interested in density tasks as of here. */
-      if (t_subtype == task_subtype_density) {
-
-        /* Too much particle movement? */
-        if (cell_need_rebuild_for_hydro_pair(ci, cj)) *rebuild_space = 1;
-
-#ifdef WITH_MPI
-        /* Activate the send/recv tasks. */
-        if (ci_nodeID != nodeID) {
-
-          /* If the local cell is active, receive data from the foreign cell. */
-          if (cj_active_hydro) {
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_xv);
-            if (ci_active_hydro) {
-              scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rho);
-#ifdef EXTRA_HYDRO_LOOP
-              scheduler_activate_recv(s, ci->mpi.recv, task_subtype_gradient);
-#endif
-            }
-          }
-          /* If the local cell is inactive and the remote cell is active, we
-           * still need to receive stuff to be able to do the force interaction
-           * on this node as well. */
-          else if (ci_active_hydro) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* NOTE: (yuyttenh, 09/2022) Since the particle communications send
-             * over whole particles currently, just activating the gradient
-             * send/recieve should be enough for now. The remote active
-             * particles are only needed for the sorts and the flux exchange on
-             * the node of the inactive cell, so sending over the xv and
-             * gradient suffices. If at any point the commutications change, we
-             * should probably also send over the rho separately. */
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_xv);
-#ifndef EXTRA_HYDRO_LOOP
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rho);
-#else
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_gradient);
-#endif
-#endif
-          }
-
-          /* If the foreign cell is active, we want its particles for the
-           * limiter */
-          if (ci_active_hydro && with_timestep_limiter) {
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_limiter);
-            scheduler_activate_unpack(s, ci->mpi.unpack, task_subtype_limiter);
-          }
-
-          /* Is the foreign cell active and will need stuff from us? */
-          if (ci_active_hydro) {
-            struct link *l = scheduler_activate_send(
-                s, cj->mpi.send, task_subtype_xv, ci_nodeID);
-
-            /* Drift the cell which will be sent at the level at which it is
-               sent, i.e. drift the cell specified in the send task (l->t)
-               itself. */
-            cell_activate_drift_part(l->t->ci, s);
-
-            /* If the local cell is also active, more stuff will be needed. */
-            if (cj_active_hydro) {
-              scheduler_activate_send(s, cj->mpi.send, task_subtype_rho,
-                                      ci_nodeID);
-
-#ifdef EXTRA_HYDRO_LOOP
-              scheduler_activate_send(s, cj->mpi.send, task_subtype_gradient,
-                                      ci_nodeID);
-#endif
-            }
-          }
-          /* If the foreign cell is inactive, but the local cell is active,
-           * we still need to send stuff to be able to do the force interaction
-           * on both nodes */
-          else if (cj_active_hydro) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* See NOTE on line 867 */
-            struct link *l = scheduler_activate_send(
-                s, cj->mpi.send, task_subtype_xv, ci_nodeID);
-            /* Drift the cell which will be sent at the level at which it is
-             * sent, i.e. drift the cell specified in the send task (l->t)
-             * itself. */
-            cell_activate_drift_part(l->t->ci, s);
-#ifndef EXTRA_HYDRO_LOOP
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_rho,
-                                    ci_nodeID);
-#else
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_gradient,
-                                    ci_nodeID);
-#endif
-#endif
-          }
-
-          /* If the local cell is active, send its particles for the limiting.
-           */
-          if (cj_active_hydro && with_timestep_limiter) {
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_limiter,
-                                    ci_nodeID);
-            scheduler_activate_pack(s, cj->mpi.pack, task_subtype_limiter,
-                                    ci_nodeID);
-          }
-
-          /* Propagating new star counts? */
-          if (with_star_formation_sink) error("TODO");
-          if (with_star_formation && with_feedback) {
-            if (ci_active_hydro && ci->hydro.count > 0) {
-              scheduler_activate_recv(s, ci->mpi.recv, task_subtype_sf_counts);
-            }
-            if (cj_active_hydro && cj->hydro.count > 0) {
-              scheduler_activate_send(s, cj->mpi.send, task_subtype_sf_counts,
-                                      ci_nodeID);
-            }
-          }
-
-        } else if (cj_nodeID != nodeID) {
-
-          /* If the local cell is active, receive data from the foreign cell. */
-          if (ci_active_hydro) {
-
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_xv);
-            if (cj_active_hydro) {
-              scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rho);
-#ifdef EXTRA_HYDRO_LOOP
-              scheduler_activate_recv(s, cj->mpi.recv, task_subtype_gradient);
-#endif
-            }
-          }
-          /* If the local cell is inactive and the remote cell is active, we
-           * still need to receive stuff to be able to do the force interaction
-           * on this node as well. */
-          else if (cj_active_hydro) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* See NOTE on line 867. */
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_xv);
-#ifndef EXTRA_HYDRO_LOOP
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rho);
-#else
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_gradient);
-#endif
-#endif
-          }
-
-          /* If the foreign cell is active, we want its particles for the
-           * limiter */
-          if (cj_active_hydro && with_timestep_limiter) {
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_limiter);
-            scheduler_activate_unpack(s, cj->mpi.unpack, task_subtype_limiter);
-          }
-
-          /* Is the foreign cell active and will need stuff from us? */
-          if (cj_active_hydro) {
-
-            struct link *l = scheduler_activate_send(
-                s, ci->mpi.send, task_subtype_xv, cj_nodeID);
-
-            /* Drift the cell which will be sent at the level at which it is
-               sent, i.e. drift the cell specified in the send task (l->t)
-               itself. */
-            cell_activate_drift_part(l->t->ci, s);
-
-            /* If the local cell is also active, more stuff will be needed. */
-            if (ci_active_hydro) {
-
-              scheduler_activate_send(s, ci->mpi.send, task_subtype_rho,
-                                      cj_nodeID);
-
-#ifdef EXTRA_HYDRO_LOOP
-              scheduler_activate_send(s, ci->mpi.send, task_subtype_gradient,
-                                      cj_nodeID);
-#endif
-            }
-          }
-          /* If the foreign cell is inactive, but the local cell is active,
-           * we still need to send stuff to be able to do the force interaction
-           * on both nodes */
-          else if (ci_active_hydro) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* See NOTE on line 867. */
-            struct link *l = scheduler_activate_send(
-                s, ci->mpi.send, task_subtype_xv, cj_nodeID);
-            /* Drift the cell which will be sent at the level at which it is
-             * sent, i.e. drift the cell specified in the send task (l->t)
-             * itself. */
-            cell_activate_drift_part(l->t->ci, s);
-#ifndef EXTRA_HYDRO_LOOP
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_rho,
-                                    cj_nodeID);
-#else
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_gradient,
-                                    cj_nodeID);
-#endif
-#endif
-          }
-
-          /* If the local cell is active, send its particles for the limiting.
-           */
-          if (ci_active_hydro && with_timestep_limiter) {
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_limiter,
-                                    cj_nodeID);
-            scheduler_activate_pack(s, ci->mpi.pack, task_subtype_limiter,
-                                    cj_nodeID);
-          }
-
-          /* Propagating new star counts? */
-          if (with_star_formation_sink) error("TODO");
-          if (with_star_formation && with_feedback) {
-            if (cj_active_hydro && cj->hydro.count > 0) {
-              scheduler_activate_recv(s, cj->mpi.recv, task_subtype_sf_counts);
-            }
-            if (ci_active_hydro && ci->hydro.count > 0) {
-              scheduler_activate_send(s, ci->mpi.send, task_subtype_sf_counts,
-                                      cj_nodeID);
-            }
-          }
-        }
-#endif
-      }
-
-      /* Only interested in stars_density tasks as of here. */
-      else if (t->subtype == task_subtype_stars_density) {
-
-        /* Too much particle movement? */
-        if (cell_need_rebuild_for_stars_pair(ci, cj)) *rebuild_space = 1;
-        if (cell_need_rebuild_for_stars_pair(cj, ci)) *rebuild_space = 1;
-
-#ifdef WITH_MPI
-        /* Activate the send/recv tasks. */
-        if (ci_nodeID != nodeID) {
-
-          if (cj_active_stars) {
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_xv);
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rho);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_part_prep1);
-#endif
-
-            /* If the local cell is active, more stuff will be needed. */
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_spart_density,
-                                    ci_nodeID);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_spart_prep2,
-                                    ci_nodeID);
-#endif
-            cell_activate_drift_spart(cj, s);
-          }
-
-          if (ci_active_stars) {
-            scheduler_activate_recv(s, ci->mpi.recv,
-                                    task_subtype_spart_density);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_spart_prep2);
-#endif
-
-            /* Is the foreign cell active and will need stuff from us? */
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_xv,
-                                    ci_nodeID);
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_rho,
-                                    ci_nodeID);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_part_prep1,
-                                    ci_nodeID);
-#endif
-
-            /* Drift the cell which will be sent; note that not all sent
-               particles will be drifted, only those that are needed. */
-            cell_activate_drift_part(cj, s);
-          }
-
-        } else if (cj_nodeID != nodeID) {
-
-          /* If the local cell is active, receive data from the foreign cell. */
-          if (ci_active_stars) {
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_xv);
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rho);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_part_prep1);
-#endif
-
-            /* If the local cell is active, more stuff will be needed. */
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_spart_density,
-                                    cj_nodeID);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_spart_prep2,
-                                    cj_nodeID);
-#endif
-            cell_activate_drift_spart(ci, s);
-          }
-
-          if (cj_active_stars) {
-            scheduler_activate_recv(s, cj->mpi.recv,
-                                    task_subtype_spart_density);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_spart_prep2);
-#endif
-
-            /* Is the foreign cell active and will need stuff from us? */
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_xv,
-                                    cj_nodeID);
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_rho,
-                                    cj_nodeID);
-#ifdef EXTRA_STAR_LOOPS
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_part_prep1,
-                                    cj_nodeID);
-#endif
-
-            /* Drift the cell which will be sent; note that not all sent
-               particles will be drifted, only those that are needed. */
-            cell_activate_drift_part(ci, s);
-          }
-        }
-#endif
-      }
-
-      /* Only interested in sink_swallow tasks as of here. */
-      else if (t->subtype == task_subtype_sink_swallow) {
-
-        /* Too much particle movement? */
-        if (cell_need_rebuild_for_sinks_pair(ci, cj)) *rebuild_space = 1;
-        if (cell_need_rebuild_for_sinks_pair(cj, ci)) *rebuild_space = 1;
-
-#ifdef WITH_MPI
-        error("TODO");
-#endif
-      }
-
-      /* Only interested in black hole density tasks as of here. */
-      else if (t->subtype == task_subtype_bh_density) {
-
-        /* Too much particle movement? */
-        if (cell_need_rebuild_for_black_holes_pair(ci, cj)) *rebuild_space = 1;
-        if (cell_need_rebuild_for_black_holes_pair(cj, ci)) *rebuild_space = 1;
-
-        scheduler_activate(s, ci->hydro.super->black_holes.swallow_ghost_0);
-        scheduler_activate(s, cj->hydro.super->black_holes.swallow_ghost_0);
-
-#ifdef WITH_MPI
-        /* Activate the send/recv tasks. */
-        if (ci_nodeID != nodeID) {
-
-          /* Receive the foreign parts to compute BH accretion rates and do the
-           * swallowing */
-          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rho);
-          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_part_swallow);
-          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_merger);
-
-          /* Send the local BHs to tag the particles to swallow and to do
-           * feedback */
-          scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_rho,
-                                  ci_nodeID);
-          scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_feedback,
-                                  ci_nodeID);
-
-          /* Drift before you send */
-          cell_activate_drift_bpart(cj, s);
-
-          /* Receive the foreign BHs to tag particles to swallow and for
-           * feedback */
-          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_rho);
-          scheduler_activate_recv(s, ci->mpi.recv, task_subtype_bpart_feedback);
-
-          /* Send the local part information */
-          scheduler_activate_send(s, cj->mpi.send, task_subtype_rho, ci_nodeID);
-          scheduler_activate_send(s, cj->mpi.send, task_subtype_part_swallow,
-                                  ci_nodeID);
-          scheduler_activate_send(s, cj->mpi.send, task_subtype_bpart_merger,
-                                  ci_nodeID);
-
-          /* Drift the cell which will be sent; note that not all sent
-             particles will be drifted, only those that are needed. */
-          cell_activate_drift_part(cj, s);
-
-        } else if (cj_nodeID != nodeID) {
-
-          /* Receive the foreign parts to compute BH accretion rates and do the
-           * swallowing */
-          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rho);
-          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_part_swallow);
-          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_merger);
-
-          /* Send the local BHs to tag the particles to swallow and to do
-           * feedback */
-          scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_rho,
-                                  cj_nodeID);
-          scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_feedback,
-                                  cj_nodeID);
-
-          /* Drift before you send */
-          cell_activate_drift_bpart(ci, s);
-
-          /* Receive the foreign BHs to tag particles to swallow and for
-           * feedback */
-          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_rho);
-          scheduler_activate_recv(s, cj->mpi.recv, task_subtype_bpart_feedback);
-
-          /* Send the local part information */
-          scheduler_activate_send(s, ci->mpi.send, task_subtype_rho, cj_nodeID);
-          scheduler_activate_send(s, ci->mpi.send, task_subtype_part_swallow,
-                                  cj_nodeID);
-          scheduler_activate_send(s, ci->mpi.send, task_subtype_bpart_merger,
-                                  cj_nodeID);
-
-          /* Drift the cell which will be sent; note that not all sent
-             particles will be drifted, only those that are needed. */
-          cell_activate_drift_part(ci, s);
-        }
-#endif
-      }
-
-      /* Only interested in gravity tasks as of here. */
-      else if (t_subtype == task_subtype_grav) {
-
-#ifdef WITH_MPI
-        /* Activate the send/recv tasks. */
-        if (ci_nodeID != nodeID) {
-
-          /* If the local cell is active, receive data from the foreign cell. */
-          if (cj_active_gravity)
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_gpart);
-
-          /* Is the foreign cell active and will need stuff from us? */
-          if (ci_active_gravity) {
-
-            struct link *l = scheduler_activate_send(
-                s, cj->mpi.send, task_subtype_gpart, ci_nodeID);
-
-            /* Drift the cell which will be sent at the level at which it is
-               sent, i.e. drift the cell specified in the send task (l->t)
-               itself. */
-            cell_activate_drift_gpart(l->t->ci, s);
-          }
-
-        } else if (cj_nodeID != nodeID) {
-
-          /* If the local cell is active, receive data from the foreign cell. */
-          if (ci_active_gravity)
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_gpart);
-
-          /* Is the foreign cell active and will need stuff from us? */
-          if (cj_active_gravity) {
-
-            struct link *l = scheduler_activate_send(
-                s, ci->mpi.send, task_subtype_gpart, cj_nodeID);
-
-            /* Drift the cell which will be sent at the level at which it is
-               sent, i.e. drift the cell specified in the send task (l->t)
-               itself. */
-            cell_activate_drift_gpart(l->t->ci, s);
-          }
-        }
-#endif
-      } /* Only interested in RT tasks as of here. */
-
-      else if (t->subtype == task_subtype_rt_gradient) {
-
-#ifdef WITH_MPI
-        /* Activate the send/recv tasks. */
-        if (ci_nodeID != nodeID) {
-
-          /* If the local cell is active, receive data from the foreign cell. */
-          if (cj_active_rt) {
-
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rt_gradient);
-
-            if (ci_active_rt) {
-              /* We only need updates later on if the other cell is active too
-               */
-              scheduler_activate_recv(s, ci->mpi.recv,
-                                      task_subtype_rt_transport);
-            }
-          } else if (ci_active_rt) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* If the local cell is inactive and the remote cell is active, we
-             * still need to receive stuff to be able to do the force
-             * interaction on this node as well. */
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rt_gradient);
-            scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rt_transport);
-#endif
-          }
-
-          /* Is the foreign cell active and will need stuff from us? */
-          if (ci_active_rt) {
-
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_rt_gradient,
-                                    ci_nodeID);
-
-            if (cj_active_rt) {
-              scheduler_activate_send(s, cj->mpi.send,
-                                      task_subtype_rt_transport, ci_nodeID);
-            }
-          } else if (cj_active_rt) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* If the foreign cell is inactive, but the local cell is active,
-             * we still need to send stuff to be able to do the force
-             * interaction on both nodes */
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_rt_gradient,
-                                    ci_nodeID);
-            scheduler_activate_send(s, cj->mpi.send, task_subtype_rt_transport,
-                                    ci_nodeID);
-#endif
-          }
-
-        } else if (cj_nodeID != nodeID) {
-
-          /* If the local cell is active, receive data from the foreign cell. */
-          if (ci_active_rt) {
-
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rt_gradient);
-
-            if (cj_active_rt) {
-              /* We only need updates later on if the other cell is active too
-               */
-              scheduler_activate_recv(s, cj->mpi.recv,
-                                      task_subtype_rt_transport);
-            }
-          } else if (cj_active_rt) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* If the local cell is inactive and the remote cell is active, we
-             * still need to receive stuff to be able to do the force
-             * interaction on this node as well. */
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rt_gradient);
-            scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rt_transport);
-#endif
-          }
-
-          /* Is the foreign cell active and will need stuff from us? */
-          if (cj_active_rt) {
-
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_rt_gradient,
-                                    cj_nodeID);
-
-            if (ci_active_rt) {
-              /* We only need updates later on if the other cell is active too
-               */
-              scheduler_activate_send(s, ci->mpi.send,
-                                      task_subtype_rt_transport, cj_nodeID);
-            }
-          } else if (ci_active_rt) {
-#ifdef MPI_SYMMETRIC_FORCE_INTERACTION
-            /* If the foreign cell is inactive, but the local cell is active,
-             * we still need to send stuff to be able to do the force
-             * interaction on both nodes */
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_rt_gradient,
-                                    cj_nodeID);
-            scheduler_activate_send(s, ci->mpi.send, task_subtype_rt_transport,
-                                    cj_nodeID);
-#endif
-          }
-        }
-#endif
-      }
-    }
-
-    /* End force for hydro ? */
-    else if (t_type == task_type_end_hydro_force) {
-
-      if (cell_is_active_hydro(t->ci, e)) scheduler_activate(s, t);
-    }
-
-    /* End force for gravity ? */
-    else if (t_type == task_type_end_grav_force) {
-
-      if (cell_is_active_gravity(t->ci, e)) scheduler_activate(s, t);
-    }
-
-    /* Activate the weighting task for neutrinos */
-    else if (t_type == task_type_neutrino_weight) {
-      if (cell_is_active_gravity(t->ci, e)) {
-        scheduler_activate(s, t);
-      }
-    }
-
-    /* Kick ? */
-    else if (t_type == task_type_kick1 || t_type == task_type_kick2) {
-
-      if (cell_is_active_hydro(t->ci, e) || cell_is_active_gravity(t->ci, e) ||
-          cell_is_active_stars(t->ci, e) || cell_is_active_sinks(t->ci, e) ||
-          cell_is_active_black_holes(t->ci, e))
-        scheduler_activate(s, t);
-    }
-
-    /* Hydro ghost tasks ? */
-    else if (t_type == task_type_ghost || t_type == task_type_extra_ghost ||
-             t_type == task_type_ghost_in || t_type == task_type_ghost_out) {
-      if (cell_is_active_hydro(t->ci, e)) scheduler_activate(s, t);
-    }
-
-    /* csds tasks ? */
-    else if (t->type == task_type_csds) {
-      if (cell_is_active_hydro(t->ci, e) || cell_is_active_gravity(t->ci, e) ||
-          cell_is_active_stars(t->ci, e))
-        scheduler_activate(s, t);
-    }
-
-    /* Gravity stuff ? */
-    else if (t_type == task_type_grav_down ||
-             t_type == task_type_grav_long_range ||
-             t_type == task_type_init_grav ||
-             t_type == task_type_init_grav_out ||
-             t_type == task_type_drift_gpart_out ||
-             t_type == task_type_grav_down_in) {
-      if (cell_is_active_gravity(t->ci, e)) scheduler_activate(s, t);
-    }
-
-    /* Multipole - Multipole interaction task */
-    else if (t_type == task_type_grav_mm) {
-
-      /* Local pointers. */
-      const struct cell *ci = t->ci;
-      const struct cell *cj = t->cj;
-#ifdef WITH_MPI
-      const int ci_nodeID = ci->nodeID;
-      const int cj_nodeID = (cj != NULL) ? cj->nodeID : -1;
-#else
-      const int ci_nodeID = nodeID;
-      const int cj_nodeID = nodeID;
-#endif
-      const int ci_active_gravity = cell_is_active_gravity_mm(ci, e);
-      const int cj_active_gravity = cell_is_active_gravity_mm(cj, e);
-
-      if ((ci_active_gravity && ci_nodeID == nodeID) ||
-          (cj_active_gravity && cj_nodeID == nodeID))
-        scheduler_activate(s, t);
-    }
-
-    /* Star ghost tasks ? */
-    else if (t_type == task_type_stars_ghost ||
-             t_type == task_type_stars_prep_ghost1 ||
-             t_type == task_type_hydro_prep_ghost1 ||
-             t_type == task_type_stars_prep_ghost2) {
-      if (cell_need_activating_stars(t->ci, e, with_star_formation,
-                                     with_star_formation_sink))
-        scheduler_activate(s, t);
-    }
-
-    /* Feedback implicit tasks? */
-    else if (t_type == task_type_stars_in || t_type == task_type_stars_out) {
-      if (cell_need_activating_stars(t->ci, e, with_star_formation,
-                                     with_star_formation_sink))
-        scheduler_activate(s, t);
-    }
-
-    /* Sink implicit tasks? */
-    else if (t_type == task_type_sink_in || t_type == task_type_sink_out ||
-             t_type == task_type_sink_ghost1) {
-      if (cell_is_active_sinks(t->ci, e) || cell_is_active_hydro(t->ci, e))
-        scheduler_activate(s, t);
-    }
-
-    /* Black hole ghost tasks ? */
-    else if (t_type == task_type_bh_density_ghost ||
-             t_type == task_type_bh_swallow_ghost1 ||
-             t_type == task_type_bh_swallow_ghost2 ||
-             t_type == task_type_bh_swallow_ghost3) {
-      if (cell_is_active_black_holes(t->ci, e)) scheduler_activate(s, t);
-    }
-
-    /* Black holes implicit tasks? */
-    else if (t_type == task_type_bh_in || t_type == task_type_bh_out) {
-      if (cell_is_active_black_holes(t->ci, e)) scheduler_activate(s, t);
-    }
-
-    /* Time-step collection? */
-    else if (t_type == task_type_timestep) {
-      t->ci->hydro.updated = 0;
-      t->ci->grav.updated = 0;
-      t->ci->stars.updated = 0;
-      t->ci->sinks.updated = 0;
-      t->ci->black_holes.updated = 0;
-
-      if (!cell_is_empty(t->ci)) {
-        if (cell_is_active_hydro(t->ci, e) ||
-            cell_is_active_gravity(t->ci, e) ||
-            cell_is_active_stars(t->ci, e) || cell_is_active_sinks(t->ci, e) ||
-            cell_is_active_black_holes(t->ci, e))
-          scheduler_activate(s, t);
-      }
-    }
-
-    /* Time-step collection? */
-    else if (t_type == task_type_collect) {
-      t->ci->hydro.updated = 0;
-      t->ci->grav.updated = 0;
-      t->ci->stars.updated = 0;
-      t->ci->sinks.updated = 0;
-      t->ci->black_holes.updated = 0;
-      t->ci->rt.updated = 0; /* this is different from time-step */
-
-      if (!cell_is_empty(t->ci)) {
-        if (cell_is_active_hydro(t->ci, e) ||
-            cell_is_active_gravity(t->ci, e) ||
-            cell_is_active_stars(t->ci, e) || cell_is_active_sinks(t->ci, e) ||
-            cell_is_active_black_holes(t->ci, e) || cell_is_rt_active(t->ci, e))
-          /* this is different from time-step ----^*/
-          scheduler_activate(s, t);
-      }
-    }
-
-    else if ((t_type == task_type_send && t_subtype == task_subtype_tend) ||
-             (t_type == task_type_recv && t_subtype == task_subtype_tend)) {
-      if (!cell_is_empty(t->ci)) {
-        scheduler_activate(s, t);
-      }
-    }
-
-    /* Subgrid tasks: cooling */
-    else if (t_type == task_type_cooling || t_type == task_type_cooling_in ||
-             t_type == task_type_cooling_out) {
-      if (cell_is_active_hydro(t->ci, e)) scheduler_activate(s, t);
-    }
-
-    /* Subgrid tasks: star formation */
-    else if (t_type == task_type_star_formation) {
-      if (cell_is_active_hydro(t->ci, e)) {
-        cell_activate_star_formation_tasks(t->ci, s, with_feedback);
-        cell_activate_super_spart_drifts(t->ci, s);
-      }
-    }
-
-    /* Subgrid tasks: star formation from sinks */
-    else if (t_type == task_type_star_formation_sink) {
-      if (cell_is_active_hydro(t->ci, e) || cell_is_active_sinks(t->ci, e)) {
-        cell_activate_star_formation_sink_tasks(t->ci, s, with_feedback);
-        cell_activate_super_spart_drifts(t->ci, s);
-      }
-    }
-
-    /* Radiative transfer implicit tasks */
-    else if (t->type == task_type_rt_in) {
-      if (cell_is_rt_active(t->ci, e) ||
-          cell_need_activating_stars(t->ci, e, with_star_formation,
-                                     with_star_formation_sink))
-        scheduler_activate(s, t);
-    }
-
-    else if (t->type == task_type_rt_ghost1 || t->type == task_type_rt_ghost2 ||
-             t->type == task_type_rt_transport_out ||
-             t->type == task_type_rt_tchem ||
-             t->type == task_type_rt_advance_cell_time ||
-             t->type == task_type_rt_out) {
-      if (cell_is_rt_active(t->ci, e)) scheduler_activate(s, t);
-      /* Note that rt_collect_times never needs to be active on main steps,
-       * which is always what follows engine_marktasks().*/
-    }
-
-    /* Subgrid tasks: sink formation */
-    else if (t_type == task_type_sink_formation) {
-      if (with_star_formation_sink && t->ci->hydro.count > 0 &&
-          cell_is_active_hydro(t->ci, e)) {
-        cell_activate_sink_formation_tasks(t->ci, s);
-        cell_activate_super_sink_drifts(t->ci, s);
-      }
-    }
-  }
-}
-
-/**
- * @brief Mark tasks to be un-skipped and set the sort flags accordingly.
- *
- * @return 1 if the space has to be rebuilt, 0 otherwise.
- */
-int engine_marktasks(struct engine *e) {
-
-  struct scheduler *s = &e->sched;
-  const ticks tic = getticks();
-  int rebuild_space = 0;
-
-  /* Run through the tasks and mark as skip or not. */
-  size_t extra_data[3] = {(size_t)e, (size_t)rebuild_space, (size_t)&e->sched};
-  threadpool_map(&e->threadpool, engine_marktasks_mapper, s->tasks, s->nr_tasks,
-                 sizeof(struct task), threadpool_auto_chunk_size, extra_data);
-  rebuild_space = extra_data[1];
-
-  if (e->verbose)
-    message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
-            clocks_getunit());
-
-  /* All is well... */
-  return rebuild_space;
-}
diff --git a/src/engine_redistribute.c b/src/engine_redistribute.c
index 1bdfa2dfe84fd77eb24f868f4ec3510103e26a03..a34c17d00ce54b890d1f83ac5161a952c5a8d124 100644
--- a/src/engine_redistribute.c
+++ b/src/engine_redistribute.c
@@ -268,8 +268,8 @@ struct redist_mapper_data {
     int *dest =                                                            \
         mydata->dest + (ptrdiff_t)(parts - (struct TYPE *)mydata->base);   \
     int *lcounts = NULL;                                                   \
-    if ((lcounts = (int *)calloc(                                          \
-             sizeof(int), mydata->nr_nodes * mydata->nr_nodes)) == NULL)   \
+    if ((lcounts = (int *)calloc(mydata->nr_nodes * mydata->nr_nodes,      \
+                                 sizeof(int))) == NULL)                    \
       error("Failed to allocate counts thread-specific buffer");           \
     for (int k = 0; k < num_elements; k++) {                               \
       for (int j = 0; j < 3; j++) {                                        \
@@ -323,6 +323,14 @@ void ENGINE_REDISTRIBUTE_DEST_MAPPER(gpart);
  */
 void ENGINE_REDISTRIBUTE_DEST_MAPPER(bpart);
 
+/**
+ * @brief Accumulate the counts of sink particles per cell.
+ * Threadpool helper for accumulating the counts of particles per cell.
+ *
+ * sink version.
+ */
+void ENGINE_REDISTRIBUTE_DEST_MAPPER(sink);
+
 #endif /* redist_mapper_data */
 
 #ifdef WITH_MPI /* savelink_mapper_data */
@@ -403,12 +411,22 @@ void ENGINE_REDISTRIBUTE_SAVELINK_MAPPER(bpart, 1);
 void ENGINE_REDISTRIBUTE_SAVELINK_MAPPER(bpart, 0);
 #endif
 
+/**
+ * @brief Save position of sink-gpart links.
+ * Threadpool helper for accumulating the counts of particles per cell.
+ */
+#ifdef SWIFT_DEBUG_CHECKS
+void ENGINE_REDISTRIBUTE_SAVELINK_MAPPER(sink, 1);
+#else
+void ENGINE_REDISTRIBUTE_SAVELINK_MAPPER(sink, 0);
+#endif
+
 #endif /* savelink_mapper_data */
 
 #ifdef WITH_MPI /* relink_mapper_data */
 
-/* Support for relinking parts, gparts, sparts and bparts after moving between
- * nodes. */
+/* Support for relinking parts, gparts, sparts, bparts and sinks after moving
+ * between nodes. */
 struct relink_mapper_data {
   int nodeID;
   int nr_nodes;
@@ -416,6 +434,7 @@ struct relink_mapper_data {
   int *s_counts;
   int *g_counts;
   int *b_counts;
+  int *sink_counts;
   struct space *s;
 };
 
@@ -439,6 +458,7 @@ void engine_redistribute_relink_mapper(void *map_data, int num_elements,
   int *g_counts = mydata->g_counts;
   int *s_counts = mydata->s_counts;
   int *b_counts = mydata->b_counts;
+  int *sink_counts = mydata->sink_counts;
   struct space *s = mydata->s;
 
   for (int i = 0; i < num_elements; i++) {
@@ -450,12 +470,14 @@ void engine_redistribute_relink_mapper(void *map_data, int num_elements,
     size_t offset_gparts = 0;
     size_t offset_sparts = 0;
     size_t offset_bparts = 0;
+    size_t offset_sinks = 0;
     for (int n = 0; n < node; n++) {
       int ind_recv = n * nr_nodes + nodeID;
       offset_parts += counts[ind_recv];
       offset_gparts += g_counts[ind_recv];
       offset_sparts += s_counts[ind_recv];
       offset_bparts += b_counts[ind_recv];
+      offset_sinks += sink_counts[ind_recv];
     }
 
     /* Number of gparts sent from this node. */
@@ -497,6 +519,17 @@ void engine_redistribute_relink_mapper(void *map_data, int num_elements,
         s->gparts[k].id_or_neg_offset = -partner_index;
         s->bparts[partner_index].gpart = &s->gparts[k];
       }
+
+      /* Does this gpart have a sink partner ? */
+      else if (s->gparts[k].type == swift_type_sink) {
+
+        const ptrdiff_t partner_index =
+            offset_sinks - s->gparts[k].id_or_neg_offset;
+
+        /* Re-link */
+        s->gparts[k].id_or_neg_offset = -partner_index;
+        s->sinks[partner_index].gpart = &s->gparts[k];
+      }
     }
   }
 }
@@ -523,13 +556,6 @@ void engine_redistribute_relink_mapper(void *map_data, int num_elements,
 void engine_redistribute(struct engine *e) {
 
 #ifdef WITH_MPI
-#ifdef SWIFT_DEBUG_CHECKS
-  const int nr_sinks_new = 0;
-#endif
-  if (e->policy & engine_policy_sinks) {
-    error("Not implemented yet");
-  }
-
   const int nr_nodes = e->nr_nodes;
   const int nodeID = e->nodeID;
   struct space *s = e->s;
@@ -540,12 +566,14 @@ void engine_redistribute(struct engine *e) {
   struct gpart *gparts = s->gparts;
   struct spart *sparts = s->sparts;
   struct bpart *bparts = s->bparts;
+  struct sink *sinks = s->sinks;
   ticks tic = getticks();
 
   size_t nr_parts = s->nr_parts;
   size_t nr_gparts = s->nr_gparts;
   size_t nr_sparts = s->nr_sparts;
   size_t nr_bparts = s->nr_bparts;
+  size_t nr_sinks = s->nr_sinks;
 
   /* Start by moving inhibited particles to the end of the arrays */
   for (size_t k = 0; k < nr_parts; /* void */) {
@@ -613,6 +641,27 @@ void engine_redistribute(struct engine *e) {
     }
   }
 
+  /* Now move inhibited sink particles to the end of the arrays */
+  for (size_t k = 0; k < nr_sinks; /* void */) {
+    if (sinks[k].time_bin == time_bin_inhibited ||
+        sinks[k].time_bin == time_bin_not_created) {
+      nr_sinks -= 1;
+
+      /* Swap the particle */
+      memswap(&s->sinks[k], &s->sinks[nr_sinks], sizeof(struct sink));
+
+      /* Swap the link with the gpart */
+      if (s->sinks[k].gpart != NULL) {
+        s->sinks[k].gpart->id_or_neg_offset = -k;
+      }
+      if (s->sinks[nr_sinks].gpart != NULL) {
+        s->sinks[nr_sinks].gpart->id_or_neg_offset = -nr_sinks;
+      }
+    } else {
+      k++;
+    }
+  }
+
   /* Finally do the same with the gravity particles */
   for (size_t k = 0; k < nr_gparts; /* void */) {
     if (gparts[k].time_bin == time_bin_inhibited ||
@@ -630,6 +679,8 @@ void engine_redistribute(struct engine *e) {
         s->sparts[-s->gparts[k].id_or_neg_offset].gpart = &s->gparts[k];
       } else if (s->gparts[k].type == swift_type_black_hole) {
         s->bparts[-s->gparts[k].id_or_neg_offset].gpart = &s->gparts[k];
+      } else if (s->gparts[k].type == swift_type_sink) {
+        s->sinks[-s->gparts[k].id_or_neg_offset].gpart = &s->gparts[k];
       }
 
       if (s->gparts[nr_gparts].type == swift_type_gas) {
@@ -641,6 +692,9 @@ void engine_redistribute(struct engine *e) {
       } else if (s->gparts[nr_gparts].type == swift_type_black_hole) {
         s->bparts[-s->gparts[nr_gparts].id_or_neg_offset].gpart =
             &s->gparts[nr_gparts];
+      } else if (s->gparts[nr_gparts].type == swift_type_sink) {
+        s->sinks[-s->gparts[nr_sinks].id_or_neg_offset].gpart =
+            &s->gparts[nr_gparts];
       }
     } else {
       k++;
@@ -652,7 +706,7 @@ void engine_redistribute(struct engine *e) {
   /* Allocate temporary arrays to store the counts of particles to be sent
    * and the destination of each particle */
   int *counts;
-  if ((counts = (int *)calloc(sizeof(int), nr_nodes * nr_nodes)) == NULL)
+  if ((counts = (int *)calloc(nr_nodes * nr_nodes, sizeof(int))) == NULL)
     error("Failed to allocate counts temporary buffer.");
 
   int *dest;
@@ -731,7 +785,7 @@ void engine_redistribute(struct engine *e) {
 
   /* Get destination of each s-particle */
   int *s_counts;
-  if ((s_counts = (int *)calloc(sizeof(int), nr_nodes * nr_nodes)) == NULL)
+  if ((s_counts = (int *)calloc(nr_nodes * nr_nodes, sizeof(int))) == NULL)
     error("Failed to allocate s_counts temporary buffer.");
 
   int *s_dest;
@@ -797,7 +851,7 @@ void engine_redistribute(struct engine *e) {
 
   /* Get destination of each b-particle */
   int *b_counts;
-  if ((b_counts = (int *)calloc(sizeof(int), nr_nodes * nr_nodes)) == NULL)
+  if ((b_counts = (int *)calloc(nr_nodes * nr_nodes, sizeof(int))) == NULL)
     error("Failed to allocate b_counts temporary buffer.");
 
   int *b_dest;
@@ -861,9 +915,76 @@ void engine_redistribute(struct engine *e) {
   }
   swift_free("b_dest", b_dest);
 
+  /* Get destination of each sink-particle */
+  int *sink_counts;
+  if ((sink_counts = (int *)calloc(nr_nodes * nr_nodes, sizeof(int))) == NULL)
+    error("Failed to allocate sink_counts temporary buffer.");
+
+  int *sink_dest;
+  if ((sink_dest = (int *)swift_malloc("sink_dest", sizeof(int) * nr_sinks)) ==
+      NULL)
+    error("Failed to allocate sink_dest temporary buffer.");
+
+  redist_data.counts = sink_counts;
+  redist_data.dest = sink_dest;
+  redist_data.base = (void *)sinks;
+
+  threadpool_map(&e->threadpool, engine_redistribute_dest_mapper_sink, sinks,
+                 nr_sinks, sizeof(struct sink), threadpool_auto_chunk_size,
+                 &redist_data);
+
+  /* Sort the particles according to their cell index. */
+  if (nr_sinks > 0)
+    space_sinks_sort(s->sinks, sink_dest, &sink_counts[nodeID * nr_nodes],
+                     nr_nodes, 0);
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Verify that the sink have been sorted correctly. */
+  for (size_t k = 0; k < nr_sinks; k++) {
+    const struct sink *sink = &s->sinks[k];
+
+    if (sink->time_bin == time_bin_inhibited)
+      error("Inhibited particle found after sorting!");
+
+    if (sink->time_bin == time_bin_not_created)
+      error("Inhibited particle found after sorting!");
+
+    /* New cell index */
+    const int new_cid =
+        cell_getid(s->cdim, sink->x[0] * s->iwidth[0],
+                   sink->x[1] * s->iwidth[1], sink->x[2] * s->iwidth[2]);
+
+    /* New cell of this sink */
+    const struct cell *c = &s->cells_top[new_cid];
+    const int new_node = c->nodeID;
+
+    if (sink_dest[k] != new_node)
+      error("sink's new node index not matching sorted index.");
+
+    if (sink->x[0] < c->loc[0] || sink->x[0] > c->loc[0] + c->width[0] ||
+        sink->x[1] < c->loc[1] || sink->x[1] > c->loc[1] + c->width[1] ||
+        sink->x[2] < c->loc[2] || sink->x[2] > c->loc[2] + c->width[2])
+      error("sink not sorted into the right top-level cell!");
+  }
+#endif
+
+  /* We need to re-link the gpart partners of sinks. */
+  if (nr_sinks > 0) {
+
+    struct savelink_mapper_data savelink_data;
+    savelink_data.nr_nodes = nr_nodes;
+    savelink_data.counts = sink_counts;
+    savelink_data.parts = (void *)sinks;
+    savelink_data.nodeID = nodeID;
+    threadpool_map(&e->threadpool, engine_redistribute_savelink_mapper_sink,
+                   nodes, nr_nodes, sizeof(int), threadpool_auto_chunk_size,
+                   &savelink_data);
+  }
+  swift_free("sink_dest", sink_dest);
+
   /* Get destination of each g-particle */
   int *g_counts;
-  if ((g_counts = (int *)calloc(sizeof(int), nr_nodes * nr_nodes)) == NULL)
+  if ((g_counts = (int *)calloc(nr_nodes * nr_nodes, sizeof(int))) == NULL)
     error("Failed to allocate g_gcount temporary buffer.");
 
   int *g_dest;
@@ -936,22 +1057,30 @@ void engine_redistribute(struct engine *e) {
                     MPI_SUM, MPI_COMM_WORLD) != MPI_SUCCESS)
     error("Failed to allreduce bparticle transfer counts.");
 
+  /* Get all the sink_counts from all the nodes. */
+  if (MPI_Allreduce(MPI_IN_PLACE, sink_counts, nr_nodes * nr_nodes, MPI_INT,
+                    MPI_SUM, MPI_COMM_WORLD) != MPI_SUCCESS)
+    error("Failed to allreduce sink particle transfer counts.");
+
   /* Report how many particles will be moved. */
   if (e->verbose) {
     if (e->nodeID == 0) {
-      size_t total = 0, g_total = 0, s_total = 0, b_total = 0;
-      size_t unmoved = 0, g_unmoved = 0, s_unmoved = 0, b_unmoved = 0;
+      size_t total = 0, g_total = 0, s_total = 0, b_total = 0, sink_total = 0;
+      size_t unmoved = 0, g_unmoved = 0, s_unmoved = 0, b_unmoved = 0,
+             sink_unmoved = 0;
       for (int p = 0, r = 0; p < nr_nodes; p++) {
         for (int n = 0; n < nr_nodes; n++) {
           total += counts[r];
           g_total += g_counts[r];
           s_total += s_counts[r];
           b_total += b_counts[r];
+          sink_total += sink_counts[r];
           if (p == n) {
             unmoved += counts[r];
             g_unmoved += g_counts[r];
             s_unmoved += s_counts[r];
             b_unmoved += b_counts[r];
+            sink_unmoved += sink_counts[r];
           }
           r++;
         }
@@ -971,14 +1100,19 @@ void engine_redistribute(struct engine *e) {
         message("%ld of %ld (%.2f%%) of b-particles moved", b_total - b_unmoved,
                 b_total,
                 100.0 * (double)(b_total - b_unmoved) / (double)b_total);
+      if (sink_total > 0)
+        message(
+            "%ld of %ld (%.2f%%) of sink-particles moved",
+            sink_total - sink_unmoved, sink_total,
+            100.0 * (double)(sink_total - sink_unmoved) / (double)sink_total);
     }
   }
 
-  /* Now each node knows how many parts, sparts, bparts, and gparts will be
-   * transferred to every other node. Get the new numbers of particles for this
-   * node. */
+  /* Now each node knows how many parts, sparts, bparts, sinks and gparts will
+   * be transferred to every other node. Get the new numbers of particles for
+   * this node. */
   size_t nr_parts_new = 0, nr_gparts_new = 0, nr_sparts_new = 0,
-         nr_bparts_new = 0;
+         nr_bparts_new = 0, nr_sinks_new = 0;
   for (int k = 0; k < nr_nodes; k++)
     nr_parts_new += counts[k * nr_nodes + nodeID];
   for (int k = 0; k < nr_nodes; k++)
@@ -987,6 +1121,8 @@ void engine_redistribute(struct engine *e) {
     nr_sparts_new += s_counts[k * nr_nodes + nodeID];
   for (int k = 0; k < nr_nodes; k++)
     nr_bparts_new += b_counts[k * nr_nodes + nodeID];
+  for (int k = 0; k < nr_nodes; k++)
+    nr_sinks_new += sink_counts[k * nr_nodes + nodeID];
 
 #ifdef WITH_CSDS
   const int initial_redistribute = e->ti_current == 0;
@@ -996,6 +1132,7 @@ void engine_redistribute(struct engine *e) {
     size_t spart_offset = 0;
     size_t gpart_offset = 0;
     size_t bpart_offset = 0;
+    size_t sink_offset = 0;
 
     for (int i = 0; i < nr_nodes; i++) {
       const size_t c_ind = engine_rank * nr_nodes + i;
@@ -1006,6 +1143,7 @@ void engine_redistribute(struct engine *e) {
         spart_offset += s_counts[c_ind];
         gpart_offset += g_counts[c_ind];
         bpart_offset += b_counts[c_ind];
+        sink_offset += sink_counts[c_ind];
         continue;
       }
 
@@ -1027,11 +1165,17 @@ void engine_redistribute(struct engine *e) {
         error("TODO");
       }
 
+      /* Log the sinks */
+      if (sink_counts[c_ind] > 0) {
+        error("TODO");
+      }
+
       /* Update the counters */
       part_offset += counts[c_ind];
       spart_offset += s_counts[c_ind];
       gpart_offset += g_counts[c_ind];
       bpart_offset += b_counts[c_ind];
+      sink_offset += sink_counts[c_ind];
     }
   }
 #endif
@@ -1086,6 +1230,15 @@ void engine_redistribute(struct engine *e) {
   s->nr_bparts = nr_bparts_new;
   s->size_bparts = engine_redistribute_alloc_margin * nr_bparts_new;
 
+  /* Sink particles. */
+  new_parts = engine_do_redistribute(
+      "sinks", sink_counts, (char *)s->sinks, nr_sinks_new, sizeof(struct sink),
+      sink_align, sink_mpi_type, nr_nodes, nodeID, e->syncredist);
+  swift_free("sinks", s->sinks);
+  s->sinks = (struct sink *)new_parts;
+  s->nr_sinks = nr_sinks_new;
+  s->size_sinks = engine_redistribute_alloc_margin * nr_sinks_new;
+
   /* All particles have now arrived. Time for some final operations on the
      stuff we just received */
 
@@ -1095,6 +1248,7 @@ void engine_redistribute(struct engine *e) {
     size_t spart_offset = 0;
     size_t gpart_offset = 0;
     size_t bpart_offset = 0;
+    size_t sink_offset = 0;
 
     for (int i = 0; i < nr_nodes; i++) {
       const size_t c_ind = i * nr_nodes + engine_rank;
@@ -1105,6 +1259,7 @@ void engine_redistribute(struct engine *e) {
         spart_offset += s_counts[c_ind];
         gpart_offset += g_counts[c_ind];
         bpart_offset += b_counts[c_ind];
+        sink_offset += sink_counts[c_ind];
         continue;
       }
 
@@ -1126,11 +1281,17 @@ void engine_redistribute(struct engine *e) {
         error("TODO");
       }
 
+      /* Log the sinks */
+      if (sink_counts[c_ind] > 0) {
+        error("TODO");
+      }
+
       /* Update the counters */
       part_offset += counts[c_ind];
       spart_offset += s_counts[c_ind];
       gpart_offset += g_counts[c_ind];
       bpart_offset += b_counts[c_ind];
+      sink_offset += sink_counts[c_ind];
     }
   }
 #endif
@@ -1144,6 +1305,7 @@ void engine_redistribute(struct engine *e) {
   relink_data.g_counts = g_counts;
   relink_data.s_counts = s_counts;
   relink_data.b_counts = b_counts;
+  relink_data.sink_counts = sink_counts;
   relink_data.nodeID = nodeID;
   relink_data.nr_nodes = nr_nodes;
 
@@ -1156,6 +1318,7 @@ void engine_redistribute(struct engine *e) {
   free(g_counts);
   free(s_counts);
   free(b_counts);
+  free(sink_counts);
 
 #ifdef SWIFT_DEBUG_CHECKS
   /* Verify that all parts are in the right place. */
@@ -1191,6 +1354,15 @@ void engine_redistribute(struct engine *e) {
       error("Received b-particle (%zu) that does not belong here (nodeID=%i).",
             k, cells[cid].nodeID);
   }
+  for (size_t k = 0; k < nr_sinks_new; k++) {
+    const int cid = cell_getid(s->cdim, s->sinks[k].x[0] * s->iwidth[0],
+                               s->sinks[k].x[1] * s->iwidth[1],
+                               s->sinks[k].x[2] * s->iwidth[2]);
+    if (cells[cid].nodeID != nodeID)
+      error(
+          "Received sink-particle (%zu) that does not belong here (nodeID=%i).",
+          k, cells[cid].nodeID);
+  }
 
   /* Verify that the links are correct */
   part_verify_links(s->parts, s->gparts, s->sinks, s->sparts, s->bparts,
@@ -1205,10 +1377,11 @@ void engine_redistribute(struct engine *e) {
     for (int k = 0; k < nr_cells; k++)
       if (cells[k].nodeID == nodeID) my_cells += 1;
     message(
-        "node %i now has %zu parts, %zu sparts, %zu bparts and %zu gparts in "
+        "node %i now has %zu parts, %zu sparts, %zu bparts, %zu sinks and %zu "
+        "gparts in "
         "%i cells.",
-        nodeID, nr_parts_new, nr_sparts_new, nr_bparts_new, nr_gparts_new,
-        my_cells);
+        nodeID, nr_parts_new, nr_sparts_new, nr_bparts_new, nr_sinks_new,
+        nr_gparts_new, my_cells);
   }
 
   /* Flag that we do not have any extra particles any more */
@@ -1216,6 +1389,7 @@ void engine_redistribute(struct engine *e) {
   s->nr_extra_gparts = 0;
   s->nr_extra_sparts = 0;
   s->nr_extra_bparts = 0;
+  s->nr_extra_sinks = 0;
 
   /* Flag that a redistribute has taken place */
   e->step_props |= engine_step_prop_redistribute;
diff --git a/src/engine_split_particles.c b/src/engine_split_particles.c
index 8f672619101bbc98189674209a62202381683633..1ea79af5a3e33d7eb5d7d73437b30a9caa589e55 100644
--- a/src/engine_split_particles.c
+++ b/src/engine_split_particles.c
@@ -33,6 +33,7 @@
 #include "random.h"
 #include "rt.h"
 #include "star_formation.h"
+#include "tools.h"
 #include "tracers.h"
 
 const int particle_split_factor = 2;
@@ -60,6 +61,7 @@ struct data_split {
   long long offset_id;
   long long *count_id;
   swift_lock_type lock;
+  FILE *extra_split_logger;
 };
 
 /**
@@ -172,19 +174,20 @@ void engine_split_gas_particle_split_mapper(void *restrict map_data, int count,
         memcpy(&global_gparts[k_gparts], gp, sizeof(struct gpart));
       }
 
-      /* Update splitting tree */
-      particle_splitting_update_binary_tree(&global_xparts[k_parts].split_data,
-                                            &xp->split_data);
-
       /* Update the IDs. */
       if (generate_random_ids) {
         /* The gas IDs are always odd, so we multiply by two here to
-         * repsect the parity. */
+         * respect the parity. */
         global_parts[k_parts].id += 2 * (long long)rand_r(&seedp);
       } else {
         global_parts[k_parts].id = offset_id + 2 * atomic_inc(count_id);
       }
 
+      /* Update splitting tree */
+      particle_splitting_update_binary_tree(
+          &xp->split_data, &global_xparts[k_parts].split_data, p->id,
+          global_parts[k_parts].id, data->extra_split_logger, &data->lock);
+
       /* Re-link everything */
       if (with_gravity) {
         global_parts[k_parts].gpart = &global_gparts[k_gparts];
@@ -312,7 +315,10 @@ void engine_split_gas_particles(struct engine *e) {
 
   /* Verify that nothing wrong happened with the IDs */
   if (data_count.max_id > e->max_parts_id) {
-    error("Found a gas particle with an ID larger than the current max!");
+    error(
+        "Found a gas particle with an ID (%lld) larger than the current max "
+        "(%lld)!",
+        data_count.max_id, e->max_parts_id);
   }
 
   /* Be verbose about this. This is an important event */
@@ -435,12 +441,22 @@ void engine_split_gas_particles(struct engine *e) {
   size_t k_parts = s->nr_parts;
   size_t k_gparts = s->nr_gparts;
 
+  FILE *extra_split_logger = NULL;
+  if (e->hydro_properties->log_extra_splits_in_file) {
+    char extra_split_logger_filename[256];
+    sprintf(extra_split_logger_filename, "splits/splits_%04d.txt", engine_rank);
+    extra_split_logger = fopen(extra_split_logger_filename, "a");
+    if (extra_split_logger == NULL) error("Error opening split logger file!");
+  }
+
   /* Loop over the particles again to split them */
   long long local_count_id = 0;
   struct data_split data_split = {
-      e,         mass_threshold, generate_random_ids, &k_parts,
-      &k_gparts, offset_id,      &local_count_id,     0};
+      e,          mass_threshold,    generate_random_ids, &k_parts,
+      &k_gparts,  offset_id,         &local_count_id,
+      /*lock=*/0, extra_split_logger};
   lock_init(&data_split.lock);
+
   threadpool_map(&e->threadpool, engine_split_gas_particle_split_mapper,
                  s->parts, nr_parts_old, sizeof(struct part), 0, &data_split);
   if (lock_destroy(&data_split.lock) != 0) error("Error destroying lock");
@@ -462,7 +478,33 @@ void engine_split_gas_particles(struct engine *e) {
   }
 #endif
 
+  /* Close the logger file */
+  if (e->hydro_properties->log_extra_splits_in_file) fclose(extra_split_logger);
+
   if (e->verbose)
     message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
             clocks_getunit());
 }
+
+void engine_init_split_gas_particles(struct engine *e) {
+
+  if (e->hydro_properties->log_extra_splits_in_file) {
+
+    /* Create the directory to host the logs */
+    if (engine_rank == 0) safe_checkdir("splits", /*create=*/1);
+
+#ifdef WITH_MPI
+    MPI_Barrier(MPI_COMM_WORLD);
+#endif
+
+    /* Create the logger files and add a header */
+    char extra_split_logger_filename[256];
+    sprintf(extra_split_logger_filename, "splits/splits_%04d.txt", engine_rank);
+    FILE *extra_split_logger = fopen(extra_split_logger_filename, "w");
+    fprintf(extra_split_logger, "# %12s %20s %20s %20s %20s\n", "Step", "ID",
+            "Progenitor", "Count", "Tree");
+
+    /* Close everything for now */
+    fclose(extra_split_logger);
+  }
+}
diff --git a/src/engine_strays.c b/src/engine_strays.c
index d55909c73e08c5b594d6ab30662f3b6e64d92a25..2918cbeb521a19ee30f9abc6bf7c47ca32d57475 100644
--- a/src/engine_strays.c
+++ b/src/engine_strays.c
@@ -33,6 +33,13 @@
 /* Local headers. */
 #include "proxy.h"
 
+#ifdef WITH_MPI
+/* Number of particle types to wait for after launching the proxies. We have
+   parts, xparts, gparts, sparts, bparts and sinks to exchange, hence 6 types.
+ */
+#define MPI_REQUEST_NUMBER_PARTICLE_TYPES 6
+#endif
+
 /**
  * @brief Exchange straying particles with other nodes.
  *
@@ -57,18 +64,22 @@
  * @param ind_bpart The foreign #cell ID of each bpart.
  * @param Nbpart The number of stray bparts, contains the number of bparts
  *        received on return.
+ * @param offset_sinks The index in the sinks array as of which the foreign
+ *        parts reside (i.e. the current number of local #sink).
+ * @param ind_sink The foreign #cell ID of each sink.
+ * @param Nsink The number of stray sinks, contains the number of sinks
+ *        received on return.
  *
  * Note that this function does not mess-up the linkage between parts and
  * gparts, i.e. the received particles have correct linkeage.
  */
-void engine_exchange_strays(struct engine *e, const size_t offset_parts,
-                            const int *restrict ind_part, size_t *Npart,
-                            const size_t offset_gparts,
-                            const int *restrict ind_gpart, size_t *Ngpart,
-                            const size_t offset_sparts,
-                            const int *restrict ind_spart, size_t *Nspart,
-                            const size_t offset_bparts,
-                            const int *restrict ind_bpart, size_t *Nbpart) {
+void engine_exchange_strays(
+    struct engine *e, const size_t offset_parts, const int *restrict ind_part,
+    size_t *Npart, const size_t offset_gparts, const int *restrict ind_gpart,
+    size_t *Ngpart, const size_t offset_sparts, const int *restrict ind_spart,
+    size_t *Nspart, const size_t offset_bparts, const int *restrict ind_bpart,
+    size_t *Nbpart, const size_t offset_sinks, const int *restrict ind_sink,
+    size_t *Nsink) {
 
 #ifdef WITH_MPI
   struct space *s = e->s;
@@ -80,6 +91,7 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
     e->proxies[k].nr_gparts_out = 0;
     e->proxies[k].nr_sparts_out = 0;
     e->proxies[k].nr_bparts_out = 0;
+    e->proxies[k].nr_sinks_out = 0;
   }
 
   /* Put the parts into the corresponding proxies. */
@@ -204,6 +216,46 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
     /* Load the bpart into the proxy */
     proxy_bparts_load(&e->proxies[pid], &s->bparts[offset_bparts + k], 1);
 
+#ifdef WITH_CSDS
+    if (e->policy & engine_policy_csds) {
+      error("Not yet implemented.");
+    }
+#endif
+  }
+
+  /* Put the sinks into the corresponding proxies. */
+  for (size_t k = 0; k < *Nsink; k++) {
+    /* Ignore the particles we want to get rid of (inhibited, ...). */
+    if (ind_sink[k] == -1) continue;
+
+    /* Get the target node and proxy ID. */
+    const int node_id = e->s->cells_top[ind_sink[k]].nodeID;
+    if (node_id < 0 || node_id >= e->nr_nodes)
+      error("Bad node ID %i.", node_id);
+    const int pid = e->proxy_ind[node_id];
+    if (pid < 0) {
+      error(
+          "Do not have a proxy for the requested nodeID %i for part with "
+          "id=%lld, x=[%e,%e,%e].",
+          node_id, s->sinks[offset_sinks + k].id,
+          s->sinks[offset_sinks + k].x[0], s->sinks[offset_sinks + k].x[1],
+          s->sinks[offset_sinks + k].x[2]);
+    }
+
+    /* Re-link the associated gpart with the buffer offset of the sink. */
+    if (s->sinks[offset_sinks + k].gpart != NULL) {
+      s->sinks[offset_sinks + k].gpart->id_or_neg_offset =
+          -e->proxies[pid].nr_sinks_out;
+    }
+
+#ifdef SWIFT_DEBUG_CHECKS
+    if (s->sinks[offset_sinks + k].time_bin == time_bin_inhibited)
+      error("Attempting to exchange an inhibited particle");
+#endif
+
+    /* Load the sink into the proxy */
+    proxy_sinks_load(&e->proxies[pid], &s->sinks[offset_sinks + k], 1);
+
 #ifdef WITH_CSDS
     if (e->policy & engine_policy_csds) {
       error("Not yet implemented.");
@@ -252,8 +304,8 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
   }
 
   /* Launch the proxies. */
-  MPI_Request reqs_in[5 * engine_maxproxies];
-  MPI_Request reqs_out[5 * engine_maxproxies];
+  MPI_Request reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * engine_maxproxies];
+  MPI_Request reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * engine_maxproxies];
   for (int k = 0; k < e->nr_proxies; k++) {
     proxy_parts_exchange_first(&e->proxies[k]);
     reqs_in[k] = e->proxies[k].req_parts_count_in;
@@ -281,18 +333,21 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
   int count_gparts_in = 0;
   int count_sparts_in = 0;
   int count_bparts_in = 0;
+  int count_sinks_in = 0;
   for (int k = 0; k < e->nr_proxies; k++) {
     count_parts_in += e->proxies[k].nr_parts_in;
     count_gparts_in += e->proxies[k].nr_gparts_in;
     count_sparts_in += e->proxies[k].nr_sparts_in;
     count_bparts_in += e->proxies[k].nr_bparts_in;
+    count_sinks_in += e->proxies[k].nr_sinks_in;
   }
   if (e->verbose) {
     message(
-        "sent out %zu/%zu/%zu/%zu parts/gparts/sparts/bparts, got %i/%i/%i/%i "
+        "sent out %zu/%zu/%zu/%zu/%zu parts/gparts/sparts/bparts/sinks, got "
+        "%i/%i/%i/%i/%i "
         "back.",
-        *Npart, *Ngpart, *Nspart, *Nbpart, count_parts_in, count_gparts_in,
-        count_sparts_in, count_bparts_in);
+        *Npart, *Ngpart, *Nspart, *Nbpart, *Nsink, count_parts_in,
+        count_gparts_in, count_sparts_in, count_bparts_in, count_sinks_in);
   }
 
   /* Reallocate the particle arrays if necessary */
@@ -356,6 +411,24 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
     }
   }
 
+  if (offset_sinks + count_sinks_in > s->size_sinks) {
+    s->size_sinks = (offset_sinks + count_sinks_in) * engine_parts_size_grow;
+    struct sink *sinks_new = NULL;
+    if (swift_memalign("sinks", (void **)&sinks_new, sink_align,
+                       sizeof(struct sink) * s->size_sinks) != 0)
+      error("Failed to allocate new sink data.");
+    memcpy(sinks_new, s->sinks, sizeof(struct sink) * offset_sinks);
+    swift_free("sinks", s->sinks);
+    s->sinks = sinks_new;
+
+    /* Reset the links */
+    for (size_t k = 0; k < offset_sinks; k++) {
+      if (s->sinks[k].gpart != NULL) {
+        s->sinks[k].gpart->id_or_neg_offset = -k;
+      }
+    }
+  }
+
   if (offset_gparts + count_gparts_in > s->size_gparts) {
     s->size_gparts = (offset_gparts + count_gparts_in) * engine_parts_size_grow;
     struct gpart *gparts_new = NULL;
@@ -374,6 +447,8 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
         s->sparts[-s->gparts[k].id_or_neg_offset].gpart = &s->gparts[k];
       } else if (s->gparts[k].type == swift_type_black_hole) {
         s->bparts[-s->gparts[k].id_or_neg_offset].gpart = &s->gparts[k];
+      } else if (s->gparts[k].type == swift_type_sink) {
+        s->sinks[-s->gparts[k].id_or_neg_offset].gpart = &s->gparts[k];
       }
     }
   }
@@ -382,82 +457,113 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
   int nr_in = 0, nr_out = 0;
   for (int k = 0; k < e->nr_proxies; k++) {
     if (e->proxies[k].nr_parts_in > 0) {
-      reqs_in[5 * k] = e->proxies[k].req_parts_in;
-      reqs_in[5 * k + 1] = e->proxies[k].req_xparts_in;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k] =
+          e->proxies[k].req_parts_in;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 1] =
+          e->proxies[k].req_xparts_in;
       nr_in += 2;
     } else {
-      reqs_in[5 * k] = reqs_in[5 * k + 1] = MPI_REQUEST_NULL;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k] =
+          reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 1] = MPI_REQUEST_NULL;
     }
     if (e->proxies[k].nr_gparts_in > 0) {
-      reqs_in[5 * k + 2] = e->proxies[k].req_gparts_in;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 2] =
+          e->proxies[k].req_gparts_in;
       nr_in += 1;
     } else {
-      reqs_in[5 * k + 2] = MPI_REQUEST_NULL;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 2] = MPI_REQUEST_NULL;
     }
     if (e->proxies[k].nr_sparts_in > 0) {
-      reqs_in[5 * k + 3] = e->proxies[k].req_sparts_in;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 3] =
+          e->proxies[k].req_sparts_in;
       nr_in += 1;
     } else {
-      reqs_in[5 * k + 3] = MPI_REQUEST_NULL;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 3] = MPI_REQUEST_NULL;
     }
     if (e->proxies[k].nr_bparts_in > 0) {
-      reqs_in[5 * k + 4] = e->proxies[k].req_bparts_in;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 4] =
+          e->proxies[k].req_bparts_in;
+      nr_in += 1;
+    } else {
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 4] = MPI_REQUEST_NULL;
+    }
+    if (e->proxies[k].nr_sinks_in > 0) {
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 5] =
+          e->proxies[k].req_sinks_in;
       nr_in += 1;
     } else {
-      reqs_in[5 * k + 4] = MPI_REQUEST_NULL;
+      reqs_in[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 5] = MPI_REQUEST_NULL;
     }
 
     if (e->proxies[k].nr_parts_out > 0) {
-      reqs_out[5 * k] = e->proxies[k].req_parts_out;
-      reqs_out[5 * k + 1] = e->proxies[k].req_xparts_out;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k] =
+          e->proxies[k].req_parts_out;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 1] =
+          e->proxies[k].req_xparts_out;
       nr_out += 2;
     } else {
-      reqs_out[5 * k] = reqs_out[5 * k + 1] = MPI_REQUEST_NULL;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k] =
+          reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 1] =
+              MPI_REQUEST_NULL;
     }
     if (e->proxies[k].nr_gparts_out > 0) {
-      reqs_out[5 * k + 2] = e->proxies[k].req_gparts_out;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 2] =
+          e->proxies[k].req_gparts_out;
       nr_out += 1;
     } else {
-      reqs_out[5 * k + 2] = MPI_REQUEST_NULL;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 2] = MPI_REQUEST_NULL;
     }
     if (e->proxies[k].nr_sparts_out > 0) {
-      reqs_out[5 * k + 3] = e->proxies[k].req_sparts_out;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 3] =
+          e->proxies[k].req_sparts_out;
       nr_out += 1;
     } else {
-      reqs_out[5 * k + 3] = MPI_REQUEST_NULL;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 3] = MPI_REQUEST_NULL;
     }
     if (e->proxies[k].nr_bparts_out > 0) {
-      reqs_out[5 * k + 4] = e->proxies[k].req_bparts_out;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 4] =
+          e->proxies[k].req_bparts_out;
       nr_out += 1;
     } else {
-      reqs_out[5 * k + 4] = MPI_REQUEST_NULL;
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 4] = MPI_REQUEST_NULL;
+    }
+    if (e->proxies[k].nr_sinks_out > 0) {
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 5] =
+          e->proxies[k].req_sinks_out;
+      nr_out += 1;
+    } else {
+      reqs_out[MPI_REQUEST_NUMBER_PARTICLE_TYPES * k + 5] = MPI_REQUEST_NULL;
     }
   }
 
   /* Wait for each part array to come in and collect the new
      parts from the proxies. */
-  int count_parts = 0, count_gparts = 0, count_sparts = 0, count_bparts = 0;
+  int count_parts = 0, count_gparts = 0, count_sparts = 0, count_bparts = 0,
+      count_sinks = 0;
   for (int k = 0; k < nr_in; k++) {
     int err, pid;
-    if ((err = MPI_Waitany(5 * e->nr_proxies, reqs_in, &pid,
-                           MPI_STATUS_IGNORE)) != MPI_SUCCESS) {
+    if ((err = MPI_Waitany(MPI_REQUEST_NUMBER_PARTICLE_TYPES * e->nr_proxies,
+                           reqs_in, &pid, MPI_STATUS_IGNORE)) != MPI_SUCCESS) {
       char buff[MPI_MAX_ERROR_STRING];
       int res;
       MPI_Error_string(err, buff, &res);
       error("MPI_Waitany failed (%s).", buff);
     }
     if (pid == MPI_UNDEFINED) break;
-    // message( "request from proxy %i has arrived." , pid / 5 );
-    pid = 5 * (pid / 5);
+    // message( "request from proxy %i has arrived." , pid /
+    // MPI_REQUEST_NUMBER_PARTICLE_TYPES );
+    pid = MPI_REQUEST_NUMBER_PARTICLE_TYPES *
+          (pid / MPI_REQUEST_NUMBER_PARTICLE_TYPES);
 
     /* If all the requests for a given proxy have arrived... */
     if (reqs_in[pid + 0] == MPI_REQUEST_NULL &&
         reqs_in[pid + 1] == MPI_REQUEST_NULL &&
         reqs_in[pid + 2] == MPI_REQUEST_NULL &&
         reqs_in[pid + 3] == MPI_REQUEST_NULL &&
-        reqs_in[pid + 4] == MPI_REQUEST_NULL) {
+        reqs_in[pid + 4] == MPI_REQUEST_NULL &&
+        reqs_in[pid + 5] == MPI_REQUEST_NULL) {
       /* Copy the particle data to the part/xpart/gpart arrays. */
-      struct proxy *prox = &e->proxies[pid / 5];
+      struct proxy *prox = &e->proxies[pid / MPI_REQUEST_NUMBER_PARTICLE_TYPES];
       memcpy(&s->parts[offset_parts + count_parts], prox->parts_in,
              sizeof(struct part) * prox->nr_parts_in);
       memcpy(&s->xparts[offset_parts + count_parts], prox->xparts_in,
@@ -468,6 +574,8 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
              sizeof(struct spart) * prox->nr_sparts_in);
       memcpy(&s->bparts[offset_bparts + count_bparts], prox->bparts_in,
              sizeof(struct bpart) * prox->nr_bparts_in);
+      memcpy(&s->sinks[offset_sinks + count_sinks], prox->sinks_in,
+             sizeof(struct sink) * prox->nr_sinks_in);
 
 #ifdef WITH_CSDS
       if (e->policy & engine_policy_csds) {
@@ -495,6 +603,12 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
         if (prox->nr_bparts_in > 0) {
           error("TODO");
         }
+
+        /* Log the sinks */
+        if (prox->nr_sinks_in > 0) {
+          /* Not implemented yet */
+          error("TODO");
+        }
       }
 #endif
       /* for (int k = offset; k < offset + count; k++)
@@ -522,6 +636,11 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
               &s->bparts[offset_bparts + count_bparts - gp->id_or_neg_offset];
           gp->id_or_neg_offset = s->bparts - bp;
           bp->gpart = gp;
+        } else if (gp->type == swift_type_sink) {
+          struct sink *sink =
+              &s->sinks[offset_sinks + count_sinks - gp->id_or_neg_offset];
+          gp->id_or_neg_offset = s->sinks - sink;
+          sink->gpart = gp;
         }
       }
 
@@ -530,13 +649,14 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
       count_gparts += prox->nr_gparts_in;
       count_sparts += prox->nr_sparts_in;
       count_bparts += prox->nr_bparts_in;
+      count_sinks += prox->nr_sinks_in;
     }
   }
 
   /* Wait for all the sends to have finished too. */
   if (nr_out > 0)
-    if (MPI_Waitall(5 * e->nr_proxies, reqs_out, MPI_STATUSES_IGNORE) !=
-        MPI_SUCCESS)
+    if (MPI_Waitall(MPI_REQUEST_NUMBER_PARTICLE_TYPES * e->nr_proxies, reqs_out,
+                    MPI_STATUSES_IGNORE) != MPI_SUCCESS)
       error("MPI_Waitall on sends failed.");
 
   /* Free the proxy memory */
@@ -553,6 +673,7 @@ void engine_exchange_strays(struct engine *e, const size_t offset_parts,
   *Ngpart = count_gparts;
   *Nspart = count_sparts;
   *Nbpart = count_bparts;
+  *Nsink = count_sinks;
 
 #else
   error("SWIFT was not compiled with MPI support.");
diff --git a/src/engine_unskip.c b/src/engine_unskip.c
index 30fc66aa16a7e87911a16e149aa6d1c64c1793ce..17389b388165a859d8425a392a56660ac7e70883 100644
--- a/src/engine_unskip.c
+++ b/src/engine_unskip.c
@@ -160,12 +160,6 @@ static void engine_do_unskip_black_holes(struct cell *c, struct engine *e) {
   /* Early abort (are we below the level where tasks are)? */
   if (!cell_get_flag(c, cell_flag_has_tasks)) return;
 
-  /* Ignore empty cells. */
-  if (c->black_holes.count == 0) return;
-
-  /* Skip inactive cells. */
-  if (!cell_is_active_black_holes(c, e)) return;
-
   /* Recurse */
   if (c->split) {
     for (int k = 0; k < 8; k++) {
@@ -428,12 +422,6 @@ void engine_unskip(struct engine *e) {
         memswap(&local_cells[k], &local_cells[num_active_cells], sizeof(int));
       num_active_cells += 1;
     }
-
-    /* Activate the top-level timestep exchange */
-#ifdef WITH_MPI
-    scheduler_activate_all_subtype(&e->sched, c->mpi.send, task_subtype_tend);
-    scheduler_activate_all_subtype(&e->sched, c->mpi.recv, task_subtype_tend);
-#endif
   }
 
   /* What kind of tasks do we have? */
diff --git a/src/entropy_floor/EAGLE/entropy_floor.c b/src/entropy_floor/EAGLE/entropy_floor.c
new file mode 100644
index 0000000000000000000000000000000000000000..f30c4d4086f55815baff33ef5163f90c1ec880ef
--- /dev/null
+++ b/src/entropy_floor/EAGLE/entropy_floor.c
@@ -0,0 +1,326 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2019 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#include <config.h>
+
+#ifdef HAVE_HDF5
+#include <hdf5.h>
+#endif
+
+/* Local includes */
+#include "cosmology.h"
+#include "entropy_floor.h"
+#include "hydro.h"
+#include "parser.h"
+
+/**
+ * @brief Compute the pressure from the entropy floor at a given density
+ *
+ * @param rho_phys The physical density (internal units).
+ * @param rho_com The comoving density (internal units).
+ * @param cosmo The cosmological model.
+ * @param props The properties of the entropy floor.
+ */
+float entropy_floor_gas_pressure(const float rho_phys, const float rho_com,
+                                 const struct cosmology *cosmo,
+                                 const struct entropy_floor_properties *props) {
+
+  /* Mean baryon density in co-moving internal units for over-density condition
+   * (Recall cosmo->critical_density_0 is 0 in a non-cosmological run,
+   * making the over-density condition a no-op) */
+  const float rho_crit_0 = cosmo->critical_density_0;
+  const float rho_crit_baryon = cosmo->Omega_b * rho_crit_0;
+
+  /* Physical pressure */
+  float pressure = 0.f;
+
+  /* Are we in the regime of the Jeans equation of state? */
+  if ((rho_com >= rho_crit_baryon * props->Jeans_over_density_threshold) &&
+      (rho_phys >= props->Jeans_density_threshold)) {
+
+    const float pressure_Jeans =
+        props->Jeans_pressure_norm *
+        powf(rho_phys * props->Jeans_density_threshold_inv,
+             props->Jeans_gamma_effective);
+
+    pressure = max(pressure, pressure_Jeans);
+  }
+
+  /* Are we in the regime of the Cool equation of state? */
+  if ((rho_com >= rho_crit_baryon * props->Cool_over_density_threshold) &&
+      (rho_phys >= props->Cool_density_threshold)) {
+
+    const float pressure_Cool =
+        props->Cool_pressure_norm *
+        powf(rho_phys * props->Cool_density_threshold_inv,
+             props->Cool_gamma_effective);
+
+    pressure = max(pressure, pressure_Cool);
+  }
+
+  return pressure;
+}
+
+/**
+ * @brief Compute the entropy floor of a given #part.
+ *
+ * Note that the particle is not updated!!
+ *
+ * @param p The #part.
+ * @param cosmo The cosmological model.
+ * @param props The properties of the entropy floor.
+ */
+float entropy_floor(const struct part *p, const struct cosmology *cosmo,
+                    const struct entropy_floor_properties *props) {
+
+  /* Comoving density in internal units */
+  const float rho_com = hydro_get_comoving_density(p);
+
+  /* Physical density in internal units */
+  const float rho_phys = hydro_get_physical_density(p, cosmo);
+
+  const float pressure =
+      entropy_floor_gas_pressure(rho_phys, rho_com, cosmo, props);
+
+  /* Convert to an entropy.
+   * (Recall that the entropy is the same in co-moving and physical frames) */
+  return gas_entropy_from_pressure(rho_phys, pressure);
+}
+
+/**
+ * @brief Compute the temperature from the entropy floor at a given density
+ *
+ * This is the temperature exactly corresponding to the imposed EoS shape.
+ * It only matches the entropy returned by the entropy_floor() function
+ * for a neutral gas with primoridal abundance.
+ *
+ * @param rho_phys The physical density (internal units).
+ * @param rho_com The comoving density (internal units).
+ * @param cosmo The cosmological model.
+ * @param props The properties of the entropy floor.
+ */
+float entropy_floor_gas_temperature(
+    const float rho_phys, const float rho_com, const struct cosmology *cosmo,
+    const struct entropy_floor_properties *props) {
+
+  /* Mean baryon density in co-moving internal units for over-density condition
+   * (Recall cosmo->critical_density_0 is 0 in a non-cosmological run,
+   * making the over-density condition a no-op) */
+  const float rho_crit_0 = cosmo->critical_density_0;
+  const float rho_crit_baryon = cosmo->Omega_b * rho_crit_0;
+
+  /* Physical */
+  float temperature = 0.f;
+
+  /* Are we in the regime of the Jeans equation of state? */
+  if ((rho_com >= rho_crit_baryon * props->Jeans_over_density_threshold) &&
+      (rho_phys >= props->Jeans_density_threshold)) {
+
+    const float jeans_slope = props->Jeans_gamma_effective - 1.f;
+
+    const float temperature_Jeans =
+        props->Jeans_temperature_norm *
+        pow(rho_phys * props->Jeans_density_threshold_inv, jeans_slope);
+
+    temperature = max(temperature, temperature_Jeans);
+  }
+
+  /* Are we in the regime of the Cool equation of state? */
+  if ((rho_com >= rho_crit_baryon * props->Cool_over_density_threshold) &&
+      (rho_phys >= props->Cool_density_threshold)) {
+
+    const float cool_slope = props->Cool_gamma_effective - 1.f;
+
+    const float temperature_Cool =
+        props->Cool_temperature_norm *
+        pow(rho_phys * props->Cool_density_threshold_inv, cool_slope);
+
+    temperature = max(temperature, temperature_Cool);
+  }
+
+  return temperature;
+}
+
+/**
+ * @brief Compute the temperature from the entropy floor for a given #part
+ *
+ * Calculate the EoS temperature, the particle is not updated.
+ * This is the temperature exactly corresponding to the imposed EoS shape.
+ * It only matches the entropy returned by the entropy_floor() function
+ * for a neutral gas with primoridal abundance.
+ *
+ * @param p The #part.
+ * @param cosmo The cosmological model.
+ * @param props The properties of the entropy floor.
+ */
+float entropy_floor_temperature(const struct part *p,
+                                const struct cosmology *cosmo,
+                                const struct entropy_floor_properties *props) {
+
+  /* Comoving density in internal units */
+  const float rho_com = hydro_get_comoving_density(p);
+
+  /* Physical density in internal units */
+  const float rho_phys = hydro_get_physical_density(p, cosmo);
+
+  return entropy_floor_gas_temperature(rho_phys, rho_com, cosmo, props);
+}
+
+/**
+ * @brief Initialise the entropy floor by reading the parameters and converting
+ * to internal units.
+ *
+ * The input temperatures and number densities are converted to entropy and
+ * density assuming a neutral gas of primoridal abundance.
+ *
+ * @param params The YAML parameter file.
+ * @param us The system of units used internally.
+ * @param phys_const The physical constants.
+ * @param hydro_props The propoerties of the hydro scheme.
+ * @param props The entropy floor properties to fill.
+ */
+void entropy_floor_init(struct entropy_floor_properties *props,
+                        const struct phys_const *phys_const,
+                        const struct unit_system *us,
+                        const struct hydro_props *hydro_props,
+                        struct swift_params *params) {
+
+  /* Read the parameters in the units they are set */
+  props->Jeans_density_threshold_H_p_cm3 = parser_get_param_float(
+      params, "EAGLEEntropyFloor:Jeans_density_threshold_H_p_cm3");
+  props->Jeans_over_density_threshold = parser_get_param_float(
+      params, "EAGLEEntropyFloor:Jeans_over_density_threshold");
+  props->Jeans_temperature_norm_K = parser_get_param_float(
+      params, "EAGLEEntropyFloor:Jeans_temperature_norm_K");
+  props->Jeans_gamma_effective =
+      parser_get_param_float(params, "EAGLEEntropyFloor:Jeans_gamma_effective");
+
+  props->Cool_density_threshold_H_p_cm3 = parser_get_param_float(
+      params, "EAGLEEntropyFloor:Cool_density_threshold_H_p_cm3");
+  props->Cool_over_density_threshold = parser_get_param_float(
+      params, "EAGLEEntropyFloor:Cool_over_density_threshold");
+  props->Cool_temperature_norm_K = parser_get_param_float(
+      params, "EAGLEEntropyFloor:Cool_temperature_norm_K");
+  props->Cool_gamma_effective =
+      parser_get_param_float(params, "EAGLEEntropyFloor:Cool_gamma_effective");
+
+  /* Cross-check that the input makes sense */
+  if (props->Cool_density_threshold_H_p_cm3 >=
+      props->Jeans_density_threshold_H_p_cm3) {
+    error(
+        "Invalid values for the entrop floor density thresholds. The 'Jeans' "
+        "threshold (%e cm^-3) should be at a higher density than the 'Cool' "
+        "threshold (%e cm^-3)",
+        props->Jeans_density_threshold_H_p_cm3,
+        props->Cool_density_threshold_H_p_cm3);
+  }
+
+  /* Initial Hydrogen abundance (mass fraction) */
+  const double X_H = hydro_props->hydrogen_mass_fraction;
+
+  /* Now convert to internal units assuming primodial Hydrogen abundance */
+  props->Jeans_temperature_norm =
+      props->Jeans_temperature_norm_K /
+      units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+  props->Jeans_density_threshold =
+      props->Jeans_density_threshold_H_p_cm3 /
+      units_cgs_conversion_factor(us, UNIT_CONV_NUMBER_DENSITY) *
+      phys_const->const_proton_mass / X_H;
+
+  props->Cool_temperature_norm =
+      props->Cool_temperature_norm_K /
+      units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+  props->Cool_density_threshold =
+      props->Cool_density_threshold_H_p_cm3 /
+      units_cgs_conversion_factor(us, UNIT_CONV_NUMBER_DENSITY) *
+      phys_const->const_proton_mass / X_H;
+
+  /* We assume neutral gas */
+  const float mean_molecular_weight = hydro_props->mu_neutral;
+
+  /* Get the common terms */
+  props->Jeans_density_threshold_inv = 1.f / props->Jeans_density_threshold;
+  props->Cool_density_threshold_inv = 1.f / props->Cool_density_threshold;
+
+  /* P_norm = (k_B * T) / (m_p * mu) * rho_threshold */
+  props->Jeans_pressure_norm =
+      ((phys_const->const_boltzmann_k * props->Jeans_temperature_norm) /
+       (phys_const->const_proton_mass * mean_molecular_weight)) *
+      props->Jeans_density_threshold;
+
+  props->Cool_pressure_norm =
+      ((phys_const->const_boltzmann_k * props->Cool_temperature_norm) /
+       (phys_const->const_proton_mass * mean_molecular_weight)) *
+      props->Cool_density_threshold;
+}
+
+/**
+ * @brief Print the properties of the entropy floor to stdout.
+ *
+ * @param props The entropy floor properties.
+ */
+void entropy_floor_print(const struct entropy_floor_properties *props) {
+
+  message("Entropy floor is 'EAGLE' with:");
+  message("Jeans limiter with slope n=%.3f at rho=%e (%e H/cm^3) and T=%.1f K",
+          props->Jeans_gamma_effective, props->Jeans_density_threshold,
+          props->Jeans_density_threshold_H_p_cm3,
+          props->Jeans_temperature_norm);
+  message(" Cool limiter with slope n=%.3f at rho=%e (%e H/cm^3) and T=%.1f K",
+          props->Cool_gamma_effective, props->Cool_density_threshold,
+          props->Cool_density_threshold_H_p_cm3, props->Cool_temperature_norm);
+}
+
+#ifdef HAVE_HDF5
+
+/**
+ * @brief Writes the current model of entropy floor to the file
+ * @param h_grp The HDF5 group in which to write
+ */
+void entropy_floor_write_flavour(hid_t h_grp) {
+
+  io_write_attribute_s(h_grp, "Entropy floor", "EAGLE");
+}
+#endif
+
+/**
+ * @brief Write an entropy floor struct to the given FILE as a stream of bytes.
+ *
+ * @param props the struct
+ * @param stream the file stream
+ */
+void entropy_floor_struct_dump(const struct entropy_floor_properties *props,
+                               FILE *stream) {
+
+  restart_write_blocks((void *)props, sizeof(struct entropy_floor_properties),
+                       1, stream, "entropy floor", "entropy floor properties");
+}
+
+/**
+ * @brief Restore a entropy floor struct from the given FILE as a stream of
+ * bytes.
+ *
+ * @param props the struct
+ * @param stream the file stream
+ */
+void entropy_floor_struct_restore(struct entropy_floor_properties *props,
+                                  FILE *stream) {
+
+  restart_read_blocks((void *)props, sizeof(struct entropy_floor_properties), 1,
+                      stream, NULL, "entropy floor properties");
+}
diff --git a/src/entropy_floor/EAGLE/entropy_floor.h b/src/entropy_floor/EAGLE/entropy_floor.h
index ba44ef4a2fe085e4746e9d8810fbda260f6b1f86..298c67f1afa052bb3c1dae0152dec3a9b5fac8e9 100644
--- a/src/entropy_floor/EAGLE/entropy_floor.h
+++ b/src/entropy_floor/EAGLE/entropy_floor.h
@@ -19,13 +19,19 @@
 #ifndef SWIFT_ENTROPY_FLOOR_EAGLE_H
 #define SWIFT_ENTROPY_FLOOR_EAGLE_H
 
-#include "adiabatic_index.h"
-#include "cosmology.h"
-#include "equation_of_state.h"
-#include "hydro.h"
-#include "hydro_properties.h"
-#include "parser.h"
-#include "units.h"
+/* Code config */
+#include <config.h>
+
+/* System include */
+#include <stdio.h>
+
+/* Pre-declarations */
+struct cosmology;
+struct part;
+struct phys_const;
+struct unit_system;
+struct hydro_props;
+struct swift_params;
 
 /**
  * @file src/entropy_floor/EAGLE/entropy_floor.h
@@ -86,303 +92,38 @@ struct entropy_floor_properties {
   float Cool_pressure_norm;
 };
 
-/**
- * @brief Compute the pressure from the entropy floor at a given density
- *
- * @param rho_phys The physical density (internal units).
- * @param rho_com The comoving density (internal units).
- * @param cosmo The cosmological model.
- * @param props The properties of the entropy floor.
- */
-static INLINE float entropy_floor_gas_pressure(
-    const float rho_phys, const float rho_com, const struct cosmology *cosmo,
-    const struct entropy_floor_properties *props) {
-
-  /* Mean baryon density in co-moving internal units for over-density condition
-   * (Recall cosmo->critical_density_0 is 0 in a non-cosmological run,
-   * making the over-density condition a no-op) */
-  const float rho_crit_0 = cosmo->critical_density_0;
-  const float rho_crit_baryon = cosmo->Omega_b * rho_crit_0;
-
-  /* Physical pressure */
-  float pressure = 0.f;
-
-  /* Are we in the regime of the Jeans equation of state? */
-  if ((rho_com >= rho_crit_baryon * props->Jeans_over_density_threshold) &&
-      (rho_phys >= props->Jeans_density_threshold)) {
-
-    const float pressure_Jeans =
-        props->Jeans_pressure_norm *
-        powf(rho_phys * props->Jeans_density_threshold_inv,
-             props->Jeans_gamma_effective);
-
-    pressure = max(pressure, pressure_Jeans);
-  }
-
-  /* Are we in the regime of the Cool equation of state? */
-  if ((rho_com >= rho_crit_baryon * props->Cool_over_density_threshold) &&
-      (rho_phys >= props->Cool_density_threshold)) {
-
-    const float pressure_Cool =
-        props->Cool_pressure_norm *
-        powf(rho_phys * props->Cool_density_threshold_inv,
-             props->Cool_gamma_effective);
-
-    pressure = max(pressure, pressure_Cool);
-  }
-
-  return pressure;
-}
-
-/**
- * @brief Compute the entropy floor of a given #part.
- *
- * Note that the particle is not updated!!
- *
- * @param p The #part.
- * @param cosmo The cosmological model.
- * @param props The properties of the entropy floor.
- */
-static INLINE float entropy_floor(
-    const struct part *p, const struct cosmology *cosmo,
-    const struct entropy_floor_properties *props) {
+float entropy_floor_gas_pressure(const float rho_phys, const float rho_com,
+                                 const struct cosmology *cosmo,
+                                 const struct entropy_floor_properties *props);
 
-  /* Comoving density in internal units */
-  const float rho_com = hydro_get_comoving_density(p);
+float entropy_floor(const struct part *p, const struct cosmology *cosmo,
+                    const struct entropy_floor_properties *props);
 
-  /* Physical density in internal units */
-  const float rho_phys = hydro_get_physical_density(p, cosmo);
-
-  const float pressure =
-      entropy_floor_gas_pressure(rho_phys, rho_com, cosmo, props);
-
-  /* Convert to an entropy.
-   * (Recall that the entropy is the same in co-moving and physical frames) */
-  return gas_entropy_from_pressure(rho_phys, pressure);
-}
-
-/**
- * @brief Compute the temperature from the entropy floor at a given density
- *
- * This is the temperature exactly corresponding to the imposed EoS shape.
- * It only matches the entropy returned by the entropy_floor() function
- * for a neutral gas with primoridal abundance.
- *
- * @param rho_phys The physical density (internal units).
- * @param rho_com The comoving density (internal units).
- * @param cosmo The cosmological model.
- * @param props The properties of the entropy floor.
- */
-static INLINE float entropy_floor_gas_temperature(
+float entropy_floor_gas_temperature(
     const float rho_phys, const float rho_com, const struct cosmology *cosmo,
-    const struct entropy_floor_properties *props) {
-
-  /* Mean baryon density in co-moving internal units for over-density condition
-   * (Recall cosmo->critical_density_0 is 0 in a non-cosmological run,
-   * making the over-density condition a no-op) */
-  const float rho_crit_0 = cosmo->critical_density_0;
-  const float rho_crit_baryon = cosmo->Omega_b * rho_crit_0;
-
-  /* Physical */
-  float temperature = 0.f;
-
-  /* Are we in the regime of the Jeans equation of state? */
-  if ((rho_com >= rho_crit_baryon * props->Jeans_over_density_threshold) &&
-      (rho_phys >= props->Jeans_density_threshold)) {
-
-    const float jeans_slope = props->Jeans_gamma_effective - 1.f;
-
-    const float temperature_Jeans =
-        props->Jeans_temperature_norm *
-        pow(rho_phys * props->Jeans_density_threshold_inv, jeans_slope);
+    const struct entropy_floor_properties *props);
 
-    temperature = max(temperature, temperature_Jeans);
-  }
+float entropy_floor_temperature(const struct part *p,
+                                const struct cosmology *cosmo,
+                                const struct entropy_floor_properties *props);
 
-  /* Are we in the regime of the Cool equation of state? */
-  if ((rho_com >= rho_crit_baryon * props->Cool_over_density_threshold) &&
-      (rho_phys >= props->Cool_density_threshold)) {
+void entropy_floor_init(struct entropy_floor_properties *props,
+                        const struct phys_const *phys_const,
+                        const struct unit_system *us,
+                        const struct hydro_props *hydro_props,
+                        struct swift_params *params);
 
-    const float cool_slope = props->Cool_gamma_effective - 1.f;
-
-    const float temperature_Cool =
-        props->Cool_temperature_norm *
-        pow(rho_phys * props->Cool_density_threshold_inv, cool_slope);
-
-    temperature = max(temperature, temperature_Cool);
-  }
-
-  return temperature;
-}
-
-/**
- * @brief Compute the temperature from the entropy floor for a given #part
- *
- * Calculate the EoS temperature, the particle is not updated.
- * This is the temperature exactly corresponding to the imposed EoS shape.
- * It only matches the entropy returned by the entropy_floor() function
- * for a neutral gas with primoridal abundance.
- *
- * @param p The #part.
- * @param cosmo The cosmological model.
- * @param props The properties of the entropy floor.
- */
-static INLINE float entropy_floor_temperature(
-    const struct part *p, const struct cosmology *cosmo,
-    const struct entropy_floor_properties *props) {
-
-  /* Comoving density in internal units */
-  const float rho_com = hydro_get_comoving_density(p);
-
-  /* Physical density in internal units */
-  const float rho_phys = hydro_get_physical_density(p, cosmo);
-
-  return entropy_floor_gas_temperature(rho_phys, rho_com, cosmo, props);
-}
-
-/**
- * @brief Initialise the entropy floor by reading the parameters and converting
- * to internal units.
- *
- * The input temperatures and number densities are converted to entropy and
- * density assuming a neutral gas of primoridal abundance.
- *
- * @param params The YAML parameter file.
- * @param us The system of units used internally.
- * @param phys_const The physical constants.
- * @param hydro_props The propoerties of the hydro scheme.
- * @param props The entropy floor properties to fill.
- */
-static INLINE void entropy_floor_init(struct entropy_floor_properties *props,
-                                      const struct phys_const *phys_const,
-                                      const struct unit_system *us,
-                                      const struct hydro_props *hydro_props,
-                                      struct swift_params *params) {
-
-  /* Read the parameters in the units they are set */
-  props->Jeans_density_threshold_H_p_cm3 = parser_get_param_float(
-      params, "EAGLEEntropyFloor:Jeans_density_threshold_H_p_cm3");
-  props->Jeans_over_density_threshold = parser_get_param_float(
-      params, "EAGLEEntropyFloor:Jeans_over_density_threshold");
-  props->Jeans_temperature_norm_K = parser_get_param_float(
-      params, "EAGLEEntropyFloor:Jeans_temperature_norm_K");
-  props->Jeans_gamma_effective =
-      parser_get_param_float(params, "EAGLEEntropyFloor:Jeans_gamma_effective");
-
-  props->Cool_density_threshold_H_p_cm3 = parser_get_param_float(
-      params, "EAGLEEntropyFloor:Cool_density_threshold_H_p_cm3");
-  props->Cool_over_density_threshold = parser_get_param_float(
-      params, "EAGLEEntropyFloor:Cool_over_density_threshold");
-  props->Cool_temperature_norm_K = parser_get_param_float(
-      params, "EAGLEEntropyFloor:Cool_temperature_norm_K");
-  props->Cool_gamma_effective =
-      parser_get_param_float(params, "EAGLEEntropyFloor:Cool_gamma_effective");
-
-  /* Cross-check that the input makes sense */
-  if (props->Cool_density_threshold_H_p_cm3 >=
-      props->Jeans_density_threshold_H_p_cm3) {
-    error(
-        "Invalid values for the entrop floor density thresholds. The 'Jeans' "
-        "threshold (%e cm^-3) should be at a higher density than the 'Cool' "
-        "threshold (%e cm^-3)",
-        props->Jeans_density_threshold_H_p_cm3,
-        props->Cool_density_threshold_H_p_cm3);
-  }
-
-  /* Initial Hydrogen abundance (mass fraction) */
-  const double X_H = hydro_props->hydrogen_mass_fraction;
-
-  /* Now convert to internal units assuming primodial Hydrogen abundance */
-  props->Jeans_temperature_norm =
-      props->Jeans_temperature_norm_K /
-      units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
-  props->Jeans_density_threshold =
-      props->Jeans_density_threshold_H_p_cm3 /
-      units_cgs_conversion_factor(us, UNIT_CONV_NUMBER_DENSITY) *
-      phys_const->const_proton_mass / X_H;
-
-  props->Cool_temperature_norm =
-      props->Cool_temperature_norm_K /
-      units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
-  props->Cool_density_threshold =
-      props->Cool_density_threshold_H_p_cm3 /
-      units_cgs_conversion_factor(us, UNIT_CONV_NUMBER_DENSITY) *
-      phys_const->const_proton_mass / X_H;
-
-  /* We assume neutral gas */
-  const float mean_molecular_weight = hydro_props->mu_neutral;
-
-  /* Get the common terms */
-  props->Jeans_density_threshold_inv = 1.f / props->Jeans_density_threshold;
-  props->Cool_density_threshold_inv = 1.f / props->Cool_density_threshold;
-
-  /* P_norm = (k_B * T) / (m_p * mu) * rho_threshold */
-  props->Jeans_pressure_norm =
-      ((phys_const->const_boltzmann_k * props->Jeans_temperature_norm) /
-       (phys_const->const_proton_mass * mean_molecular_weight)) *
-      props->Jeans_density_threshold;
-
-  props->Cool_pressure_norm =
-      ((phys_const->const_boltzmann_k * props->Cool_temperature_norm) /
-       (phys_const->const_proton_mass * mean_molecular_weight)) *
-      props->Cool_density_threshold;
-}
-
-/**
- * @brief Print the properties of the entropy floor to stdout.
- *
- * @param props The entropy floor properties.
- */
-static INLINE void entropy_floor_print(
-    const struct entropy_floor_properties *props) {
-
-  message("Entropy floor is 'EAGLE' with:");
-  message("Jeans limiter with slope n=%.3f at rho=%e (%e H/cm^3) and T=%.1f K",
-          props->Jeans_gamma_effective, props->Jeans_density_threshold,
-          props->Jeans_density_threshold_H_p_cm3,
-          props->Jeans_temperature_norm);
-  message(" Cool limiter with slope n=%.3f at rho=%e (%e H/cm^3) and T=%.1f K",
-          props->Cool_gamma_effective, props->Cool_density_threshold,
-          props->Cool_density_threshold_H_p_cm3, props->Cool_temperature_norm);
-}
+void entropy_floor_print(const struct entropy_floor_properties *props);
 
 #ifdef HAVE_HDF5
 
-/**
- * @brief Writes the current model of entropy floor to the file
- * @param h_grp The HDF5 group in which to write
- */
-INLINE static void entropy_floor_write_flavour(hid_t h_grp) {
-
-  io_write_attribute_s(h_grp, "Entropy floor", "EAGLE");
-}
+void entropy_floor_write_flavour(hid_t h_grp);
 #endif
 
-/**
- * @brief Write an entropy floor struct to the given FILE as a stream of bytes.
- *
- * @param props the struct
- * @param stream the file stream
- */
-static INLINE void entropy_floor_struct_dump(
-    const struct entropy_floor_properties *props, FILE *stream) {
-
-  restart_write_blocks((void *)props, sizeof(struct entropy_floor_properties),
-                       1, stream, "entropy floor", "entropy floor properties");
-}
-
-/**
- * @brief Restore a entropy floor struct from the given FILE as a stream of
- * bytes.
- *
- * @param props the struct
- * @param stream the file stream
- */
-static INLINE void entropy_floor_struct_restore(
-    struct entropy_floor_properties *props, FILE *stream) {
+void entropy_floor_struct_dump(const struct entropy_floor_properties *props,
+                               FILE *stream);
 
-  restart_read_blocks((void *)props, sizeof(struct entropy_floor_properties), 1,
-                      stream, NULL, "entropy floor properties");
-}
+void entropy_floor_struct_restore(struct entropy_floor_properties *props,
+                                  FILE *stream);
 
 #endif /* SWIFT_ENTROPY_FLOOR_EAGLE_H */
diff --git a/src/equation_of_state.c b/src/equation_of_state.c
index 5b1c0dc50b18f321e57a90e00a0a0363d90a2f1e..d6fb9cb64a904d4de02bf87a49acde954fad0a95 100644
--- a/src/equation_of_state.c
+++ b/src/equation_of_state.c
@@ -35,6 +35,9 @@
 struct eos_parameters eos = {.Til_iron.rho_0 = -1.f};
 #elif defined(EOS_ISOTHERMAL_GAS)
 struct eos_parameters eos = {.isothermal_internal_energy = -1.};
+#elif defined(EOS_BAROTROPIC_GAS)
+struct eos_parameters eos = {.vacuum_sound_speed2 = -1.,
+                             .inverse_core_density = -1.};
 #else
 struct eos_parameters eos;
 #endif
diff --git a/src/equation_of_state.h b/src/equation_of_state.h
index 769e3dbb6888f52efc16d52858b26ed0d8150515..a002118020acadbc8d50f6f18013f8e150a6cc55 100644
--- a/src/equation_of_state.h
+++ b/src/equation_of_state.h
@@ -36,6 +36,8 @@
 #include "./equation_of_state/isothermal/equation_of_state.h"
 #elif defined(EOS_PLANETARY)
 #include "./equation_of_state/planetary/equation_of_state.h"
+#elif defined(EOS_BAROTROPIC_GAS)
+#include "./equation_of_state/barotropic/equation_of_state.h"
 #else
 #error "Invalid choice of equation of state"
 #endif
diff --git a/src/equation_of_state/barotropic/equation_of_state.h b/src/equation_of_state/barotropic/equation_of_state.h
new file mode 100644
index 0000000000000000000000000000000000000000..71be2944437879bb0fa65142d7674fa830ed824c
--- /dev/null
+++ b/src/equation_of_state/barotropic/equation_of_state.h
@@ -0,0 +1,277 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023  Orestis Karapiperis (karapiperis@lorentz.leidenuniv.nl).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_BAROTROPIC_GAS_EQUATION_OF_STATE_H
+#define SWIFT_BAROTROPIC_GAS_EQUATION_OF_STATE_H
+
+/* Some standard headers. */
+#include <math.h>
+
+/* Local headers. */
+#include "adiabatic_index.h"
+#include "common_io.h"
+#include "inline.h"
+#include "physical_constants.h"
+
+extern struct eos_parameters eos;
+
+/**
+ * @brief The parameters of the equation of state for the gas.
+ *
+ * Barotropic equation of state from Hennebelle et al., 2008, A&A, 477, 9
+ * reimplemented following Pakmor et al., 2011, MNRAS, 418, 1392
+ */
+struct eos_parameters {
+
+  /*! Square of barotropic sound speed in vacuum */
+  float vacuum_sound_speed2;
+
+  /*! Inverse of the core density */
+  float inverse_core_density;
+};
+
+/**
+ * @brief Returns the internal energy given density and entropy
+ *
+ * Since we are using a barotropic EoS, the entropy value is ignored.
+ * Computes \f$u = c_0^2 \frac{1 + \left(\frac{\rho}{\rho_c}\right)^\gamma
+ * }{\gamma - 1}\f$.
+ *
+ * @param density The density \f$\rho\f$.
+ * @param entropy The entropy \f$A\f$.
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_internal_energy_from_entropy(const float density, const float entropy) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return eos.vacuum_sound_speed2 * sqrtf(1.0f + density_factor) *
+         hydro_one_over_gamma_minus_one;
+}
+
+/**
+ * @brief Returns the pressure given density and entropy
+ *
+ * Since we are using a barotropic EoS, the entropy value is ignored.
+ * Computes \f$P = c_0^2 \left(1 +
+ * \left(\frac{\rho}{\rho_c}\right)^\gamma\right)\f$.
+ *
+ * @param density The density \f$\rho\f$.
+ * @param entropy The entropy \f$A\f$.
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_pressure_from_entropy(const float density, const float entropy) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return eos.vacuum_sound_speed2 * density * sqrtf(1.0f + density_factor);
+}
+
+/**
+ * @brief Returns the entropy given density and pressure.
+ *
+ * Since we are using a barotropic EoS, the pressure value is ignored.
+ * Computes \f$A = \rho^{1-\gamma}c_0^2 \left(1 +
+ * \left(\frac{\rho}{\rho_c}\right)^\gamma\right)\f$.
+ *
+ * @param density The density \f$\rho\f$.
+ * @param pressure The pressure \f$P\f$.
+ * @return The entropy \f$A\f$.
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_entropy_from_pressure(const float density, const float pressure) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return eos.vacuum_sound_speed2 * pow_minus_gamma_minus_one(density) *
+         sqrtf(1.0f + density_factor);
+}
+
+/**
+ * @brief Returns the sound speed given density and entropy
+ *
+ * Since we are using a barotropic EoS, the entropy is ignored.
+ * Computes \f$c = \sqrt{c_0^2 \left(1 +
+ * \left(\frac{\rho}{\rho_c}\right)^\gamma\right)}\f$.
+ *
+ * @param density The density \f$\rho\f$.
+ * @param entropy The entropy \f$A\f$.
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_soundspeed_from_entropy(const float density, const float entropy) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return sqrtf(eos.vacuum_sound_speed2 * sqrtf(1.0f + density_factor));
+}
+
+/**
+ * @brief Returns the entropy given density and internal energy
+ *
+ * Since we are using a barotropic EoS, the internal energy value is ignored.
+ * Computes \f$A = \rho^{1-\gamma}c_0^2 \left(1 +
+ * \left(\frac{\rho}{\rho_c}\right)^\gamma\right)\f$.
+ *
+ * @param density The density \f$\rho\f$
+ * @param u The internal energy \f$u\f$
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_entropy_from_internal_energy(const float density, const float u) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return eos.vacuum_sound_speed2 * pow_minus_gamma_minus_one(density) *
+         sqrtf(1.0f + density_factor);
+}
+
+/**
+ * @brief Returns the pressure given density and internal energy
+ *
+ * Since we are using a barotropic EoS, the internal energy value is ignored.
+ * Computes \f$P = c_0^2 \left(1 +
+ * \left(\frac{\rho}{\rho_c}\right)^\gamma\right)\f$.
+ *
+ * @param density The density \f$\rho\f$
+ * @param u The internal energy \f$u\f$
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_pressure_from_internal_energy(const float density, const float u) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return eos.vacuum_sound_speed2 * density * sqrtf(1.0f + density_factor);
+}
+
+/**
+ * @brief Returns the internal energy given density and pressure.
+ *
+ * Since we are using a barotropic EoS, the pressure value is ignored.
+ * Computes \f$u = c_0^2 \frac{1 + \left(\frac{\rho}{\rho_c}\right)^\gamma
+ * }{\gamma - 1}\f$.
+ *
+ * @param density The density \f$\rho\f$.
+ * @param pressure The pressure \f$P\f$.
+ * @return The internal energy \f$u\f$.
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_internal_energy_from_pressure(const float density, const float pressure) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return eos.vacuum_sound_speed2 * sqrtf(1.0f + density_factor) *
+         hydro_one_over_gamma_minus_one;
+}
+
+/**
+ * @brief Returns the sound speed given density and internal energy
+ *
+ * Since we are using a barotropic EoS, the internal energy value is ignored.
+ * Computes \f$c = \sqrt{c_0^2 \left(1 +
+ * \left(\frac{\rho}{\rho_c}\right)^\gamma\right)}\f$.
+ *
+ * @param density The density \f$\rho\f$
+ * @param u The internal energy \f$u\f$
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_soundspeed_from_internal_energy(const float density, const float u) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return sqrtf(eos.vacuum_sound_speed2 * sqrtf(1.0f + density_factor));
+}
+
+/**
+ * @brief Returns the sound speed given density and pressure
+ *
+ * Since we are using a barotropic EoS, the pressure value is ignored.
+ * Computes \f$c = \sqrt{c_0^2 \left(1 +
+ * \left(\frac{\rho}{\rho_c}\right)^\gamma\right)}\f$.
+ *
+ * @param density The density \f$\rho\f$
+ * @param P The pressure \f$P\f$
+ */
+__attribute__((always_inline, const)) INLINE static float
+gas_soundspeed_from_pressure(const float density, const float P) {
+
+  const float density_frac = density * eos.inverse_core_density;
+  const float density_factor = pow_gamma(density_frac);
+
+  return sqrtf(eos.vacuum_sound_speed2 * sqrtf(1.0f + density_factor));
+}
+
+/**
+ * @brief Initialize the eos parameters
+ *
+ * Read the vacuum sound speed and core density from the parameter file.
+ *
+ * @param e The #eos_parameters.
+ * @param phys_const The physical constants in the internal unit system.
+ * @param us The internal unit system.
+ * @param params The parsed parameters.
+ */
+INLINE static void eos_init(struct eos_parameters *e,
+                            const struct phys_const *phys_const,
+                            const struct unit_system *us,
+                            struct swift_params *params) {
+
+  const float vacuum_sound_speed =
+      parser_get_param_float(params, "EoS:barotropic_vacuum_sound_speed");
+  e->vacuum_sound_speed2 = vacuum_sound_speed * vacuum_sound_speed;
+  e->inverse_core_density =
+      1.f / parser_get_param_float(params, "EoS:barotropic_core_density");
+}
+/**
+ * @brief Print the equation of state
+ *
+ * @param e The #eos_parameters
+ */
+INLINE static void eos_print(const struct eos_parameters *e) {
+
+  message(
+      "Equation of state: Barotropic gas with vacuum sound speed set to %f and "
+      "core density set to %f.",
+      sqrtf(e->vacuum_sound_speed2), 1. / e->inverse_core_density);
+
+  message("Adiabatic index gamma: %f.", hydro_gamma);
+}
+
+#if defined(HAVE_HDF5)
+/**
+ * @brief Write equation of state information to the snapshot
+ *
+ * @param h_grpsph The HDF5 group in which to write
+ * @param e The #eos_parameters
+ */
+INLINE static void eos_print_snapshot(hid_t h_grpsph,
+                                      const struct eos_parameters *e) {
+
+  io_write_attribute_f(h_grpsph, "Adiabatic index", hydro_gamma);
+
+  io_write_attribute_s(h_grpsph, "Equation of state", "Barotropic gas");
+}
+#endif
+
+#endif /* SWIFT_BAROTROPIC_GAS_EQUATION_OF_STATE_H */
diff --git a/src/equation_of_state/ideal_gas/equation_of_state.h b/src/equation_of_state/ideal_gas/equation_of_state.h
index cfdff5c7fd4af8052cf4a36b8bace66718e235e5..61d76e4fb36d204f9c40ef3c76a7d875ff59a5be 100644
--- a/src/equation_of_state/ideal_gas/equation_of_state.h
+++ b/src/equation_of_state/ideal_gas/equation_of_state.h
@@ -46,7 +46,7 @@ struct eos_parameters {};
  * @param entropy The entropy \f$A\f$.
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_internal_energy_from_entropy(float density, float entropy) {
+gas_internal_energy_from_entropy(const float density, const float entropy) {
 
   return entropy * pow_gamma_minus_one(density) *
          hydro_one_over_gamma_minus_one;
@@ -61,7 +61,7 @@ gas_internal_energy_from_entropy(float density, float entropy) {
  * @param entropy The entropy \f$A\f$.
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_pressure_from_entropy(float density, float entropy) {
+gas_pressure_from_entropy(const float density, const float entropy) {
 
   return entropy * pow_gamma(density);
 }
@@ -76,7 +76,7 @@ gas_pressure_from_entropy(float density, float entropy) {
  * @return The entropy \f$A\f$.
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_entropy_from_pressure(float density, float pressure) {
+gas_entropy_from_pressure(const float density, const float pressure) {
 
   return pressure * pow_minus_gamma(density);
 }
@@ -90,7 +90,7 @@ gas_entropy_from_pressure(float density, float pressure) {
  * @param entropy The entropy \f$A\f$.
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_soundspeed_from_entropy(float density, float entropy) {
+gas_soundspeed_from_entropy(const float density, const float entropy) {
 
   return sqrtf(hydro_gamma * pow_gamma_minus_one(density) * entropy);
 }
@@ -104,7 +104,7 @@ gas_soundspeed_from_entropy(float density, float entropy) {
  * @param u The internal energy \f$u\f$
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_entropy_from_internal_energy(float density, float u) {
+gas_entropy_from_internal_energy(const float density, const float u) {
 
   return hydro_gamma_minus_one * u * pow_minus_gamma_minus_one(density);
 }
@@ -118,7 +118,7 @@ gas_entropy_from_internal_energy(float density, float u) {
  * @param u The internal energy \f$u\f$
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_pressure_from_internal_energy(float density, float u) {
+gas_pressure_from_internal_energy(const float density, const float u) {
 
   return hydro_gamma_minus_one * u * density;
 }
@@ -133,7 +133,7 @@ gas_pressure_from_internal_energy(float density, float u) {
  * @return The internal energy \f$u\f$.
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_internal_energy_from_pressure(float density, float pressure) {
+gas_internal_energy_from_pressure(const float density, const float pressure) {
 
   return hydro_one_over_gamma_minus_one * pressure / density;
 }
@@ -147,7 +147,7 @@ gas_internal_energy_from_pressure(float density, float pressure) {
  * @param u The internal energy \f$u\f$
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_soundspeed_from_internal_energy(float density, float u) {
+gas_soundspeed_from_internal_energy(const float density, const float u) {
 
   return sqrtf(u * hydro_gamma * hydro_gamma_minus_one);
 }
@@ -161,7 +161,7 @@ gas_soundspeed_from_internal_energy(float density, float u) {
  * @param P The pressure \f$P\f$
  */
 __attribute__((always_inline, const)) INLINE static float
-gas_soundspeed_from_pressure(float density, float P) {
+gas_soundspeed_from_pressure(const float density, const float P) {
 
   return sqrtf(hydro_gamma * P / density);
 }
diff --git a/src/equation_of_state/isothermal/equation_of_state.h b/src/equation_of_state/isothermal/equation_of_state.h
index 3ee1e18ebb0b8dcc6be8e02d4ee640edadfc35d6..0ff1b1a5063146e9630c588683d57c0993edff60 100644
--- a/src/equation_of_state/isothermal/equation_of_state.h
+++ b/src/equation_of_state/isothermal/equation_of_state.h
@@ -50,7 +50,7 @@ struct eos_parameters {
  * @param entropy The entropy \f$S\f$.
  */
 __attribute__((always_inline)) INLINE static float
-gas_internal_energy_from_entropy(float density, float entropy) {
+gas_internal_energy_from_entropy(const float density, const float entropy) {
 
   return eos.isothermal_internal_energy;
 }
@@ -65,7 +65,7 @@ gas_internal_energy_from_entropy(float density, float entropy) {
  * @param entropy The entropy \f$S\f$.
  */
 __attribute__((always_inline)) INLINE static float gas_pressure_from_entropy(
-    float density, float entropy) {
+    const float density, const float entropy) {
 
   return hydro_gamma_minus_one * eos.isothermal_internal_energy * density;
 }
@@ -81,7 +81,7 @@ __attribute__((always_inline)) INLINE static float gas_pressure_from_entropy(
  * @return The entropy \f$A\f$.
  */
 __attribute__((always_inline)) INLINE static float gas_entropy_from_pressure(
-    float density, float pressure) {
+    const float density, const float pressure) {
 
   return hydro_gamma_minus_one * eos.isothermal_internal_energy *
          pow_minus_gamma_minus_one(density);
@@ -98,7 +98,7 @@ __attribute__((always_inline)) INLINE static float gas_entropy_from_pressure(
  * @param entropy The entropy \f$S\f$.
  */
 __attribute__((always_inline)) INLINE static float gas_soundspeed_from_entropy(
-    float density, float entropy) {
+    const float density, const float entropy) {
 
   return sqrtf(eos.isothermal_internal_energy * hydro_gamma *
                hydro_gamma_minus_one);
@@ -114,7 +114,7 @@ __attribute__((always_inline)) INLINE static float gas_soundspeed_from_entropy(
  * @param u The internal energy \f$u\f$
  */
 __attribute__((always_inline)) INLINE static float
-gas_entropy_from_internal_energy(float density, float u) {
+gas_entropy_from_internal_energy(const float density, const float u) {
 
   return hydro_gamma_minus_one * eos.isothermal_internal_energy *
          pow_minus_gamma_minus_one(density);
@@ -130,7 +130,7 @@ gas_entropy_from_internal_energy(float density, float u) {
  * @param u The internal energy \f$u\f$
  */
 __attribute__((always_inline)) INLINE static float
-gas_pressure_from_internal_energy(float density, float u) {
+gas_pressure_from_internal_energy(const float density, const float u) {
 
   return hydro_gamma_minus_one * eos.isothermal_internal_energy * density;
 }
@@ -145,7 +145,7 @@ gas_pressure_from_internal_energy(float density, float u) {
  * @return The internal energy \f$u\f$ (which is constant).
  */
 __attribute__((always_inline)) INLINE static float
-gas_internal_energy_from_pressure(float density, float pressure) {
+gas_internal_energy_from_pressure(const float density, const float pressure) {
   return eos.isothermal_internal_energy;
 }
 
@@ -160,7 +160,7 @@ gas_internal_energy_from_pressure(float density, float pressure) {
  * @param u The internal energy \f$u\f$
  */
 __attribute__((always_inline)) INLINE static float
-gas_soundspeed_from_internal_energy(float density, float u) {
+gas_soundspeed_from_internal_energy(const float density, const float u) {
 
   return sqrtf(eos.isothermal_internal_energy * hydro_gamma *
                hydro_gamma_minus_one);
@@ -177,7 +177,7 @@ gas_soundspeed_from_internal_energy(float density, float u) {
  * @param P The pressure \f$P\f$
  */
 __attribute__((always_inline)) INLINE static float gas_soundspeed_from_pressure(
-    float density, float P) {
+    const float density, const float P) {
 
   return sqrtf(eos.isothermal_internal_energy * hydro_gamma *
                hydro_gamma_minus_one);
diff --git a/src/equation_of_state/planetary/equation_of_state.h b/src/equation_of_state/planetary/equation_of_state.h
index 699ff93c42359dc1f1793cfc48fdbb1c7746b69e..4a46cadb80cb76fd72aba393026914b94252dbb5 100755
--- a/src/equation_of_state/planetary/equation_of_state.h
+++ b/src/equation_of_state/planetary/equation_of_state.h
@@ -102,6 +102,10 @@ enum eos_planetary_material_id {
   eos_planetary_id_Til_basalt =
       eos_planetary_type_Til * eos_planetary_type_factor + 3,
 
+  /*! Tillotson ice */
+  eos_planetary_id_Til_ice =
+      eos_planetary_type_Til * eos_planetary_type_factor + 4,
+
   /* Hubbard & MacFarlane (1980) Uranus/Neptune */
 
   /*! Hydrogen-helium atmosphere */
@@ -134,6 +138,23 @@ enum eos_planetary_material_id {
   eos_planetary_id_SS08_water =
       eos_planetary_type_SESAME * eos_planetary_type_factor + 3,
 
+  /*! AQUA (Haldemann et al. 2020) SESAME-like water */
+  eos_planetary_id_AQUA =
+      eos_planetary_type_SESAME * eos_planetary_type_factor + 4,
+
+  /*! CMS19 hydrogen (Chabrier et al. 2019) SESAME-like hydrogen */
+  eos_planetary_id_CMS19_H =
+      eos_planetary_type_SESAME * eos_planetary_type_factor + 5,
+
+  /*! CMS19 helium (Chabrier et al. 2019) SESAME-like helium */
+  eos_planetary_id_CMS19_He =
+      eos_planetary_type_SESAME * eos_planetary_type_factor + 6,
+
+  /*! CD21 hydrogen-helium (Chabrier & Debras 2021) SESAME-like H-He mixture
+     (Y=0.245) */
+  eos_planetary_id_CD21_HHe =
+      eos_planetary_type_SESAME * eos_planetary_type_factor + 7,
+
   /* ANEOS */
 
   /*! ANEOS forsterite (Stewart et al. 2019) -- in SESAME-style tables */
@@ -149,6 +170,10 @@ enum eos_planetary_material_id {
       eos_planetary_type_ANEOS * eos_planetary_type_factor + 2,
 };
 
+/* Base material ID for custom Tillotson EoS */
+#define eos_planetary_Til_custom_base_id \
+  (eos_planetary_type_Til * eos_planetary_type_factor + 90)
+
 /* Individual EOS function headers. */
 #include "hm80.h"
 #include "ideal_gas.h"
@@ -160,9 +185,11 @@ enum eos_planetary_material_id {
  */
 struct eos_parameters {
   struct idg_params idg_def;
-  struct Til_params Til_iron, Til_granite, Til_water, Til_basalt;
+  struct Til_params Til_iron, Til_granite, Til_water, Til_basalt, Til_ice,
+      Til_custom[10];
   struct HM80_params HM80_HHe, HM80_ice, HM80_rock;
-  struct SESAME_params SESAME_iron, SESAME_basalt, SESAME_water, SS08_water;
+  struct SESAME_params SESAME_iron, SESAME_basalt, SESAME_water, SS08_water,
+      AQUA, CMS19_H, CMS19_He, CD21_HHe;
   struct SESAME_params ANEOS_forsterite, ANEOS_iron, ANEOS_Fe85Si15;
   struct SESAME_params custom[10];
 };
@@ -223,8 +250,20 @@ gas_internal_energy_from_entropy(float density, float entropy,
                                                   &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_internal_energy_from_entropy(density, entropy,
+                                                  &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_internal_energy_from_entropy(density, entropy,
+                                                    &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -278,6 +317,42 @@ gas_internal_energy_from_entropy(float density, float entropy,
                                                      &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.AQUA.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_AQUA: 1");
+#endif
+          return SESAME_internal_energy_from_entropy(density, entropy,
+                                                     &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.CMS19_H.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_CMS19_H: 1");
+#endif
+          return SESAME_internal_energy_from_entropy(density, entropy,
+                                                     &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.CMS19_He.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_CMS19_He: 1");
+#endif
+          return SESAME_internal_energy_from_entropy(density, entropy,
+                                                     &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.CD21_HHe.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_CD21_HHe: 1");
+#endif
+          return SESAME_internal_energy_from_entropy(density, entropy,
+                                                     &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -372,8 +447,19 @@ __attribute__((always_inline)) INLINE static float gas_pressure_from_entropy(
           return Til_pressure_from_entropy(density, entropy, &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_pressure_from_entropy(density, entropy, &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_pressure_from_entropy(density, entropy,
+                                             &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -424,6 +510,22 @@ __attribute__((always_inline)) INLINE static float gas_pressure_from_entropy(
                                               &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+          return SESAME_pressure_from_entropy(density, entropy, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_pressure_from_entropy(density, entropy, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_pressure_from_entropy(density, entropy, &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_pressure_from_entropy(density, entropy, &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -519,8 +621,19 @@ __attribute__((always_inline)) INLINE static float gas_entropy_from_pressure(
           return Til_entropy_from_pressure(density, P, &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_entropy_from_pressure(density, P, &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_entropy_from_pressure(density, P,
+                                             &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -567,6 +680,22 @@ __attribute__((always_inline)) INLINE static float gas_entropy_from_pressure(
           return SESAME_entropy_from_pressure(density, P, &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+          return SESAME_entropy_from_pressure(density, P, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_entropy_from_pressure(density, P, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_entropy_from_pressure(density, P, &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_entropy_from_pressure(density, P, &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -659,8 +788,19 @@ __attribute__((always_inline)) INLINE static float gas_soundspeed_from_entropy(
           return Til_soundspeed_from_entropy(density, entropy, &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_soundspeed_from_entropy(density, entropy, &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_soundspeed_from_entropy(density, entropy,
+                                               &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -711,6 +851,24 @@ __attribute__((always_inline)) INLINE static float gas_soundspeed_from_entropy(
                                                 &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+          return SESAME_soundspeed_from_entropy(density, entropy, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_soundspeed_from_entropy(density, entropy, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_soundspeed_from_entropy(density, entropy,
+                                                &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_soundspeed_from_entropy(density, entropy,
+                                                &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -805,8 +963,19 @@ gas_entropy_from_internal_energy(float density, float u,
           return Til_entropy_from_internal_energy(density, u, &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_entropy_from_internal_energy(density, u, &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_entropy_from_internal_energy(density, u,
+                                                    &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -857,6 +1026,22 @@ gas_entropy_from_internal_energy(float density, float u,
                                                      &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+          return SESAME_entropy_from_internal_energy(density, u, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_entropy_from_internal_energy(density, u, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_entropy_from_internal_energy(density, u, &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_entropy_from_internal_energy(density, u, &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -978,11 +1163,26 @@ gas_pressure_from_internal_energy(float density, float u,
           return Til_pressure_from_internal_energy(density, u, &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.Til_ice.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_Til_ice: 1");
+#endif
+          return Til_pressure_from_internal_energy(density, u, &eos.Til_ice);
+          break;
+
         default:
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_pressure_from_internal_energy(density, u,
+                                                     &eos.Til_custom[i_custom]);
+          } else {
 #ifdef SWIFT_DEBUG_CHECKS
-          error("Unknown material ID! mat_id = %d", mat_id);
+            error("Unknown material ID! mat_id = %d", mat_id);
 #endif
-          return -1.f;
+            return -1.f;
+          }
       };
       break;
 
@@ -1070,6 +1270,40 @@ gas_pressure_from_internal_energy(float density, float u,
                                                       &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.AQUA.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_AQUA: 1");
+#endif
+          return SESAME_pressure_from_internal_energy(density, u, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.CMS19_H.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_CMS19_H: 1");
+#endif
+          return SESAME_pressure_from_internal_energy(density, u, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.CMS19_He.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_CMS19_He: 1");
+#endif
+          return SESAME_pressure_from_internal_energy(density, u,
+                                                      &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+#ifdef SWIFT_DEBUG_CHECKS
+          if (eos.CD21_HHe.mat_id != mat_id)
+            error("EoS not enabled. Please set EoS:planetary_use_CD21_HHe: 1");
+#endif
+          return SESAME_pressure_from_internal_energy(density, u,
+                                                      &eos.CD21_HHe);
+          break;
+
         default:
 #ifdef SWIFT_DEBUG_CHECKS
           error("Unknown material ID! mat_id = %d", mat_id);
@@ -1200,8 +1434,19 @@ gas_internal_energy_from_pressure(float density, float P,
           return Til_internal_energy_from_pressure(density, P, &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_internal_energy_from_pressure(density, P, &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_internal_energy_from_pressure(density, P,
+                                                     &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -1252,6 +1497,24 @@ gas_internal_energy_from_pressure(float density, float P,
                                                       &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+          return SESAME_internal_energy_from_pressure(density, P, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_internal_energy_from_pressure(density, P, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_internal_energy_from_pressure(density, P,
+                                                      &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_internal_energy_from_pressure(density, P,
+                                                      &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -1350,8 +1613,19 @@ gas_soundspeed_from_internal_energy(float density, float u,
                                                      &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_soundspeed_from_internal_energy(density, u, &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_soundspeed_from_internal_energy(
+                density, u, &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -1405,6 +1679,25 @@ gas_soundspeed_from_internal_energy(float density, float u,
                                                         &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+          return SESAME_soundspeed_from_internal_energy(density, u, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_soundspeed_from_internal_energy(density, u,
+                                                        &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_soundspeed_from_internal_energy(density, u,
+                                                        &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_soundspeed_from_internal_energy(density, u,
+                                                        &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -1499,8 +1792,19 @@ __attribute__((always_inline)) INLINE static float gas_soundspeed_from_pressure(
           return Til_soundspeed_from_pressure(density, P, &eos.Til_basalt);
           break;
 
+        case eos_planetary_id_Til_ice:
+          return Til_soundspeed_from_pressure(density, P, &eos.Til_ice);
+          break;
+
         default:
-          return -1.f;
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_soundspeed_from_pressure(density, P,
+                                                &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
       };
       break;
 
@@ -1548,6 +1852,22 @@ __attribute__((always_inline)) INLINE static float gas_soundspeed_from_pressure(
           return SESAME_soundspeed_from_pressure(density, P, &eos.SS08_water);
           break;
 
+        case eos_planetary_id_AQUA:
+          return SESAME_soundspeed_from_pressure(density, P, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_soundspeed_from_pressure(density, P, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_soundspeed_from_pressure(density, P, &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_soundspeed_from_pressure(density, P, &eos.CD21_HHe);
+          break;
+
         default:
           return -1.f;
       };
@@ -1591,106 +1911,740 @@ __attribute__((always_inline)) INLINE static float gas_soundspeed_from_pressure(
 }
 
 /**
- * @brief Initialize the eos parameters
+ * @brief Returns the temperature given density and internal energy
  *
- * @param e The #eos_parameters
- * @param params The parsed parameters
+ * @param density The density \f$\rho\f$
+ * @param u The internal energy \f$u\f$
  */
-__attribute__((always_inline)) INLINE static void eos_init(
-    struct eos_parameters *e, const struct phys_const *phys_const,
-    const struct unit_system *us, struct swift_params *params) {
+__attribute__((always_inline)) INLINE static float
+gas_temperature_from_internal_energy(float density, float u,
+                                     enum eos_planetary_material_id mat_id) {
+  const enum eos_planetary_type_id type =
+      (enum eos_planetary_type_id)(mat_id / eos_planetary_type_factor);
 
-  // Prepare any/all requested EoS: Set the parameters and material IDs, load
-  // tables etc., and convert to internal units
+  /* Select the material base type */
+  switch (type) {
 
-  // Ideal gas
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_idg_def", 0)) {
-    set_idg_def(&e->idg_def, eos_planetary_id_idg_def);
-  }
+    /* Ideal gas EoS */
+    case eos_planetary_type_idg:
 
-  // Tillotson
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_iron", 0)) {
-    set_Til_iron(&e->Til_iron, eos_planetary_id_Til_iron);
-    convert_units_Til(&e->Til_iron, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_granite", 0)) {
-    set_Til_granite(&e->Til_granite, eos_planetary_id_Til_granite);
-    convert_units_Til(&e->Til_granite, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_water", 0)) {
-    set_Til_water(&e->Til_water, eos_planetary_id_Til_water);
-    convert_units_Til(&e->Til_water, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_basalt", 0)) {
-    set_Til_basalt(&e->Til_basalt, eos_planetary_id_Til_basalt);
-    convert_units_Til(&e->Til_basalt, us);
-  }
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_idg_def:
+          return idg_temperature_from_internal_energy(density, u, &eos.idg_def);
+          break;
 
-  // Hubbard & MacFarlane (1980)
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_HM80_HHe", 0)) {
-    char HM80_HHe_table_file[PARSER_MAX_LINE_SIZE];
-    set_HM80_HHe(&e->HM80_HHe, eos_planetary_id_HM80_HHe);
-    parser_get_param_string(params, "EoS:planetary_HM80_HHe_table_file",
-                            HM80_HHe_table_file);
-    load_table_HM80(&e->HM80_HHe, HM80_HHe_table_file);
-    prepare_table_HM80(&e->HM80_HHe);
-    convert_units_HM80(&e->HM80_HHe, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_HM80_ice", 0)) {
-    char HM80_ice_table_file[PARSER_MAX_LINE_SIZE];
-    set_HM80_ice(&e->HM80_ice, eos_planetary_id_HM80_ice);
-    parser_get_param_string(params, "EoS:planetary_HM80_ice_table_file",
-                            HM80_ice_table_file);
-    load_table_HM80(&e->HM80_ice, HM80_ice_table_file);
-    prepare_table_HM80(&e->HM80_ice);
-    convert_units_HM80(&e->HM80_ice, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_HM80_rock", 0)) {
-    char HM80_rock_table_file[PARSER_MAX_LINE_SIZE];
-    set_HM80_rock(&e->HM80_rock, eos_planetary_id_HM80_rock);
-    parser_get_param_string(params, "EoS:planetary_HM80_rock_table_file",
-                            HM80_rock_table_file);
-    load_table_HM80(&e->HM80_rock, HM80_rock_table_file);
-    prepare_table_HM80(&e->HM80_rock);
-    convert_units_HM80(&e->HM80_rock, us);
-  }
+        default:
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Unknown material ID! mat_id = %d", mat_id);
+#endif
+          return -1.f;
+      };
+      break;
 
-  // SESAME
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_SESAME_iron", 0)) {
-    char SESAME_iron_table_file[PARSER_MAX_LINE_SIZE];
-    set_SESAME_iron(&e->SESAME_iron, eos_planetary_id_SESAME_iron);
-    parser_get_param_string(params, "EoS:planetary_SESAME_iron_table_file",
-                            SESAME_iron_table_file);
-    load_table_SESAME(&e->SESAME_iron, SESAME_iron_table_file);
-    prepare_table_SESAME(&e->SESAME_iron);
-    convert_units_SESAME(&e->SESAME_iron, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_SESAME_basalt", 0)) {
-    char SESAME_basalt_table_file[PARSER_MAX_LINE_SIZE];
-    set_SESAME_basalt(&e->SESAME_basalt, eos_planetary_id_SESAME_basalt);
-    parser_get_param_string(params, "EoS:planetary_SESAME_basalt_table_file",
-                            SESAME_basalt_table_file);
-    load_table_SESAME(&e->SESAME_basalt, SESAME_basalt_table_file);
-    prepare_table_SESAME(&e->SESAME_basalt);
-    convert_units_SESAME(&e->SESAME_basalt, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_SESAME_water", 0)) {
-    char SESAME_water_table_file[PARSER_MAX_LINE_SIZE];
-    set_SESAME_water(&e->SESAME_water, eos_planetary_id_SESAME_water);
-    parser_get_param_string(params, "EoS:planetary_SESAME_water_table_file",
-                            SESAME_water_table_file);
-    load_table_SESAME(&e->SESAME_water, SESAME_water_table_file);
-    prepare_table_SESAME(&e->SESAME_water);
-    convert_units_SESAME(&e->SESAME_water, us);
-  }
-  if (parser_get_opt_param_int(params, "EoS:planetary_use_SS08_water", 0)) {
-    char SS08_water_table_file[PARSER_MAX_LINE_SIZE];
-    set_SS08_water(&e->SESAME_water, eos_planetary_id_SS08_water);
-    parser_get_param_string(params, "EoS:planetary_SS08_water_table_file",
-                            SS08_water_table_file);
-    load_table_SESAME(&e->SS08_water, SS08_water_table_file);
-    prepare_table_SESAME(&e->SS08_water);
-    convert_units_SESAME(&e->SS08_water, us);
+    /* Tillotson EoS */
+    case eos_planetary_type_Til:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_Til_iron:
+          return Til_temperature_from_internal_energy(density, u,
+                                                      &eos.Til_iron);
+          break;
+
+        case eos_planetary_id_Til_granite:
+          return Til_temperature_from_internal_energy(density, u,
+                                                      &eos.Til_granite);
+          break;
+
+        case eos_planetary_id_Til_water:
+          return Til_temperature_from_internal_energy(density, u,
+                                                      &eos.Til_water);
+          break;
+
+        case eos_planetary_id_Til_basalt:
+          return Til_temperature_from_internal_energy(density, u,
+                                                      &eos.Til_basalt);
+          break;
+
+        case eos_planetary_id_Til_ice:
+          return Til_temperature_from_internal_energy(density, u, &eos.Til_ice);
+          break;
+
+        default:
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_temperature_from_internal_energy(
+                density, u, &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
+      };
+      break;
+
+    /* Hubbard & MacFarlane (1980) EoS */
+    case eos_planetary_type_HM80:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_HM80_HHe:
+          return HM80_temperature_from_internal_energy(density, u,
+                                                       &eos.HM80_HHe);
+          break;
+
+        case eos_planetary_id_HM80_ice:
+          return HM80_temperature_from_internal_energy(density, u,
+                                                       &eos.HM80_ice);
+          break;
+
+        case eos_planetary_id_HM80_rock:
+          return HM80_temperature_from_internal_energy(density, u,
+                                                       &eos.HM80_rock);
+          break;
+
+        default:
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Unknown material ID! mat_id = %d", mat_id);
+#endif
+          return -1.f;
+      };
+      break;
+
+    /* SESAME EoS */
+    case eos_planetary_type_SESAME:;
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_SESAME_iron:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.SESAME_iron);
+          break;
+
+        case eos_planetary_id_SESAME_basalt:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.SESAME_basalt);
+          break;
+
+        case eos_planetary_id_SESAME_water:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.SESAME_water);
+          break;
+
+        case eos_planetary_id_SS08_water:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.SS08_water);
+          break;
+
+        case eos_planetary_id_AQUA:
+          return SESAME_temperature_from_internal_energy(density, u, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.CD21_HHe);
+          break;
+
+        default:
+          return -1.f;
+      };
+      break;
+
+    /* ANEOS -- using SESAME-style tables */
+    case eos_planetary_type_ANEOS:;
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_ANEOS_forsterite:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.ANEOS_forsterite);
+          break;
+
+        case eos_planetary_id_ANEOS_iron:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.ANEOS_iron);
+          break;
+
+        case eos_planetary_id_ANEOS_Fe85Si15:
+          return SESAME_temperature_from_internal_energy(density, u,
+                                                         &eos.ANEOS_Fe85Si15);
+          break;
+
+        default:
+          return -1.f;
+      };
+      break;
+
+    /*! Generic user-provided custom tables */
+    case eos_planetary_type_custom: {
+      const int i_custom =
+          mat_id - eos_planetary_type_custom * eos_planetary_type_factor;
+      return SESAME_temperature_from_internal_energy(density, u,
+                                                     &eos.custom[i_custom]);
+      break;
+    }
+
+    default:
+      return -1.f;
+  }
+}
+
+/**
+ * @brief Returns the density given pressure and temperature
+ *
+ * @param P The pressure \f$P\f$
+ * @param T The temperature \f$T\f$
+ */
+__attribute__((always_inline)) INLINE static float
+gas_density_from_pressure_and_temperature(
+    float P, float T, enum eos_planetary_material_id mat_id) {
+  const enum eos_planetary_type_id type =
+      (enum eos_planetary_type_id)(mat_id / eos_planetary_type_factor);
+
+  /* Select the material base type */
+  switch (type) {
+
+    /* Ideal gas EoS */
+    case eos_planetary_type_idg:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_idg_def:
+          return idg_density_from_pressure_and_temperature(P, T, &eos.idg_def);
+          break;
+
+        default:
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Unknown material ID! mat_id = %d", mat_id);
+#endif
+          return -1.f;
+      };
+      break;
+
+    /* Tillotson EoS */
+    case eos_planetary_type_Til:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_Til_iron:
+          return Til_density_from_pressure_and_temperature(P, T, &eos.Til_iron);
+          break;
+
+        case eos_planetary_id_Til_granite:
+          return Til_density_from_pressure_and_temperature(P, T,
+                                                           &eos.Til_granite);
+          break;
+
+        case eos_planetary_id_Til_water:
+          return Til_density_from_pressure_and_temperature(P, T,
+                                                           &eos.Til_water);
+          break;
+
+        case eos_planetary_id_Til_basalt:
+          return Til_density_from_pressure_and_temperature(P, T,
+                                                           &eos.Til_basalt);
+          break;
+
+        case eos_planetary_id_Til_ice:
+          return Til_density_from_pressure_and_temperature(P, T, &eos.Til_ice);
+          break;
+
+        default:
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_density_from_pressure_and_temperature(
+                P, T, &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
+      };
+      break;
+
+    /* Hubbard & MacFarlane (1980) EoS */
+    case eos_planetary_type_HM80:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_HM80_HHe:
+          return HM80_density_from_pressure_and_temperature(P, T,
+                                                            &eos.HM80_HHe);
+          break;
+
+        case eos_planetary_id_HM80_ice:
+          return HM80_density_from_pressure_and_temperature(P, T,
+                                                            &eos.HM80_ice);
+          break;
+
+        case eos_planetary_id_HM80_rock:
+          return HM80_density_from_pressure_and_temperature(P, T,
+                                                            &eos.HM80_rock);
+          break;
+
+        default:
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Unknown material ID! mat_id = %d", mat_id);
+#endif
+          return -1.f;
+      };
+      break;
+
+    /* SESAME EoS */
+    case eos_planetary_type_SESAME:;
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_SESAME_iron:
+          return SESAME_density_from_pressure_and_temperature(P, T,
+                                                              &eos.SESAME_iron);
+          break;
+
+        case eos_planetary_id_SESAME_basalt:
+          return SESAME_density_from_pressure_and_temperature(
+              P, T, &eos.SESAME_basalt);
+          break;
+
+        case eos_planetary_id_SESAME_water:
+          return SESAME_density_from_pressure_and_temperature(
+              P, T, &eos.SESAME_water);
+          break;
+
+        case eos_planetary_id_SS08_water:
+          return SESAME_density_from_pressure_and_temperature(P, T,
+                                                              &eos.SS08_water);
+          break;
+
+        case eos_planetary_id_AQUA:
+          return SESAME_density_from_pressure_and_temperature(P, T, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_density_from_pressure_and_temperature(P, T,
+                                                              &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_density_from_pressure_and_temperature(P, T,
+                                                              &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_density_from_pressure_and_temperature(P, T,
+                                                              &eos.CD21_HHe);
+          break;
+
+        default:
+          return -1.f;
+      };
+      break;
+
+    /* ANEOS -- using SESAME-style tables */
+    case eos_planetary_type_ANEOS:;
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_ANEOS_forsterite:
+          return SESAME_density_from_pressure_and_temperature(
+              P, T, &eos.ANEOS_forsterite);
+          break;
+
+        case eos_planetary_id_ANEOS_iron:
+          return SESAME_density_from_pressure_and_temperature(P, T,
+                                                              &eos.ANEOS_iron);
+          break;
+
+        case eos_planetary_id_ANEOS_Fe85Si15:
+          return SESAME_density_from_pressure_and_temperature(
+              P, T, &eos.ANEOS_Fe85Si15);
+          break;
+
+        default:
+          return -1.f;
+      };
+      break;
+
+    /*! Generic user-provided custom tables */
+    case eos_planetary_type_custom: {
+      const int i_custom =
+          mat_id - eos_planetary_type_custom * eos_planetary_type_factor;
+      return SESAME_density_from_pressure_and_temperature(
+          P, T, &eos.custom[i_custom]);
+      break;
+    }
+
+    default:
+      return -1.f;
+  }
+}
+
+/**
+ * @brief Returns the density given pressure and internal energy
+ *
+ * @param P The pressure \f$P\f$
+ * @param T The temperature \f$T\f$
+ */
+__attribute__((always_inline)) INLINE static float
+gas_density_from_pressure_and_internal_energy(
+    float P, float u, float rho_ref, float rho_sph,
+    enum eos_planetary_material_id mat_id) {
+  const enum eos_planetary_type_id type =
+      (enum eos_planetary_type_id)(mat_id / eos_planetary_type_factor);
+
+  /* Select the material base type */
+  switch (type) {
+
+    /* Ideal gas EoS */
+    case eos_planetary_type_idg:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_idg_def:
+          return idg_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.idg_def);
+          break;
+
+        default:
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Unknown material ID! mat_id = %d", mat_id);
+#endif
+          return -1.f;
+      };
+      break;
+
+    /* Tillotson EoS */
+    case eos_planetary_type_Til:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_Til_iron:
+          return Til_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.Til_iron);
+          break;
+
+        case eos_planetary_id_Til_granite:
+          return Til_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.Til_granite);
+          break;
+
+        case eos_planetary_id_Til_water:
+          return Til_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.Til_water);
+          break;
+
+        case eos_planetary_id_Til_basalt:
+          return Til_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.Til_basalt);
+          break;
+
+        case eos_planetary_id_Til_ice:
+          return Til_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.Til_ice);
+          break;
+
+        default:
+          // Custom user-provided Tillotson
+          if (mat_id >= eos_planetary_Til_custom_base_id) {
+            const int i_custom = mat_id - eos_planetary_Til_custom_base_id;
+            return Til_density_from_pressure_and_internal_energy(
+                P, u, rho_ref, rho_sph, &eos.Til_custom[i_custom]);
+          } else {
+            return -1.f;
+          }
+      };
+      break;
+
+    /* Hubbard & MacFarlane (1980) EoS */
+    case eos_planetary_type_HM80:
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_HM80_HHe:
+          return HM80_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.HM80_HHe);
+          break;
+
+        case eos_planetary_id_HM80_ice:
+          return HM80_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.HM80_ice);
+          break;
+
+        case eos_planetary_id_HM80_rock:
+          return HM80_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.HM80_rock);
+          break;
+
+        default:
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Unknown material ID! mat_id = %d", mat_id);
+#endif
+          return -1.f;
+      };
+      break;
+
+    /* SESAME EoS */
+    case eos_planetary_type_SESAME:;
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_SESAME_iron:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.SESAME_iron);
+          break;
+
+        case eos_planetary_id_SESAME_basalt:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.SESAME_basalt);
+          break;
+
+        case eos_planetary_id_SESAME_water:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.SESAME_water);
+          break;
+
+        case eos_planetary_id_SS08_water:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.SS08_water);
+          break;
+
+        case eos_planetary_id_AQUA:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.AQUA);
+          break;
+
+        case eos_planetary_id_CMS19_H:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.CMS19_H);
+          break;
+
+        case eos_planetary_id_CMS19_He:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.CMS19_He);
+          break;
+
+        case eos_planetary_id_CD21_HHe:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.CD21_HHe);
+          break;
+
+        default:
+          return -1.f;
+      };
+      break;
+
+    /* ANEOS -- using SESAME-style tables */
+    case eos_planetary_type_ANEOS:;
+
+      /* Select the material of this type */
+      switch (mat_id) {
+        case eos_planetary_id_ANEOS_forsterite:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.ANEOS_forsterite);
+          break;
+
+        case eos_planetary_id_ANEOS_iron:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.ANEOS_iron);
+          break;
+
+        case eos_planetary_id_ANEOS_Fe85Si15:
+          return SESAME_density_from_pressure_and_internal_energy(
+              P, u, rho_ref, rho_sph, &eos.ANEOS_Fe85Si15);
+          break;
+
+        default:
+          return -1.f;
+      };
+      break;
+
+    /*! Generic user-provided custom tables */
+    case eos_planetary_type_custom: {
+      const int i_custom =
+          mat_id - eos_planetary_type_custom * eos_planetary_type_factor;
+      return SESAME_density_from_pressure_and_internal_energy(
+          P, u, rho_ref, rho_sph, &eos.custom[i_custom]);
+      break;
+    }
+
+    default:
+      return -1.f;
+  }
+}
+
+/**
+ * @brief Initialize the eos parameters
+ *
+ * @param e The #eos_parameters
+ * @param params The parsed parameters
+ */
+__attribute__((always_inline)) INLINE static void eos_init(
+    struct eos_parameters *e, const struct phys_const *phys_const,
+    const struct unit_system *us, struct swift_params *params) {
+
+  // Prepare any/all requested EoS: Set the parameters and material IDs, load
+  // tables etc., and convert to internal units
+
+  // Ideal gas
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_idg_def", 0)) {
+    set_idg_def(&e->idg_def, eos_planetary_id_idg_def);
+  }
+
+  // Tillotson
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_iron", 0)) {
+    set_Til_iron(&e->Til_iron, eos_planetary_id_Til_iron);
+    set_Til_u_cold(&e->Til_iron, eos_planetary_id_Til_iron);
+    convert_units_Til(&e->Til_iron, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_granite", 0)) {
+    set_Til_granite(&e->Til_granite, eos_planetary_id_Til_granite);
+    set_Til_u_cold(&e->Til_granite, eos_planetary_id_Til_granite);
+    convert_units_Til(&e->Til_granite, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_water", 0)) {
+    set_Til_water(&e->Til_water, eos_planetary_id_Til_water);
+    set_Til_u_cold(&e->Til_water, eos_planetary_id_Til_water);
+    convert_units_Til(&e->Til_water, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_basalt", 0)) {
+    set_Til_basalt(&e->Til_basalt, eos_planetary_id_Til_basalt);
+    set_Til_u_cold(&e->Til_basalt, eos_planetary_id_Til_basalt);
+    convert_units_Til(&e->Til_basalt, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_Til_ice", 0)) {
+    set_Til_ice(&e->Til_ice, eos_planetary_id_Til_ice);
+    set_Til_u_cold(&e->Til_ice, eos_planetary_id_Til_ice);
+    convert_units_Til(&e->Til_ice, us);
+  }
+
+  // Custom user-provided Tillotson
+  for (int i_custom = 0; i_custom <= 9; i_custom++) {
+    char param_name[PARSER_MAX_LINE_SIZE];
+    sprintf(param_name, "EoS:planetary_use_Til_custom_%d", i_custom);
+    if (parser_get_opt_param_int(params, param_name, 0)) {
+      char Til_custom_file[PARSER_MAX_LINE_SIZE];
+      int mat_id = eos_planetary_Til_custom_base_id + i_custom;
+
+      sprintf(param_name, "EoS:planetary_Til_custom_%d_param_file", i_custom);
+      parser_get_param_string(params, param_name, Til_custom_file);
+
+      set_Til_custom(&e->Til_custom[i_custom],
+                     (enum eos_planetary_material_id)mat_id, Til_custom_file);
+      set_Til_u_cold(&e->Til_custom[i_custom],
+                     (enum eos_planetary_material_id)mat_id);
+      convert_units_Til(&e->Til_custom[i_custom], us);
+    }
+  }
+
+  // Hubbard & MacFarlane (1980)
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_HM80_HHe", 0)) {
+    char HM80_HHe_table_file[PARSER_MAX_LINE_SIZE];
+    set_HM80_HHe(&e->HM80_HHe, eos_planetary_id_HM80_HHe);
+    parser_get_param_string(params, "EoS:planetary_HM80_HHe_table_file",
+                            HM80_HHe_table_file);
+    load_table_HM80(&e->HM80_HHe, HM80_HHe_table_file);
+    prepare_table_HM80(&e->HM80_HHe);
+    convert_units_HM80(&e->HM80_HHe, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_HM80_ice", 0)) {
+    char HM80_ice_table_file[PARSER_MAX_LINE_SIZE];
+    set_HM80_ice(&e->HM80_ice, eos_planetary_id_HM80_ice);
+    parser_get_param_string(params, "EoS:planetary_HM80_ice_table_file",
+                            HM80_ice_table_file);
+    load_table_HM80(&e->HM80_ice, HM80_ice_table_file);
+    prepare_table_HM80(&e->HM80_ice);
+    convert_units_HM80(&e->HM80_ice, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_HM80_rock", 0)) {
+    char HM80_rock_table_file[PARSER_MAX_LINE_SIZE];
+    set_HM80_rock(&e->HM80_rock, eos_planetary_id_HM80_rock);
+    parser_get_param_string(params, "EoS:planetary_HM80_rock_table_file",
+                            HM80_rock_table_file);
+    load_table_HM80(&e->HM80_rock, HM80_rock_table_file);
+    prepare_table_HM80(&e->HM80_rock);
+    convert_units_HM80(&e->HM80_rock, us);
+  }
+
+  // SESAME
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_SESAME_iron", 0)) {
+    char SESAME_iron_table_file[PARSER_MAX_LINE_SIZE];
+    set_SESAME_iron(&e->SESAME_iron, eos_planetary_id_SESAME_iron);
+    parser_get_param_string(params, "EoS:planetary_SESAME_iron_table_file",
+                            SESAME_iron_table_file);
+    load_table_SESAME(&e->SESAME_iron, SESAME_iron_table_file);
+    prepare_table_SESAME(&e->SESAME_iron);
+    convert_units_SESAME(&e->SESAME_iron, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_SESAME_basalt", 0)) {
+    char SESAME_basalt_table_file[PARSER_MAX_LINE_SIZE];
+    set_SESAME_basalt(&e->SESAME_basalt, eos_planetary_id_SESAME_basalt);
+    parser_get_param_string(params, "EoS:planetary_SESAME_basalt_table_file",
+                            SESAME_basalt_table_file);
+    load_table_SESAME(&e->SESAME_basalt, SESAME_basalt_table_file);
+    prepare_table_SESAME(&e->SESAME_basalt);
+    convert_units_SESAME(&e->SESAME_basalt, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_SESAME_water", 0)) {
+    char SESAME_water_table_file[PARSER_MAX_LINE_SIZE];
+    set_SESAME_water(&e->SESAME_water, eos_planetary_id_SESAME_water);
+    parser_get_param_string(params, "EoS:planetary_SESAME_water_table_file",
+                            SESAME_water_table_file);
+    load_table_SESAME(&e->SESAME_water, SESAME_water_table_file);
+    prepare_table_SESAME(&e->SESAME_water);
+    convert_units_SESAME(&e->SESAME_water, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_SS08_water", 0)) {
+    char SS08_water_table_file[PARSER_MAX_LINE_SIZE];
+    set_SS08_water(&e->SS08_water, eos_planetary_id_SS08_water);
+    parser_get_param_string(params, "EoS:planetary_SS08_water_table_file",
+                            SS08_water_table_file);
+    load_table_SESAME(&e->SS08_water, SS08_water_table_file);
+    prepare_table_SESAME(&e->SS08_water);
+    convert_units_SESAME(&e->SS08_water, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_AQUA", 0)) {
+    char AQUA_table_file[PARSER_MAX_LINE_SIZE];
+    set_AQUA(&e->AQUA, eos_planetary_id_AQUA);
+    parser_get_param_string(params, "EoS:planetary_AQUA_table_file",
+                            AQUA_table_file);
+    load_table_SESAME(&e->AQUA, AQUA_table_file);
+    prepare_table_SESAME(&e->AQUA);
+    convert_units_SESAME(&e->AQUA, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_CMS19_H", 0)) {
+    char CMS19_H_table_file[PARSER_MAX_LINE_SIZE];
+    set_CMS19_H(&e->CMS19_H, eos_planetary_id_CMS19_H);
+    parser_get_param_string(params, "EoS:planetary_CMS19_H_table_file",
+                            CMS19_H_table_file);
+    load_table_SESAME(&e->CMS19_H, CMS19_H_table_file);
+    prepare_table_SESAME(&e->CMS19_H);
+    convert_units_SESAME(&e->CMS19_H, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_CMS19_He", 0)) {
+    char CMS19_He_table_file[PARSER_MAX_LINE_SIZE];
+    set_CMS19_He(&e->CMS19_He, eos_planetary_id_CMS19_He);
+    parser_get_param_string(params, "EoS:planetary_CMS19_He_table_file",
+                            CMS19_He_table_file);
+    load_table_SESAME(&e->CMS19_He, CMS19_He_table_file);
+    prepare_table_SESAME(&e->CMS19_He);
+    convert_units_SESAME(&e->CMS19_He, us);
+  }
+  if (parser_get_opt_param_int(params, "EoS:planetary_use_CD21_HHe", 0)) {
+    char CD21_HHe_table_file[PARSER_MAX_LINE_SIZE];
+    set_CD21_HHe(&e->CD21_HHe, eos_planetary_id_CD21_HHe);
+    parser_get_param_string(params, "EoS:planetary_CD21_HHe_table_file",
+                            CD21_HHe_table_file);
+    load_table_SESAME(&e->CD21_HHe, CD21_HHe_table_file);
+    prepare_table_SESAME(&e->CD21_HHe);
+    convert_units_SESAME(&e->CD21_HHe, us);
   }
 
   // ANEOS -- using SESAME-style tables
diff --git a/src/equation_of_state/planetary/hm80.h b/src/equation_of_state/planetary/hm80.h
index d969e86e88d5807e514fad8606d4a63188e99a5d..893ef60afa9b07f37e94367824d25b8f3fddc06b 100644
--- a/src/equation_of_state/planetary/hm80.h
+++ b/src/equation_of_state/planetary/hm80.h
@@ -179,7 +179,7 @@ INLINE static void convert_units_HM80(struct HM80_params *mat,
 
 // gas_internal_energy_from_entropy
 INLINE static float HM80_internal_energy_from_entropy(
-    float density, float entropy, const struct HM80_params *mat) {
+    const float density, const float entropy, const struct HM80_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -187,7 +187,8 @@ INLINE static float HM80_internal_energy_from_entropy(
 }
 
 // gas_pressure_from_entropy
-INLINE static float HM80_pressure_from_entropy(float density, float entropy,
+INLINE static float HM80_pressure_from_entropy(const float density,
+                                               const float entropy,
                                                const struct HM80_params *mat) {
 
   error("This EOS function is not yet implemented!");
@@ -196,7 +197,8 @@ INLINE static float HM80_pressure_from_entropy(float density, float entropy,
 }
 
 // gas_entropy_from_pressure
-INLINE static float HM80_entropy_from_pressure(float density, float pressure,
+INLINE static float HM80_entropy_from_pressure(const float density,
+                                               const float pressure,
                                                const struct HM80_params *mat) {
 
   error("This EOS function is not yet implemented!");
@@ -206,7 +208,7 @@ INLINE static float HM80_entropy_from_pressure(float density, float pressure,
 
 // gas_soundspeed_from_entropy
 INLINE static float HM80_soundspeed_from_entropy(
-    float density, float entropy, const struct HM80_params *mat) {
+    const float density, const float entropy, const struct HM80_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -215,29 +217,25 @@ INLINE static float HM80_soundspeed_from_entropy(
 
 // gas_entropy_from_internal_energy
 INLINE static float HM80_entropy_from_internal_energy(
-    float density, float u, const struct HM80_params *mat) {
+    const float density, const float u, const struct HM80_params *mat) {
 
   return 0.f;
 }
 
 // gas_pressure_from_internal_energy
 INLINE static float HM80_pressure_from_internal_energy(
-    float density, float u, const struct HM80_params *mat) {
-
-  float log_P, log_P_1, log_P_2, log_P_3, log_P_4;
+    const float density, const float u, const struct HM80_params *mat) {
 
   if (u <= 0.f) {
     return 0.f;
   }
 
-  int idx_rho, idx_u;
-  float intp_rho, intp_u;
   const float log_rho = logf(density);
   const float log_u = logf(u);
 
   // 2D interpolation (bilinear with log(rho), log(u)) to find P(rho, u)
-  idx_rho = floor((log_rho - mat->log_rho_min) * mat->inv_log_rho_step);
-  idx_u = floor((log_u - mat->log_u_min) * mat->inv_log_u_step);
+  int idx_rho = floor((log_rho - mat->log_rho_min) * mat->inv_log_rho_step);
+  int idx_u = floor((log_u - mat->log_u_min) * mat->inv_log_u_step);
 
   // If outside the table then extrapolate from the edge and edge-but-one values
   if (idx_rho <= -1) {
@@ -251,26 +249,31 @@ INLINE static float HM80_pressure_from_internal_energy(
     idx_u = mat->num_u - 2;
   }
 
-  intp_rho = (log_rho - mat->log_rho_min - idx_rho * mat->log_rho_step) *
-             mat->inv_log_rho_step;
-  intp_u =
+  const float intp_rho =
+      (log_rho - mat->log_rho_min - idx_rho * mat->log_rho_step) *
+      mat->inv_log_rho_step;
+  const float intp_u =
       (log_u - mat->log_u_min - idx_u * mat->log_u_step) * mat->inv_log_u_step;
 
   // Table values
-  log_P_1 = mat->table_log_P_rho_u[idx_rho * mat->num_u + idx_u];
-  log_P_2 = mat->table_log_P_rho_u[idx_rho * mat->num_u + idx_u + 1];
-  log_P_3 = mat->table_log_P_rho_u[(idx_rho + 1) * mat->num_u + idx_u];
-  log_P_4 = mat->table_log_P_rho_u[(idx_rho + 1) * mat->num_u + idx_u + 1];
-
-  log_P = (1.f - intp_rho) * ((1.f - intp_u) * log_P_1 + intp_u * log_P_2) +
-          intp_rho * ((1.f - intp_u) * log_P_3 + intp_u * log_P_4);
+  const float log_P_1 = mat->table_log_P_rho_u[idx_rho * mat->num_u + idx_u];
+  const float log_P_2 =
+      mat->table_log_P_rho_u[idx_rho * mat->num_u + idx_u + 1];
+  const float log_P_3 =
+      mat->table_log_P_rho_u[(idx_rho + 1) * mat->num_u + idx_u];
+  const float log_P_4 =
+      mat->table_log_P_rho_u[(idx_rho + 1) * mat->num_u + idx_u + 1];
+
+  const float log_P =
+      (1.f - intp_rho) * ((1.f - intp_u) * log_P_1 + intp_u * log_P_2) +
+      intp_rho * ((1.f - intp_u) * log_P_3 + intp_u * log_P_4);
 
   return expf(log_P);
 }
 
 // gas_internal_energy_from_pressure
 INLINE static float HM80_internal_energy_from_pressure(
-    float density, float P, const struct HM80_params *mat) {
+    const float density, const float P, const struct HM80_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -279,9 +282,9 @@ INLINE static float HM80_internal_energy_from_pressure(
 
 // gas_soundspeed_from_internal_energy
 INLINE static float HM80_soundspeed_from_internal_energy(
-    float density, float u, const struct HM80_params *mat) {
+    const float density, const float u, const struct HM80_params *mat) {
 
-  float c, P;
+  float c;
 
   // Bulk modulus
   if (mat->bulk_mod != 0) {
@@ -289,10 +292,10 @@ INLINE static float HM80_soundspeed_from_internal_energy(
   }
   // Ideal gas
   else {
-    P = HM80_pressure_from_internal_energy(density, u, mat);
+    const float P = HM80_pressure_from_internal_energy(density, u, mat);
     c = sqrtf(hydro_gamma * P / density);
 
-    if (c <= 0) {
+    if (c <= 0.f) {
       c = sqrtf(hydro_gamma * mat->P_min_for_c_min / density);
     }
   }
@@ -302,7 +305,35 @@ INLINE static float HM80_soundspeed_from_internal_energy(
 
 // gas_soundspeed_from_pressure
 INLINE static float HM80_soundspeed_from_pressure(
-    float density, float P, const struct HM80_params *mat) {
+    const float density, const float P, const struct HM80_params *mat) {
+
+  error("This EOS function is not yet implemented!");
+
+  return 0.f;
+}
+
+// gas_entropy_from_internal_energy
+INLINE static float HM80_temperature_from_internal_energy(
+    const float density, const float u, const struct HM80_params *mat) {
+
+  error("This EOS function is not yet implemented!");
+
+  return 0.f;
+}
+
+// gas_density_from_pressure_and_temperature
+INLINE static float HM80_density_from_pressure_and_temperature(
+    const float P, const float T, const struct HM80_params *mat) {
+
+  error("This EOS function is not yet implemented!");
+
+  return 0.f;
+}
+
+// gas_density_from_pressure_and_internal_energy
+INLINE static float HM80_density_from_pressure_and_internal_energy(
+    const float P, const float u, const float rho_ref, const float rho_sph,
+    const struct HM80_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
diff --git a/src/equation_of_state/planetary/ideal_gas.h b/src/equation_of_state/planetary/ideal_gas.h
index c6da2445dad98d37cd03caa444efb9744f80d238..a2fe001e5f92791854606d2c347ee6fbac1fd88e 100644
--- a/src/equation_of_state/planetary/ideal_gas.h
+++ b/src/equation_of_state/planetary/ideal_gas.h
@@ -58,7 +58,7 @@ INLINE static void set_idg_def(struct idg_params *mat,
  * @param entropy The entropy \f$A\f$.
  */
 INLINE static float idg_internal_energy_from_entropy(
-    float density, float entropy, const struct idg_params *mat) {
+    const float density, const float entropy, const struct idg_params *mat) {
 
   return entropy * powf(density, mat->gamma - 1.f) *
          mat->one_over_gamma_minus_one;
@@ -72,7 +72,8 @@ INLINE static float idg_internal_energy_from_entropy(
  * @param density The density \f$\rho\f$.
  * @param entropy The entropy \f$A\f$.
  */
-INLINE static float idg_pressure_from_entropy(float density, float entropy,
+INLINE static float idg_pressure_from_entropy(const float density,
+                                              const float entropy,
                                               const struct idg_params *mat) {
 
   return entropy * powf(density, mat->gamma);
@@ -87,7 +88,8 @@ INLINE static float idg_pressure_from_entropy(float density, float entropy,
  * @param pressure The pressure \f$P\f$.
  * @return The entropy \f$A\f$.
  */
-INLINE static float idg_entropy_from_pressure(float density, float pressure,
+INLINE static float idg_entropy_from_pressure(const float density,
+                                              const float pressure,
                                               const struct idg_params *mat) {
 
   return pressure * powf(density, -mat->gamma);
@@ -101,7 +103,8 @@ INLINE static float idg_entropy_from_pressure(float density, float pressure,
  * @param density The density \f$\rho\f$.
  * @param entropy The entropy \f$A\f$.
  */
-INLINE static float idg_soundspeed_from_entropy(float density, float entropy,
+INLINE static float idg_soundspeed_from_entropy(const float density,
+                                                const float entropy,
                                                 const struct idg_params *mat) {
 
   return sqrtf(mat->gamma * powf(density, mat->gamma - 1.f) * entropy);
@@ -116,7 +119,7 @@ INLINE static float idg_soundspeed_from_entropy(float density, float entropy,
  * @param u The internal energy \f$u\f$
  */
 INLINE static float idg_entropy_from_internal_energy(
-    float density, float u, const struct idg_params *mat) {
+    const float density, const float u, const struct idg_params *mat) {
 
   return (mat->gamma - 1.f) * u * powf(density, 1.f - mat->gamma);
 }
@@ -130,7 +133,7 @@ INLINE static float idg_entropy_from_internal_energy(
  * @param u The internal energy \f$u\f$
  */
 INLINE static float idg_pressure_from_internal_energy(
-    float density, float u, const struct idg_params *mat) {
+    const float density, const float u, const struct idg_params *mat) {
 
   return (mat->gamma - 1.f) * u * density;
 }
@@ -145,7 +148,7 @@ INLINE static float idg_pressure_from_internal_energy(
  * @return The internal energy \f$u\f$.
  */
 INLINE static float idg_internal_energy_from_pressure(
-    float density, float pressure, const struct idg_params *mat) {
+    const float density, const float pressure, const struct idg_params *mat) {
 
   return mat->one_over_gamma_minus_one * pressure / density;
 }
@@ -159,7 +162,7 @@ INLINE static float idg_internal_energy_from_pressure(
  * @param u The internal energy \f$u\f$
  */
 INLINE static float idg_soundspeed_from_internal_energy(
-    float density, float u, const struct idg_params *mat) {
+    const float density, const float u, const struct idg_params *mat) {
 
   return sqrtf(u * mat->gamma * (mat->gamma - 1.f));
 }
@@ -172,10 +175,37 @@ INLINE static float idg_soundspeed_from_internal_energy(
  * @param density The density \f$\rho\f$
  * @param P The pressure \f$P\f$
  */
-INLINE static float idg_soundspeed_from_pressure(float density, float P,
+INLINE static float idg_soundspeed_from_pressure(const float density,
+                                                 const float P,
                                                  const struct idg_params *mat) {
 
   return sqrtf(mat->gamma * P / density);
 }
 
+// gas_temperature_from_internal_energy
+INLINE static float idg_temperature_from_internal_energy(
+    const float density, const float u, const struct idg_params *mat) {
+
+  error("This EOS function is not yet implemented!");
+
+  return 0.f;
+}
+
+// gas_density_from_pressure_and_temperature
+INLINE static float idg_density_from_pressure_and_temperature(
+    const float P, const float T, const struct idg_params *mat) {
+
+  error("This EOS function is not yet implemented!");
+
+  return 0.f;
+}
+
+// gas_density_from_pressure_and_internal_energy
+INLINE static float idg_density_from_pressure_and_internal_energy(
+    const float P, const float u, const float rho_ref, const float rho_sph,
+    const struct idg_params *mat) {
+
+  return mat->one_over_gamma_minus_one * P / u;
+}
+
 #endif /* SWIFT_IDEAL_GAS_EQUATION_OF_STATE_H */
diff --git a/src/equation_of_state/planetary/sesame.h b/src/equation_of_state/planetary/sesame.h
old mode 100755
new mode 100644
index e86111664cce4e98625629a24d2729ddf5e67f8d..cb4b8cccbcaaa4507727f1a4a3a61caaf66501ac
--- a/src/equation_of_state/planetary/sesame.h
+++ b/src/equation_of_state/planetary/sesame.h
@@ -43,6 +43,7 @@
 // SESAME parameters
 struct SESAME_params {
   float *table_log_rho;
+  float *table_log_T;
   float *table_log_u_rho_T;
   float *table_P_rho_T;
   float *table_c_rho_T;
@@ -77,6 +78,30 @@ INLINE static void set_SS08_water(struct SESAME_params *mat,
   mat->mat_id = mat_id;
   mat->version_date = 20220714;
 }
+INLINE static void set_AQUA(struct SESAME_params *mat,
+                            enum eos_planetary_material_id mat_id) {
+  // Haldemann et al. (2020)
+  mat->mat_id = mat_id;
+  mat->version_date = 20220714;
+}
+INLINE static void set_CMS19_H(struct SESAME_params *mat,
+                               enum eos_planetary_material_id mat_id) {
+  // Chabrier et al. (2019)
+  mat->mat_id = mat_id;
+  mat->version_date = 20220905;
+}
+INLINE static void set_CMS19_He(struct SESAME_params *mat,
+                                enum eos_planetary_material_id mat_id) {
+  // Chabrier et al. (2019)
+  mat->mat_id = mat_id;
+  mat->version_date = 20220905;
+}
+INLINE static void set_CD21_HHe(struct SESAME_params *mat,
+                                enum eos_planetary_material_id mat_id) {
+  // Chabrier & Debras (2021)
+  mat->mat_id = mat_id;
+  mat->version_date = 20220905;
+}
 INLINE static void set_ANEOS_forsterite(struct SESAME_params *mat,
                                         enum eos_planetary_material_id mat_id) {
   // Stewart et al. (2019)
@@ -176,6 +201,7 @@ INLINE static void load_table_SESAME(struct SESAME_params *mat,
 
   // Allocate table memory
   mat->table_log_rho = (float *)malloc(mat->num_rho * sizeof(float));
+  mat->table_log_T = (float *)malloc(mat->num_T * sizeof(float));
   mat->table_log_u_rho_T =
       (float *)malloc(mat->num_rho * mat->num_T * sizeof(float));
   mat->table_P_rho_T =
@@ -197,10 +223,16 @@ INLINE static void load_table_SESAME(struct SESAME_params *mat,
     }
   }
 
-  // Temperatures (ignored)
+  // Temperatures (not log yet)
   for (int i_T = -1; i_T < mat->num_T; i_T++) {
-    c = fscanf(f, "%f", &ignore);
-    if (c != 1) error("Failed to read the SESAME EoS table %s", table_file);
+    // Ignore the first elements of rho = 0, T = 0
+    if (i_T == -1) {
+      c = fscanf(f, "%f", &ignore);
+      if (c != 1) error("Failed to read the SESAME EoS table %s", table_file);
+    } else {
+      c = fscanf(f, "%f", &mat->table_log_T[i_T]);
+      if (c != 1) error("Failed to read the SESAME EoS table %s", table_file);
+    }
   }
 
   // Sp. int. energies (not log yet), pressures, sound speeds, and sp.
@@ -232,6 +264,10 @@ INLINE static void prepare_table_SESAME(struct SESAME_params *mat) {
   for (int i_rho = 0; i_rho < mat->num_rho; i_rho++) {
     mat->table_log_rho[i_rho] = logf(mat->table_log_rho[i_rho]);
   }
+  // Convert temperatures to log(temperature)
+  for (int i_T = 0; i_T < mat->num_T; i_T++) {
+    mat->table_log_T[i_T] = logf(mat->table_log_T[i_T]);
+  }
 
   // Initialise tiny values
   mat->u_tiny = FLT_MAX;
@@ -297,6 +333,11 @@ INLINE static void prepare_table_SESAME(struct SESAME_params *mat) {
 
       mat->table_log_s_rho_T[i_rho * mat->num_T + i_T] =
           logf(mat->table_log_s_rho_T[i_rho * mat->num_T + i_T]);
+
+      // Ensure P > 0
+      if (mat->table_P_rho_T[i_rho * mat->num_T + i_T] <= 0) {
+        mat->table_P_rho_T[i_rho * mat->num_T + i_T] = mat->P_tiny;
+      }
     }
   }
 }
@@ -316,6 +357,13 @@ INLINE static void convert_units_SESAME(struct SESAME_params *mat,
              units_cgs_conversion_factor(us, UNIT_CONV_DENSITY));
   }
 
+  // Temperatures (log)
+  for (int i_T = 0; i_T < mat->num_T; i_T++) {
+    mat->table_log_T[i_T] +=
+        logf(units_cgs_conversion_factor(&si, UNIT_CONV_TEMPERATURE) /
+             units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE));
+  }
+
   // Sp. int. energies (log), pressures, sound speeds, and sp. entropies
   for (int i_rho = 0; i_rho < mat->num_rho; i_rho++) {
     for (int i_T = 0; i_T < mat->num_T; i_T++) {
@@ -352,28 +400,26 @@ INLINE static void convert_units_SESAME(struct SESAME_params *mat,
 
 // gas_internal_energy_from_entropy
 INLINE static float SESAME_internal_energy_from_entropy(
-    float density, float entropy, const struct SESAME_params *mat) {
-
-  float u, log_u_1, log_u_2, log_u_3, log_u_4;
+    const float density, const float entropy, const struct SESAME_params *mat) {
 
+  // Return zero if entropy is zero
   if (entropy <= 0.f) {
     return 0.f;
   }
 
-  int idx_rho, idx_s_1, idx_s_2;
-  float intp_rho, intp_s_1, intp_s_2;
   const float log_rho = logf(density);
   const float log_s = logf(entropy);
 
-  // 2D interpolation (bilinear with log(rho), log(s)) to find u(rho, s))
+  // 2D interpolation (bilinear with log(rho), log(s) to find u(rho, s))
+
   // Density index
-  idx_rho =
+  int idx_rho =
       find_value_in_monot_incr_array(log_rho, mat->table_log_rho, mat->num_rho);
 
   // Sp. entropy at this and the next density (in relevant slice of s array)
-  idx_s_1 = find_value_in_monot_incr_array(
+  int idx_s_1 = find_value_in_monot_incr_array(
       log_s, mat->table_log_s_rho_T + idx_rho * mat->num_T, mat->num_T);
-  idx_s_2 = find_value_in_monot_incr_array(
+  int idx_s_2 = find_value_in_monot_incr_array(
       log_s, mat->table_log_s_rho_T + (idx_rho + 1) * mat->num_T, mat->num_T);
 
   // If outside the table then extrapolate from the edge and edge-but-one values
@@ -394,6 +440,7 @@ INLINE static float SESAME_internal_energy_from_entropy(
   }
 
   // Check for duplicates in SESAME tables before interpolation
+  float intp_rho, intp_s_1, intp_s_2;
   if (mat->table_log_rho[idx_rho + 1] != mat->table_log_rho[idx_rho]) {
     intp_rho = (log_rho - mat->table_log_rho[idx_rho]) /
                (mat->table_log_rho[idx_rho + 1] - mat->table_log_rho[idx_rho]);
@@ -420,10 +467,13 @@ INLINE static float SESAME_internal_energy_from_entropy(
   }
 
   // Table values
-  log_u_1 = mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_s_1];
-  log_u_2 = mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_s_1 + 1];
-  log_u_3 = mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_s_2];
-  log_u_4 = mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_s_2 + 1];
+  const float log_u_1 = mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_s_1];
+  const float log_u_2 =
+      mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_s_1 + 1];
+  const float log_u_3 =
+      mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_s_2];
+  const float log_u_4 =
+      mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_s_2 + 1];
 
   // If below the minimum s at this rho then just use the lowest table values
   if ((idx_rho > 0.f) && ((intp_s_1 < 0.f) || (intp_s_2 < 0.f) ||
@@ -433,18 +483,17 @@ INLINE static float SESAME_internal_energy_from_entropy(
   }
 
   // Interpolate with the log values
-  u = (1.f - intp_rho) * ((1.f - intp_s_1) * log_u_1 + intp_s_1 * log_u_2) +
+  const float u =
+      (1.f - intp_rho) * ((1.f - intp_s_1) * log_u_1 + intp_s_1 * log_u_2) +
       intp_rho * ((1.f - intp_s_2) * log_u_3 + intp_s_2 * log_u_4);
 
   // Convert back from log
-  u = expf(u);
-
-  return u;
+  return expf(u);
 }
 
 // gas_pressure_from_entropy
 INLINE static float SESAME_pressure_from_entropy(
-    float density, float entropy, const struct SESAME_params *mat) {
+    const float density, const float entropy, const struct SESAME_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -453,7 +502,8 @@ INLINE static float SESAME_pressure_from_entropy(
 
 // gas_entropy_from_pressure
 INLINE static float SESAME_entropy_from_pressure(
-    float density, float pressure, const struct SESAME_params *mat) {
+    const float density, const float pressure,
+    const struct SESAME_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -462,7 +512,7 @@ INLINE static float SESAME_entropy_from_pressure(
 
 // gas_soundspeed_from_entropy
 INLINE static float SESAME_soundspeed_from_entropy(
-    float density, float entropy, const struct SESAME_params *mat) {
+    const float density, const float entropy, const struct SESAME_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -471,35 +521,32 @@ INLINE static float SESAME_soundspeed_from_entropy(
 
 // gas_entropy_from_internal_energy
 INLINE static float SESAME_entropy_from_internal_energy(
-    float density, float u, const struct SESAME_params *mat) {
+    const float density, const float u, const struct SESAME_params *mat) {
 
   return 0.f;
 }
 
 // gas_pressure_from_internal_energy
 INLINE static float SESAME_pressure_from_internal_energy(
-    float density, float u, const struct SESAME_params *mat) {
-
-  float P, P_1, P_2, P_3, P_4;
+    const float density, const float u, const struct SESAME_params *mat) {
 
   if (u <= 0.f) {
     return 0.f;
   }
 
-  int idx_rho, idx_u_1, idx_u_2;
-  float intp_rho, intp_u_1, intp_u_2;
   const float log_rho = logf(density);
   const float log_u = logf(u);
 
-  // 2D interpolation (bilinear with log(rho), log(u)) to find P(rho, u))
+  // 2D interpolation (bilinear with log(rho), log(u) to find P(rho, u))
+
   // Density index
-  idx_rho =
+  int idx_rho =
       find_value_in_monot_incr_array(log_rho, mat->table_log_rho, mat->num_rho);
 
   // Sp. int. energy at this and the next density (in relevant slice of u array)
-  idx_u_1 = find_value_in_monot_incr_array(
+  int idx_u_1 = find_value_in_monot_incr_array(
       log_u, mat->table_log_u_rho_T + idx_rho * mat->num_T, mat->num_T);
-  idx_u_2 = find_value_in_monot_incr_array(
+  int idx_u_2 = find_value_in_monot_incr_array(
       log_u, mat->table_log_u_rho_T + (idx_rho + 1) * mat->num_T, mat->num_T);
 
   // If outside the table then extrapolate from the edge and edge-but-one values
@@ -520,18 +567,19 @@ INLINE static float SESAME_pressure_from_internal_energy(
   }
 
   // Check for duplicates in SESAME tables before interpolation
+  float intp_rho, intp_u_1, intp_u_2;
+  const int idx_u_rho_T = idx_rho * mat->num_T + idx_u_1;
   if (mat->table_log_rho[idx_rho + 1] != mat->table_log_rho[idx_rho]) {
     intp_rho = (log_rho - mat->table_log_rho[idx_rho]) /
                (mat->table_log_rho[idx_rho + 1] - mat->table_log_rho[idx_rho]);
   } else {
     intp_rho = 1.f;
   }
-  if (mat->table_log_u_rho_T[idx_rho * mat->num_T + (idx_u_1 + 1)] !=
-      mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_u_1]) {
-    intp_u_1 =
-        (log_u - mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_u_1]) /
-        (mat->table_log_u_rho_T[idx_rho * mat->num_T + (idx_u_1 + 1)] -
-         mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_u_1]);
+  if (mat->table_log_u_rho_T[idx_u_rho_T + 1] !=
+      mat->table_log_u_rho_T[idx_u_rho_T]) {
+    intp_u_1 = (log_u - mat->table_log_u_rho_T[idx_u_rho_T]) /
+               (mat->table_log_u_rho_T[idx_u_rho_T + 1] -
+                mat->table_log_u_rho_T[idx_u_rho_T]);
   } else {
     intp_u_1 = 1.f;
   }
@@ -546,10 +594,10 @@ INLINE static float SESAME_pressure_from_internal_energy(
   }
 
   // Table values
-  P_1 = mat->table_P_rho_T[idx_rho * mat->num_T + idx_u_1];
-  P_2 = mat->table_P_rho_T[idx_rho * mat->num_T + idx_u_1 + 1];
-  P_3 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2];
-  P_4 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2 + 1];
+  float P_1 = mat->table_P_rho_T[idx_u_rho_T];
+  float P_2 = mat->table_P_rho_T[idx_u_rho_T + 1];
+  float P_3 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2];
+  float P_4 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2 + 1];
 
   // If below the minimum u at this rho then just use the lowest table values
   if ((idx_rho > 0.f) &&
@@ -582,19 +630,18 @@ INLINE static float SESAME_pressure_from_internal_energy(
   P_2 = logf(P_2);
   P_3 = logf(P_3);
   P_4 = logf(P_4);
+  const float P_1_2 = (1.f - intp_u_1) * P_1 + intp_u_1 * P_2;
+  const float P_3_4 = (1.f - intp_u_2) * P_3 + intp_u_2 * P_4;
 
-  P = (1.f - intp_rho) * ((1.f - intp_u_1) * P_1 + intp_u_1 * P_2) +
-      intp_rho * ((1.f - intp_u_2) * P_3 + intp_u_2 * P_4);
+  const float P = (1.f - intp_rho) * P_1_2 + intp_rho * P_3_4;
 
   // Convert back from log
-  P = expf(P);
-
-  return P;
+  return expf(P);
 }
 
 // gas_internal_energy_from_pressure
 INLINE static float SESAME_internal_energy_from_pressure(
-    float density, float P, const struct SESAME_params *mat) {
+    const float density, const float P, const struct SESAME_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -603,28 +650,26 @@ INLINE static float SESAME_internal_energy_from_pressure(
 
 // gas_soundspeed_from_internal_energy
 INLINE static float SESAME_soundspeed_from_internal_energy(
-    float density, float u, const struct SESAME_params *mat) {
-
-  float c, c_1, c_2, c_3, c_4;
+    const float density, const float u, const struct SESAME_params *mat) {
 
+  // Return zero if internal energy is non-positive
   if (u <= 0.f) {
     return 0.f;
   }
 
-  int idx_rho, idx_u_1, idx_u_2;
-  float intp_rho, intp_u_1, intp_u_2;
   const float log_rho = logf(density);
   const float log_u = logf(u);
 
-  // 2D interpolation (bilinear with log(rho), log(u)) to find c(rho, u))
+  // 2D interpolation (bilinear with log(rho), log(u) to find c(rho, u))
+
   // Density index
-  idx_rho =
+  int idx_rho =
       find_value_in_monot_incr_array(log_rho, mat->table_log_rho, mat->num_rho);
 
   // Sp. int. energy at this and the next density (in relevant slice of u array)
-  idx_u_1 = find_value_in_monot_incr_array(
+  int idx_u_1 = find_value_in_monot_incr_array(
       log_u, mat->table_log_u_rho_T + idx_rho * mat->num_T, mat->num_T);
-  idx_u_2 = find_value_in_monot_incr_array(
+  int idx_u_2 = find_value_in_monot_incr_array(
       log_u, mat->table_log_u_rho_T + (idx_rho + 1) * mat->num_T, mat->num_T);
 
   // If outside the table then extrapolate from the edge and edge-but-one values
@@ -645,18 +690,19 @@ INLINE static float SESAME_soundspeed_from_internal_energy(
   }
 
   // Check for duplicates in SESAME tables before interpolation
+  float intp_rho, intp_u_1, intp_u_2;
   if (mat->table_log_rho[idx_rho + 1] != mat->table_log_rho[idx_rho]) {
     intp_rho = (log_rho - mat->table_log_rho[idx_rho]) /
                (mat->table_log_rho[idx_rho + 1] - mat->table_log_rho[idx_rho]);
   } else {
     intp_rho = 1.f;
   }
-  if (mat->table_log_u_rho_T[idx_rho * mat->num_T + (idx_u_1 + 1)] !=
-      mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_u_1]) {
-    intp_u_1 =
-        (log_u - mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_u_1]) /
-        (mat->table_log_u_rho_T[idx_rho * mat->num_T + (idx_u_1 + 1)] -
-         mat->table_log_u_rho_T[idx_rho * mat->num_T + idx_u_1]);
+  const int idx_u_rho_T = idx_rho * mat->num_T + idx_u_1;
+  if (mat->table_log_u_rho_T[idx_u_rho_T + 1] !=
+      mat->table_log_u_rho_T[idx_u_rho_T]) {
+    intp_u_1 = (log_u - mat->table_log_u_rho_T[idx_u_rho_T]) /
+               (mat->table_log_u_rho_T[idx_u_rho_T + 1] -
+                mat->table_log_u_rho_T[idx_u_rho_T]);
   } else {
     intp_u_1 = 1.f;
   }
@@ -671,10 +717,10 @@ INLINE static float SESAME_soundspeed_from_internal_energy(
   }
 
   // Table values
-  c_1 = mat->table_c_rho_T[idx_rho * mat->num_T + idx_u_1];
-  c_2 = mat->table_c_rho_T[idx_rho * mat->num_T + idx_u_1 + 1];
-  c_3 = mat->table_c_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2];
-  c_4 = mat->table_c_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2 + 1];
+  float c_1 = mat->table_c_rho_T[idx_u_rho_T];
+  float c_2 = mat->table_c_rho_T[idx_u_rho_T + 1];
+  float c_3 = mat->table_c_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2];
+  float c_4 = mat->table_c_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2 + 1];
 
   // If more than two table values are non-positive then return zero
   int num_non_pos = 0;
@@ -710,22 +756,536 @@ INLINE static float SESAME_soundspeed_from_internal_energy(
     intp_u_2 = 0;
   }
 
-  c = (1.f - intp_rho) * ((1.f - intp_u_1) * c_1 + intp_u_1 * c_2) +
-      intp_rho * ((1.f - intp_u_2) * c_3 + intp_u_2 * c_4);
+  const float c = (1.f - intp_rho) * ((1.f - intp_u_1) * c_1 + intp_u_1 * c_2) +
+                  intp_rho * ((1.f - intp_u_2) * c_3 + intp_u_2 * c_4);
 
   // Convert back from log
-  c = expf(c);
-
-  return c;
+  return expf(c);
 }
 
 // gas_soundspeed_from_pressure
 INLINE static float SESAME_soundspeed_from_pressure(
-    float density, float P, const struct SESAME_params *mat) {
+    const float density, const float P, const struct SESAME_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
   return 0.f;
 }
 
+// gas_temperature_from_internal_energy
+INLINE static float SESAME_temperature_from_internal_energy(
+    const float density, const float u, const struct SESAME_params *mat) {
+
+  // Return zero if zero internal energy
+  if (u <= 0.f) {
+    return 0.f;
+  }
+
+  const float log_rho = logf(density);
+  const float log_u = logf(u);
+
+  // 2D interpolation (bilinear with log(rho), log(u) to find T(rho, u)
+
+  // Density index
+  int idx_rho =
+      find_value_in_monot_incr_array(log_rho, mat->table_log_rho, mat->num_rho);
+
+  // Sp. int. energy at this and the next density (in relevant slice of u array)
+  int idx_u_1 = find_value_in_monot_incr_array(
+      log_u, mat->table_log_u_rho_T + idx_rho * mat->num_T, mat->num_T);
+  int idx_u_2 = find_value_in_monot_incr_array(
+      log_u, mat->table_log_u_rho_T + (idx_rho + 1) * mat->num_T, mat->num_T);
+
+  // If outside the table then extrapolate from the edge and edge-but-one values
+  if (idx_rho <= -1) {
+    idx_rho = 0;
+  } else if (idx_rho >= mat->num_rho) {
+    idx_rho = mat->num_rho - 2;
+  }
+  if (idx_u_1 <= -1) {
+    idx_u_1 = 0;
+  } else if (idx_u_1 >= mat->num_T) {
+    idx_u_1 = mat->num_T - 2;
+  }
+  if (idx_u_2 <= -1) {
+    idx_u_2 = 0;
+  } else if (idx_u_2 >= mat->num_T) {
+    idx_u_2 = mat->num_T - 2;
+  }
+
+  // Check for duplicates in SESAME tables before interpolation
+  float intp_u_1, intp_u_2;
+  const int idx_u_rho_T = idx_rho * mat->num_T + idx_u_1;
+  if (mat->table_log_u_rho_T[idx_u_rho_T + 1] !=
+      mat->table_log_u_rho_T[idx_u_rho_T]) {
+    intp_u_1 = (log_u - mat->table_log_u_rho_T[idx_u_rho_T]) /
+               (mat->table_log_u_rho_T[idx_u_rho_T + 1] -
+                mat->table_log_u_rho_T[idx_u_rho_T]);
+  } else {
+    intp_u_1 = 1.f;
+  }
+  if (mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + (idx_u_2 + 1)] !=
+      mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]) {
+    intp_u_2 =
+        (log_u - mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]) /
+        (mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + (idx_u_2 + 1)] -
+         mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]);
+  } else {
+    intp_u_2 = 1.f;
+  }
+
+  // Compute line points
+  float log_rho_1 = mat->table_log_rho[idx_rho];
+  float log_rho_2 = mat->table_log_rho[idx_rho + 1];
+  float log_T_1 = mat->table_log_T[idx_u_1];
+  log_T_1 +=
+      intp_u_1 * (mat->table_log_T[idx_u_1 + 1] - mat->table_log_T[idx_u_1]);
+  float log_T_2 = mat->table_log_T[idx_u_2];
+  log_T_2 +=
+      intp_u_2 * (mat->table_log_T[idx_u_2 + 1] - mat->table_log_T[idx_u_2]);
+
+  // Intersect line passing through (log_rho_1, log_T_1), (log_rho_2, log_T_2)
+  // with line density = log_rho
+
+  // Check for log_T_1 == log_T_2
+  float log_T;
+  if (log_T_1 == log_T_2) {
+    log_T = log_T_1;
+  } else {
+    // log_rho = slope*log_T + intercept
+    const float slope = (log_rho_1 - log_rho_2) / (log_T_1 - log_T_2);
+    const float intercept = log_rho_1 - slope * log_T_1;
+    log_T = (log_rho - intercept) / slope;
+  }
+
+  // Convert back from log
+  return expf(log_T);
+}
+
+// gas_density_from_pressure_and_temperature
+INLINE static float SESAME_density_from_pressure_and_temperature(
+    float P, float T, const struct SESAME_params *mat) {
+
+  // Return zero if pressure is non-positive
+  if (P <= 0.f) {
+    return 0.f;
+  }
+
+  const float log_T = logf(T);
+
+  // 2D interpolation (bilinear with log(T), P to find rho(T, P))
+
+  // Temperature index
+  int idx_T =
+      find_value_in_monot_incr_array(log_T, mat->table_log_T, mat->num_T);
+
+  // If outside the table then extrapolate from the edge and edge-but-one values
+  if (idx_T <= -1) {
+    idx_T = 0;
+  } else if (idx_T >= mat->num_T) {
+    idx_T = mat->num_T - 2;
+  }
+
+  // Pressure at this and the next temperature (in relevant vertical slice of P
+  // array)
+  int idx_P_1 = vertical_find_value_in_monot_incr_array(
+      P, mat->table_P_rho_T, mat->num_rho, mat->num_T, idx_T);
+  int idx_P_2 = vertical_find_value_in_monot_incr_array(
+      P, mat->table_P_rho_T, mat->num_rho, mat->num_T, idx_T + 1);
+
+  // If outside the table then extrapolate from the edge and edge-but-one values
+  if (idx_P_1 <= -1) {
+    idx_P_1 = 0;
+  } else if (idx_P_1 >= mat->num_rho) {
+    idx_P_1 = mat->num_rho - 2;
+  }
+  if (idx_P_2 <= -1) {
+    idx_P_2 = 0;
+  } else if (idx_P_2 >= mat->num_rho) {
+    idx_P_2 = mat->num_rho - 2;
+  }
+
+  // Check for duplicates in SESAME tables before interpolation
+  float intp_P_1, intp_P_2;
+  if (mat->table_P_rho_T[(idx_P_1 + 1) * mat->num_T + idx_T] !=
+      mat->table_P_rho_T[idx_P_1 * mat->num_T + idx_T]) {
+    intp_P_1 = (P - mat->table_P_rho_T[idx_P_1 * mat->num_T + idx_T]) /
+               (mat->table_P_rho_T[(idx_P_1 + 1) * mat->num_T + idx_T] -
+                mat->table_P_rho_T[idx_P_1 * mat->num_T + idx_T]);
+  } else {
+    intp_P_1 = 1.f;
+  }
+  if (mat->table_P_rho_T[(idx_P_2 + 1) * mat->num_T + (idx_T + 1)] !=
+      mat->table_P_rho_T[idx_P_2 * mat->num_T + (idx_T + 1)]) {
+    intp_P_2 = (P - mat->table_P_rho_T[idx_P_2 * mat->num_T + (idx_T + 1)]) /
+               (mat->table_P_rho_T[(idx_P_2 + 1) * mat->num_T + (idx_T + 1)] -
+                mat->table_P_rho_T[idx_P_2 * mat->num_T + (idx_T + 1)]);
+  } else {
+    intp_P_2 = 1.f;
+  }
+
+  // Compute line points
+  const float log_T_1 = mat->table_log_T[idx_T];
+  const float log_T_2 = mat->table_log_T[idx_T + 1];
+  const float log_rho_1 = (1.f - intp_P_1) * mat->table_log_rho[idx_P_1] +
+                          intp_P_1 * mat->table_log_rho[idx_P_1 + 1];
+  const float log_rho_2 = (1.f - intp_P_2) * mat->table_log_rho[idx_P_2] +
+                          intp_P_2 * mat->table_log_rho[idx_P_2 + 1];
+
+  // Intersect line passing through (log_rho_1, log_T_1), (log_rho_2, log_T_2)
+  // with line temperature = log_T
+
+  // Check for log_rho_1 == log_rho_2
+  float log_rho;
+  if (log_rho_1 == log_rho_2) {
+    log_rho = log_rho_1;
+  } else {
+    // log_T = slope*log_rho + intercept
+    const float slope = (log_T_1 - log_T_2) / (log_rho_1 - log_rho_2);
+    const float intercept = log_T_1 - slope * log_rho_1;
+    log_rho = (log_T - intercept) / slope;
+  }
+
+  // Convert back from log
+  return expf(log_rho);
+}
+
+// gas_density_from_pressure_and_internal_energy
+INLINE static float SESAME_density_from_pressure_and_internal_energy(
+    float P, float u, float rho_ref, float rho_sph,
+    const struct SESAME_params *mat) {
+
+  // Return the unchanged density if u or P is non-positive
+  if (u <= 0.f || P <= 0.f) {
+    return rho_sph;
+  }
+
+  // Convert inputs to log
+  const float log_u = logf(u);
+  const float log_P = logf(P);
+  const float log_rho_ref = logf(rho_ref);
+
+  // Find rounded down index of reference density. This is where we start our
+  // search
+  int idx_rho_ref = find_value_in_monot_incr_array(
+      log_rho_ref, mat->table_log_rho, mat->num_rho);
+
+  // If no roots are found in the current search range, we increase search range
+  // by search_factor_log_rho above and below the reference density each
+  // iteration.
+  const float search_factor_log_rho = logf(10.f);
+
+  // Initialise the minimum and maximum densities we're searching to at the
+  // reference density. These will change before the first iteration.
+  float log_rho_min = log_rho_ref;
+  float log_rho_max = log_rho_ref;
+
+  // Initialise search indices around rho_ref
+  int idx_rho_below_min, idx_rho_above_max;
+
+  // If we find a root, it will get stored as closest_root
+  float closest_root = -1.f;
+  float root_below;
+
+  // Initialise pressures
+  float P_above_lower, P_above_upper;
+  float P_below_lower, P_below_upper;
+  P_above_upper = 0.f;
+  P_below_lower = 0.f;
+
+  // Increase search range by search_factor_log_rho
+  log_rho_max += search_factor_log_rho;
+  idx_rho_above_max = find_value_in_monot_incr_array(
+      log_rho_max, mat->table_log_rho, mat->num_rho);
+  log_rho_min -= search_factor_log_rho;
+  idx_rho_below_min = find_value_in_monot_incr_array(
+      log_rho_min, mat->table_log_rho, mat->num_rho);
+
+  // When searching above/below, we are looking for where the pressure P(rho, u)
+  // of the table densities changes from being less than to more than, or vice
+  // versa, the desired pressure. If this is the case, there is a root between
+  // these table values of rho.
+
+  // First look for roots above rho_ref
+  int idx_u_rho_T;
+  float intp_rho;
+  for (int idx_rho = idx_rho_ref; idx_rho <= idx_rho_above_max; idx_rho++) {
+
+    // This is similar to P_u_rho, but we're not interested in intp_rho,
+    // and instead calculate the pressure for both intp_rho=0 and intp_rho=1
+
+    // Sp. int. energy at this and the next density (in relevant slice of u
+    // array)
+    int idx_u_1 = find_value_in_monot_incr_array(
+        log_u, mat->table_log_u_rho_T + idx_rho * mat->num_T, mat->num_T);
+    int idx_u_2 = find_value_in_monot_incr_array(
+        log_u, mat->table_log_u_rho_T + (idx_rho + 1) * mat->num_T, mat->num_T);
+
+    // If outside the table then extrapolate from the edge and edge-but-one
+    // values
+    if (idx_rho <= -1) {
+      idx_rho = 0;
+    } else if (idx_rho >= mat->num_rho) {
+      idx_rho = mat->num_rho - 2;
+    }
+    if (idx_u_1 <= -1) {
+      idx_u_1 = 0;
+    } else if (idx_u_1 >= mat->num_T) {
+      idx_u_1 = mat->num_T - 2;
+    }
+    if (idx_u_2 <= -1) {
+      idx_u_2 = 0;
+    } else if (idx_u_2 >= mat->num_T) {
+      idx_u_2 = mat->num_T - 2;
+    }
+
+    float intp_u_1, intp_u_2;
+    idx_u_rho_T = idx_rho * mat->num_T + idx_u_1;
+    if (mat->table_log_u_rho_T[idx_u_rho_T + 1] !=
+        mat->table_log_u_rho_T[idx_u_rho_T]) {
+      intp_u_1 = (log_u - mat->table_log_u_rho_T[idx_u_rho_T]) /
+                 (mat->table_log_u_rho_T[idx_u_rho_T + 1] -
+                  mat->table_log_u_rho_T[idx_u_rho_T]);
+    } else {
+      intp_u_1 = 1.f;
+    }
+    if (mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + (idx_u_2 + 1)] !=
+        mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]) {
+      intp_u_2 =
+          (log_u -
+           mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]) /
+          (mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + (idx_u_2 + 1)] -
+           mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]);
+    } else {
+      intp_u_2 = 1.f;
+    }
+
+    // Table values
+    float P_1 = mat->table_P_rho_T[idx_u_rho_T];
+    float P_2 = mat->table_P_rho_T[idx_u_rho_T + 1];
+    float P_3 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2];
+    float P_4 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2 + 1];
+
+    // If below the minimum u at this rho then just use the lowest table values
+    if ((idx_rho > 0.f) &&
+        ((intp_u_1 < 0.f) || (intp_u_2 < 0.f) || (P_1 > P_2) || (P_3 > P_4))) {
+      intp_u_1 = 0;
+      intp_u_2 = 0;
+    }
+
+    // If more than two table values are non-positive then return zero
+    int num_non_pos = 0;
+    if (P_1 <= 0.f) num_non_pos++;
+    if (P_2 <= 0.f) num_non_pos++;
+    if (P_3 <= 0.f) num_non_pos++;
+    if (P_4 <= 0.f) num_non_pos++;
+    if (num_non_pos > 0) {
+      // If just one or two are non-positive then replace them with a tiny value
+      // Unless already trying to extrapolate in which case return zero
+      if ((num_non_pos > 2) || (mat->P_tiny == 0.f) || (intp_u_1 < 0.f) ||
+          (intp_u_2 < 0.f)) {
+        break;  // return rho_sph;
+      }
+      if (P_1 <= 0.f) P_1 = mat->P_tiny;
+      if (P_2 <= 0.f) P_2 = mat->P_tiny;
+      if (P_3 <= 0.f) P_3 = mat->P_tiny;
+      if (P_4 <= 0.f) P_4 = mat->P_tiny;
+    }
+
+    // Interpolate with the log values
+    P_1 = logf(P_1);
+    P_2 = logf(P_2);
+    P_3 = logf(P_3);
+    P_4 = logf(P_4);
+    float P_1_2 = (1.f - intp_u_1) * P_1 + intp_u_1 * P_2;
+    float P_3_4 = (1.f - intp_u_2) * P_3 + intp_u_2 * P_4;
+
+    // Pressure for intp_rho = 0
+    P_above_lower = expf(P_1_2);
+
+    // Because of linear interpolation, pressures are not exactly continuous
+    // as we go from one side of a grid point to another. See if there is
+    // a root between the last P_above_upper and the new P_above_lower,
+    // which are approx the same.
+    if (idx_rho != idx_rho_ref) {
+      if ((P_above_lower - P) * (P_above_upper - P) <= 0) {
+        closest_root = expf(mat->table_log_rho[idx_rho]);
+        break;
+      }
+    }
+
+    // Pressure for intp_rho = 1
+    P_above_upper = expf(P_3_4);
+
+    // Does the pressure of the adjacent table densities switch from being
+    // above to below the desired pressure, or vice versa? If so, there is a
+    // root.
+    if ((P_above_lower - P) * (P_above_upper - P) <= 0.f) {
+
+      // If there is a root, interpolate between the table values:
+      intp_rho = (log_P - P_1_2) / (P_3_4 - P_1_2);
+
+      closest_root = expf(mat->table_log_rho[idx_rho] +
+                          intp_rho * (mat->table_log_rho[idx_rho + 1] -
+                                      mat->table_log_rho[idx_rho]));
+
+      // If the root is between the same table values as the reference value,
+      // then this is the closest root, so we can return it without further
+      // searching
+      if (idx_rho == idx_rho_ref) {
+        return closest_root;
+      }
+
+      // Found a root, so no need to search higher densities
+      break;
+    }
+  }
+
+  // If we found a root above, change search range below so that we're only
+  // looking for closer (in log) roots than the one we found
+  if (closest_root > 0.f) {
+    log_rho_min = log_rho_ref - (logf(closest_root) - log_rho_ref);
+    idx_rho_below_min = find_value_in_monot_incr_array(
+        log_rho_min, mat->table_log_rho, mat->num_rho);
+  }
+
+  // Now look for roots below rho_ref
+  for (int idx_rho = idx_rho_ref; idx_rho >= idx_rho_below_min; idx_rho--) {
+
+    // Sp. int. energy at this and the next density (in relevant slice of u
+    // array)
+    int idx_u_1 = find_value_in_monot_incr_array(
+        log_u, mat->table_log_u_rho_T + idx_rho * mat->num_T, mat->num_T);
+    int idx_u_2 = find_value_in_monot_incr_array(
+        log_u, mat->table_log_u_rho_T + (idx_rho + 1) * mat->num_T, mat->num_T);
+
+    // If outside the table then extrapolate from the edge and edge-but-one
+    // values
+    if (idx_rho <= -1) {
+      idx_rho = 0;
+    } else if (idx_rho >= mat->num_rho) {
+      idx_rho = mat->num_rho - 2;
+    }
+    if (idx_u_1 <= -1) {
+      idx_u_1 = 0;
+    } else if (idx_u_1 >= mat->num_T) {
+      idx_u_1 = mat->num_T - 2;
+    }
+    if (idx_u_2 <= -1) {
+      idx_u_2 = 0;
+    } else if (idx_u_2 >= mat->num_T) {
+      idx_u_2 = mat->num_T - 2;
+    }
+
+    idx_u_rho_T = idx_rho * mat->num_T + idx_u_1;
+    float intp_u_1, intp_u_2;
+    if (mat->table_log_u_rho_T[idx_u_rho_T + 1] !=
+        mat->table_log_u_rho_T[idx_u_rho_T]) {
+      intp_u_1 = (log_u - mat->table_log_u_rho_T[idx_u_rho_T]) /
+                 (mat->table_log_u_rho_T[idx_u_rho_T + 1] -
+                  mat->table_log_u_rho_T[idx_u_rho_T]);
+    } else {
+      intp_u_1 = 1.f;
+    }
+    if (mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + (idx_u_2 + 1)] !=
+        mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]) {
+      intp_u_2 =
+          (log_u -
+           mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]) /
+          (mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + (idx_u_2 + 1)] -
+           mat->table_log_u_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2]);
+    } else {
+      intp_u_2 = 1.f;
+    }
+
+    // Table values
+    float P_1 = mat->table_P_rho_T[idx_u_rho_T];
+    float P_2 = mat->table_P_rho_T[idx_u_rho_T + 1];
+    float P_3 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2];
+    float P_4 = mat->table_P_rho_T[(idx_rho + 1) * mat->num_T + idx_u_2 + 1];
+
+    // If below the minimum u at this rho then just use the lowest table values
+    if ((idx_rho > 0.f) &&
+        ((intp_u_1 < 0.f) || (intp_u_2 < 0.f) || (P_1 > P_2) || (P_3 > P_4))) {
+      intp_u_1 = 0;
+      intp_u_2 = 0;
+    }
+
+    // If more than two table values are non-positive then return zero
+    int num_non_pos = 0;
+    if (P_1 <= 0.f) num_non_pos++;
+    if (P_2 <= 0.f) num_non_pos++;
+    if (P_3 <= 0.f) num_non_pos++;
+    if (P_4 <= 0.f) num_non_pos++;
+    if (num_non_pos > 0) {
+      // If just one or two are non-positive then replace them with a tiny value
+      // Unless already trying to extrapolate in which case return zero
+      if ((num_non_pos > 2) || (mat->P_tiny == 0.f) || (intp_u_1 < 0.f) ||
+          (intp_u_2 < 0.f)) {
+        break;
+      }
+      if (P_1 <= 0.f) P_1 = mat->P_tiny;
+      if (P_2 <= 0.f) P_2 = mat->P_tiny;
+      if (P_3 <= 0.f) P_3 = mat->P_tiny;
+      if (P_4 <= 0.f) P_4 = mat->P_tiny;
+    }
+
+    // Interpolate with the log values
+    P_1 = logf(P_1);
+    P_2 = logf(P_2);
+    P_3 = logf(P_3);
+    P_4 = logf(P_4);
+    const float P_1_2 = (1.f - intp_u_1) * P_1 + intp_u_1 * P_2;
+    const float P_3_4 = (1.f - intp_u_2) * P_3 + intp_u_2 * P_4;
+
+    // Pressure for intp_rho = 1
+    P_below_upper = expf(P_3_4);
+    // Because of linear interpolation, pressures are not exactly continuous
+    // as we go from one side of a grid point to another. See if there is
+    // a root between the last P_below_lower and the new P_below_upper,
+    // which are approx the same.
+    if (idx_rho != idx_rho_ref) {
+      if ((P_below_lower - P) * (P_below_upper - P) <= 0) {
+        closest_root = expf(mat->table_log_rho[idx_rho + 1]);
+        break;
+      }
+    }
+    // Pressure for intp_rho = 0
+    P_below_lower = expf(P_1_2);
+
+    // Does the pressure of the adjacent table densities switch from being
+    // above to below the desired pressure, or vice versa? If so, there is a
+    // root.
+    if ((P_below_lower - P) * (P_below_upper - P) <= 0.f) {
+
+      // If there is a root, interpolate between the table values:
+      intp_rho = (log_P - P_1_2) / (P_3_4 - P_1_2);
+
+      root_below = expf(mat->table_log_rho[idx_rho] +
+                        intp_rho * (mat->table_log_rho[idx_rho + 1] -
+                                    mat->table_log_rho[idx_rho]));
+
+      // If we found a root above, which one is closer to the reference rho?
+      if (closest_root > 0.f) {
+        if (fabsf(logf(root_below) - logf(rho_ref)) <
+            fabsf(logf(closest_root) - logf(rho_ref))) {
+          closest_root = root_below;
+        }
+      } else {
+        closest_root = root_below;
+      }
+      // Found a root, so no need to search higher densities
+      break;
+    }
+  }
+
+  // Return the root if we found one
+  if (closest_root > 0.f) {
+    return closest_root;
+  }
+
+  // If we don't find a root before we reach max_counter, return rho_ref. Maybe
+  // we should give an error here?
+  return rho_sph;
+}
 #endif /* SWIFT_SESAME_EQUATION_OF_STATE_H */
diff --git a/src/equation_of_state/planetary/tillotson.h b/src/equation_of_state/planetary/tillotson.h
index 5831b1785485d5dc2563718c94fc9cc04a08b822..dc5b5576c33616446795451fd0229e6e58fdfec4 100644
--- a/src/equation_of_state/planetary/tillotson.h
+++ b/src/equation_of_state/planetary/tillotson.h
@@ -37,12 +37,16 @@
 #include "equation_of_state.h"
 #include "inline.h"
 #include "physical_constants.h"
+#include "sesame.h"
 #include "units.h"
 
 // Tillotson parameters
 struct Til_params {
-  float rho_0, a, b, A, B, u_0, u_iv, u_cv, alpha, beta, eta_min, eta_zero,
-      P_min;
+  float rho_0, a, b, A, B, u_0, u_iv, u_cv, alpha, beta;
+  float eta_min, eta_zero, P_min, C_V;
+  float *A1_u_cold;
+  float rho_cold_min, rho_cold_max;
+  float rho_min, rho_max;
   enum eos_planetary_material_id mat_id;
 };
 
@@ -62,7 +66,12 @@ INLINE static void set_Til_iron(struct Til_params *mat,
   mat->beta = 5.0f;
   mat->eta_min = 0.0f;
   mat->eta_zero = 0.0f;
-  mat->P_min = 0.0f;
+  mat->P_min = 0.01f;
+  mat->C_V = 449.0f;
+  mat->rho_cold_min = 100.0f;
+  mat->rho_cold_max = 1.0e5f;
+  mat->rho_min = 1.0f;
+  mat->rho_max = 1.0e5f;
 }
 INLINE static void set_Til_granite(struct Til_params *mat,
                                    enum eos_planetary_material_id mat_id) {
@@ -79,7 +88,12 @@ INLINE static void set_Til_granite(struct Til_params *mat,
   mat->beta = 5.0f;
   mat->eta_min = 0.0f;
   mat->eta_zero = 0.0f;
-  mat->P_min = 0.0f;
+  mat->P_min = 0.01f;
+  mat->C_V = 790.0f;
+  mat->rho_cold_min = 100.0f;
+  mat->rho_cold_max = 1.0e5f;
+  mat->rho_min = 1.0f;
+  mat->rho_max = 1.0e5f;
 }
 INLINE static void set_Til_basalt(struct Til_params *mat,
                                   enum eos_planetary_material_id mat_id) {
@@ -96,7 +110,12 @@ INLINE static void set_Til_basalt(struct Til_params *mat,
   mat->beta = 5.0f;
   mat->eta_min = 0.0f;
   mat->eta_zero = 0.0f;
-  mat->P_min = 0.0f;
+  mat->P_min = 0.01f;
+  mat->C_V = 790.0f;
+  mat->rho_cold_min = 100.0f;
+  mat->rho_cold_max = 1.0e5f;
+  mat->rho_min = 1.0f;
+  mat->rho_max = 1.0e5f;
 }
 INLINE static void set_Til_water(struct Til_params *mat,
                                  enum eos_planetary_material_id mat_id) {
@@ -113,7 +132,77 @@ INLINE static void set_Til_water(struct Til_params *mat,
   mat->beta = 5.0f;
   mat->eta_min = 0.925f;
   mat->eta_zero = 0.875f;
+  mat->P_min = 0.01f;
+  mat->C_V = 4186.0f;
+  mat->rho_cold_min = 100.0f;
+  mat->rho_cold_max = 1.0e5f;
+  mat->rho_min = 1.0f;
+  mat->rho_max = 1.0e5f;
+}
+INLINE static void set_Til_ice(struct Til_params *mat,
+                               enum eos_planetary_material_id mat_id) {
+  mat->mat_id = mat_id;
+  mat->rho_0 = 1293.0f;
+  mat->a = 0.3f;
+  mat->b = 0.1f;
+  mat->A = 1.07e10f;
+  mat->B = 6.5e10f;
+  mat->u_0 = 1.0e7f;
+  mat->u_iv = 7.73e5f;
+  mat->u_cv = 3.04e6f;
+  mat->alpha = 10.0f;
+  mat->beta = 5.0f;
+  mat->eta_min = 0.925f;
+  mat->eta_zero = 0.875f;
   mat->P_min = 0.0f;
+  mat->C_V = 2093.0f;
+  mat->rho_cold_min = 100.0f;
+  mat->rho_cold_max = 1.0e5f;
+  mat->rho_min = 1.0f;
+  mat->rho_max = 1.0e5f;
+}
+
+/*
+    Read the parameters from a file.
+
+    File contents
+    -------------
+    # header (5 lines)
+    rho_0 (kg/m3)  a (-)  b (-)  A (Pa)  B (Pa)
+    u_0 (J)  u_iv (J)  u_cv (J)  alpha (-)  beta (-)
+    eta_min (-)  eta_zero (-)  P_min (Pa)  C_V (J kg^-1 K^-1)
+    rho_cold_min (kg/m3)  rho_cold_max (kg/m3)  rho_min (kg/m3)  rho_max (kg/m3)
+*/
+INLINE static void set_Til_custom(struct Til_params *mat,
+                                  enum eos_planetary_material_id mat_id,
+                                  char *param_file) {
+  mat->mat_id = mat_id;
+
+  // Load table contents from file
+  FILE *f = fopen(param_file, "r");
+  if (f == NULL)
+    error("Failed to open the Tillotson EoS file '%s'", param_file);
+
+  // Skip header lines
+  skip_lines(f, 5);
+
+  // Read parameters (SI)
+  int c;
+  c = fscanf(f, "%f %f %f %f %f", &mat->rho_0, &mat->a, &mat->b, &mat->A,
+             &mat->B);
+  if (c != 5) error("Failed to read the Tillotson EoS file %s", param_file);
+
+  c = fscanf(f, "%f %f %f %f %f", &mat->u_0, &mat->u_iv, &mat->u_cv,
+             &mat->alpha, &mat->beta);
+  if (c != 5) error("Failed to read the Tillotson EoS file %s", param_file);
+
+  c = fscanf(f, "%f %f %f %f", &mat->eta_min, &mat->eta_zero, &mat->P_min,
+             &mat->C_V);
+  if (c != 4) error("Failed to read the Tillotson EoS file %s", param_file);
+
+  c = fscanf(f, "%f %f %f %f", &mat->rho_cold_min, &mat->rho_cold_max,
+             &mat->rho_min, &mat->rho_max);
+  if (c != 4) error("Failed to read the Tillotson EoS file %s", param_file);
 }
 
 // Convert to internal units
@@ -123,6 +212,8 @@ INLINE static void convert_units_Til(struct Til_params *mat,
   struct unit_system si;
   units_init_si(&si);
 
+  const int N = 10000;
+
   // SI to cgs
   mat->rho_0 *= units_cgs_conversion_factor(&si, UNIT_CONV_DENSITY);
   mat->A *= units_cgs_conversion_factor(&si, UNIT_CONV_PRESSURE);
@@ -132,6 +223,19 @@ INLINE static void convert_units_Til(struct Til_params *mat,
   mat->u_cv *= units_cgs_conversion_factor(&si, UNIT_CONV_ENERGY_PER_UNIT_MASS);
   mat->P_min *= units_cgs_conversion_factor(&si, UNIT_CONV_PRESSURE);
 
+  for (int i = 0; i < N; i++) {
+    mat->A1_u_cold[i] *=
+        units_cgs_conversion_factor(&si, UNIT_CONV_ENERGY_PER_UNIT_MASS);
+  }
+
+  mat->C_V *= units_cgs_conversion_factor(&si, UNIT_CONV_ENERGY_PER_UNIT_MASS);
+  // Entropy units don't work? using internal kelvin
+
+  mat->rho_cold_min *= units_cgs_conversion_factor(&si, UNIT_CONV_DENSITY);
+  mat->rho_cold_max *= units_cgs_conversion_factor(&si, UNIT_CONV_DENSITY);
+  mat->rho_min *= units_cgs_conversion_factor(&si, UNIT_CONV_DENSITY);
+  mat->rho_max *= units_cgs_conversion_factor(&si, UNIT_CONV_DENSITY);
+
   // cgs to internal
   mat->rho_0 /= units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
   mat->A /= units_cgs_conversion_factor(us, UNIT_CONV_PRESSURE);
@@ -140,11 +244,24 @@ INLINE static void convert_units_Til(struct Til_params *mat,
   mat->u_iv /= units_cgs_conversion_factor(us, UNIT_CONV_ENERGY_PER_UNIT_MASS);
   mat->u_cv /= units_cgs_conversion_factor(us, UNIT_CONV_ENERGY_PER_UNIT_MASS);
   mat->P_min /= units_cgs_conversion_factor(us, UNIT_CONV_PRESSURE);
+
+  for (int i = 0; i < N; i++) {
+    mat->A1_u_cold[i] /=
+        units_cgs_conversion_factor(us, UNIT_CONV_ENERGY_PER_UNIT_MASS);
+  }
+
+  mat->C_V /= units_cgs_conversion_factor(us, UNIT_CONV_ENERGY_PER_UNIT_MASS);
+  // Entropy units don't work? using internal kelvin
+
+  mat->rho_cold_min /= units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
+  mat->rho_cold_max /= units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
+  mat->rho_min /= units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
+  mat->rho_max /= units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
 }
 
 // gas_internal_energy_from_entropy
 INLINE static float Til_internal_energy_from_entropy(
-    float density, float entropy, const struct Til_params *mat) {
+    const float density, const float entropy, const struct Til_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -152,7 +269,8 @@ INLINE static float Til_internal_energy_from_entropy(
 }
 
 // gas_pressure_from_entropy
-INLINE static float Til_pressure_from_entropy(float density, float entropy,
+INLINE static float Til_pressure_from_entropy(const float density,
+                                              const float entropy,
                                               const struct Til_params *mat) {
 
   error("This EOS function is not yet implemented!");
@@ -161,7 +279,8 @@ INLINE static float Til_pressure_from_entropy(float density, float entropy,
 }
 
 // gas_entropy_from_pressure
-INLINE static float Til_entropy_from_pressure(float density, float pressure,
+INLINE static float Til_entropy_from_pressure(const float density,
+                                              const float pressure,
                                               const struct Til_params *mat) {
 
   error("This EOS function is not yet implemented!");
@@ -170,7 +289,8 @@ INLINE static float Til_entropy_from_pressure(float density, float pressure,
 }
 
 // gas_soundspeed_from_entropy
-INLINE static float Til_soundspeed_from_entropy(float density, float entropy,
+INLINE static float Til_soundspeed_from_entropy(const float density,
+                                                const float entropy,
                                                 const struct Til_params *mat) {
 
   error("This EOS function is not yet implemented!");
@@ -180,14 +300,14 @@ INLINE static float Til_soundspeed_from_entropy(float density, float entropy,
 
 // gas_entropy_from_internal_energy
 INLINE static float Til_entropy_from_internal_energy(
-    float density, float u, const struct Til_params *mat) {
+    const float density, const float u, const struct Til_params *mat) {
 
   return 0.f;
 }
 
 // gas_pressure_from_internal_energy
 INLINE static float Til_pressure_from_internal_energy(
-    float density, float u, const struct Til_params *mat) {
+    const float density, const float u, const struct Til_params *mat) {
 
   const float eta = density / mat->rho_0;
   const float eta_sq = eta * eta;
@@ -195,10 +315,9 @@ INLINE static float Til_pressure_from_internal_energy(
   const float nu = 1.f / eta - 1.f;
   const float w = u / (mat->u_0 * eta_sq) + 1.f;
   const float w_inv = 1.f / w;
-  float P_c, P_e, P;
 
   // Condensed or cold
-  P_c =
+  float P_c =
       (mat->a + mat->b * w_inv) * density * u + mat->A * mu + mat->B * mu * mu;
   if (eta < mat->eta_zero) {
     P_c = 0.f;
@@ -206,11 +325,12 @@ INLINE static float Til_pressure_from_internal_energy(
     P_c *= (eta - mat->eta_zero) / (mat->eta_min - mat->eta_zero);
   }
   // Expanded and hot
-  P_e = mat->a * density * u +
-        (mat->b * density * u * w_inv + mat->A * mu * expf(-mat->beta * nu)) *
-            expf(-mat->alpha * nu * nu);
+  float P_e = mat->a * density * u + (mat->b * density * u * w_inv +
+                                      mat->A * mu * expf(-mat->beta * nu)) *
+                                         expf(-mat->alpha * nu * nu);
 
   // Condensed or cold state
+  float P;
   if ((1.f < eta) || (u < mat->u_iv)) {
     P = P_c;
   }
@@ -234,7 +354,7 @@ INLINE static float Til_pressure_from_internal_energy(
 
 // gas_internal_energy_from_pressure
 INLINE static float Til_internal_energy_from_pressure(
-    float density, float P, const struct Til_params *mat) {
+    const float density, const float P, const struct Til_params *mat) {
 
   error("This EOS function is not yet implemented!");
 
@@ -243,7 +363,7 @@ INLINE static float Til_internal_energy_from_pressure(
 
 // gas_soundspeed_from_internal_energy
 INLINE static float Til_soundspeed_from_internal_energy(
-    float density, float u, const struct Til_params *mat) {
+    const float density, const float u, const struct Til_params *mat) {
 
   const float rho_0_inv = 1.f / mat->rho_0;
   const float eta = density * rho_0_inv;
@@ -256,25 +376,25 @@ INLINE static float Til_soundspeed_from_internal_energy(
   const float w_inv_sq = w_inv * w_inv;
   const float exp_beta = expf(-mat->beta * nu);
   const float exp_alpha = expf(-mat->alpha * nu * nu);
-  float P_c, P_e, c_sq_c, c_sq_e, c_sq;
 
   // Condensed or cold
-  P_c =
+  float P_c =
       (mat->a + mat->b * w_inv) * density * u + mat->A * mu + mat->B * mu * mu;
   if (eta < mat->eta_zero) {
     P_c = 0.f;
   } else if (eta < mat->eta_min) {
     P_c *= (eta - mat->eta_zero) / (mat->eta_min - mat->eta_zero);
   }
-  c_sq_c = P_c * rho_inv * (1.f + mat->a + mat->b * w_inv) +
-           mat->b * (w - 1.f) * w_inv_sq * (2.f * u - P_c * rho_inv) +
-           rho_inv * (mat->A + mat->B * (eta_sq - 1.f));
+  float c_sq_c = P_c * rho_inv * (1.f + mat->a + mat->b * w_inv) +
+                 mat->b * (w - 1.f) * w_inv_sq * (2.f * u - P_c * rho_inv) +
+                 rho_inv * (mat->A + mat->B * (eta_sq - 1.f));
 
   // Expanded and hot
-  P_e = mat->a * density * u +
-        (mat->b * density * u * w_inv + mat->A * mu * exp_beta) * exp_alpha;
+  float P_e =
+      mat->a * density * u +
+      (mat->b * density * u * w_inv + mat->A * mu * exp_beta) * exp_alpha;
 
-  c_sq_e =
+  float c_sq_e =
       P_e * rho_inv * (1.f + mat->a + mat->b * w_inv * exp_alpha) +
       (mat->b * density * u * w_inv_sq / eta_sq *
            (rho_inv / mat->u_0 * (2.f * u - P_e * rho_inv) +
@@ -285,6 +405,7 @@ INLINE static float Til_soundspeed_from_internal_energy(
           exp_alpha;
 
   // Condensed or cold state
+  float c_sq;
   if ((1.f < eta) || (u < mat->u_iv)) {
     c_sq = c_sq_c;
   }
@@ -304,7 +425,8 @@ INLINE static float Til_soundspeed_from_internal_energy(
 }
 
 // gas_soundspeed_from_pressure
-INLINE static float Til_soundspeed_from_pressure(float density, float P,
+INLINE static float Til_soundspeed_from_pressure(const float density,
+                                                 const float P,
                                                  const struct Til_params *mat) {
 
   error("This EOS function is not yet implemented!");
@@ -312,4 +434,494 @@ INLINE static float Til_soundspeed_from_pressure(float density, float P,
   return 0.f;
 }
 
+// Compute u cold
+INLINE static float compute_u_cold(
+    const float density, const struct Til_params *mat,
+    const enum eos_planetary_material_id mat_id) {
+  const int N = 10000;
+  const float rho_0 = mat->rho_0;
+  const float drho = (density - rho_0) / N;
+
+  float x = rho_0;
+  float u_cold = 1e-9;
+  for (int i = 0; i < N; i++) {
+    x += drho;
+    u_cold +=
+        Til_pressure_from_internal_energy(x, u_cold, mat) * drho / (x * x);
+  }
+
+  return u_cold;
+}
+
+// Compute A1_u_cold
+INLINE static void set_Til_u_cold(struct Til_params *mat,
+                                  enum eos_planetary_material_id mat_id) {
+
+  const int N = 10000;
+  const float rho_min = 100.f;
+  const float rho_max = 100000.f;
+
+  // Allocate table memory
+  mat->A1_u_cold = (float *)malloc(N * sizeof(float));
+
+  float rho = rho_min;
+  const float drho = (rho_max - rho_min) / (N - 1);
+
+  for (int i = 0; i < N; i++) {
+    mat->A1_u_cold[i] = compute_u_cold(rho, mat, mat_id);
+    rho += drho;
+  }
+}
+
+// Compute u cold fast from precomputed values
+INLINE static float compute_fast_u_cold(const float density,
+                                        const struct Til_params *mat) {
+
+  const int N = 10000;
+  const float rho_min = mat->rho_cold_min;
+  const float rho_max = mat->rho_cold_max;
+
+  const float drho = (rho_max - rho_min) / (N - 1);
+
+  const int a = (int)((density - rho_min) / drho);
+  const int b = a + 1;
+
+  float u_cold;
+  if (a >= 0 && a < (N - 1)) {
+    u_cold = mat->A1_u_cold[a];
+    u_cold += ((mat->A1_u_cold[b] - mat->A1_u_cold[a]) / drho) *
+              (density - rho_min - a * drho);
+  } else if (density < rho_min) {
+    u_cold = mat->A1_u_cold[0];
+  } else {
+    u_cold = mat->A1_u_cold[N - 1];
+    u_cold += ((mat->A1_u_cold[N - 1] - mat->A1_u_cold[N - 2]) / drho) *
+              (density - rho_max);
+  }
+  return u_cold;
+}
+
+// gas_temperature_from_internal_energy
+INLINE static float Til_temperature_from_internal_energy(
+    const float density, const float u, const struct Til_params *mat) {
+
+  const float u_cold = compute_fast_u_cold(density, mat);
+
+  float T = (u - u_cold) / (mat->C_V);
+  if (T < 0.f) {
+    T = 0.f;
+  }
+
+  return T;
+}
+
+// gas_pressure_from_density_and_temperature
+INLINE static float Til_pressure_from_temperature(
+    const float density, const float T, const struct Til_params *mat) {
+
+  const float u = compute_fast_u_cold(density, mat) + mat->C_V * T;
+  const float P = Til_pressure_from_internal_energy(density, u, mat);
+
+  return P;
+}
+
+// gas_density_from_pressure_and_temperature
+INLINE static float Til_density_from_pressure_and_temperature(
+    const float P, const float T, const struct Til_params *mat) {
+
+  float rho_min = mat->rho_min;
+  float rho_max = mat->rho_max;
+  float rho_mid = (rho_min + rho_max) / 2.f;
+
+  // Check for P == 0 or T == 0
+  float P_des;
+  if (P <= mat->P_min) {
+    P_des = mat->P_min;
+  } else {
+    P_des = P;
+  }
+
+  float P_min = Til_pressure_from_temperature(rho_min, T, mat);
+  float P_mid = Til_pressure_from_temperature(rho_mid, T, mat);
+  float P_max = Til_pressure_from_temperature(rho_max, T, mat);
+
+  // quick fix?
+  if (P_des < P_min) {
+    P_des = P_min;
+  }
+
+  const float tolerance = 0.001 * rho_min;
+  int counter = 0;
+  const int max_counter = 200;
+  if (P_des >= P_min && P_des <= P_max) {
+    while ((rho_max - rho_min) > tolerance && counter < max_counter) {
+
+      P_min = Til_pressure_from_temperature(rho_min, T, mat);
+      P_mid = Til_pressure_from_temperature(rho_mid, T, mat);
+      P_max = Til_pressure_from_temperature(rho_max, T, mat);
+
+      const float f0 = P_des - P_min;
+      const float f2 = P_des - P_mid;
+
+      if ((f0 * f2) > 0) {
+        rho_min = rho_mid;
+      } else {
+        rho_max = rho_mid;
+      }
+
+      rho_mid = (rho_min + rho_max) / 2.f;
+      counter += 1;
+    }
+  } else {
+    error("Error in Til_density_from_pressure_and_temperature");
+    return 0.f;
+  }
+  return rho_mid;
+}
+
+// gas_density_from_pressure_and_internal_energy
+INLINE static float Til_density_from_pressure_and_internal_energy(
+    const float P, const float u, const float rho_ref, const float rho_sph,
+    const struct Til_params *mat) {
+
+  if (P <= mat->P_min || u == 0) {
+    return rho_sph;
+  }
+
+  // We start search on the same curve as rho_ref, since this is most likely
+  // curve to find rho on
+  float eta_ref = rho_ref / mat->rho_0;
+
+  /*
+  There are 5 possible curves:
+    1: cold_min
+    2: cold
+    3: hybrid_min
+    4: hybrid
+    5: hot
+
+  These curves cover different eta ranges within three different regions of u:
+  u REGION 1 (u < u_iv):
+      eta < eta_min:           cold_min
+      eta_min < eta:           cold
+
+  u REGION 2 (u_iv < u < u_cv):
+      eta < eta_min:           hybrid_min
+      eta_min < eta < 1:       hybrid
+      1 < eta:                 cold
+
+  u REGION 3 (u_cv < u):
+      eta < 1:                 hot
+      1 < eta:                 cold
+
+  NOTE: for a lot of EoS, eta_min = 0, so search this region last if given the
+  option to save time for most EoS
+  */
+  enum Til_region {
+    Til_region_none,
+    Til_region_cold_min,
+    Til_region_cold,
+    Til_region_hybrid_min,
+    Til_region_hybrid,
+    Til_region_hot
+  };
+
+  // Based on our u region, what possible curves can rho be on? Ordered based on
+  // order we search for roots. Numbers correspond to curves in order given
+  // above. 0 is a dummy which breaks the loop.
+  enum Til_region possible_curves[3];
+  // u REGION 1
+  if (u <= mat->u_iv) {
+    if (eta_ref <= mat->eta_min) {
+      possible_curves[0] = Til_region_cold_min;
+      possible_curves[1] = Til_region_cold;
+      possible_curves[2] = Til_region_none;
+    } else {
+      possible_curves[0] = Til_region_cold;
+      possible_curves[1] = Til_region_cold_min;
+      possible_curves[2] = Til_region_none;
+    }
+    // u REGION 2
+  } else if (u <= mat->u_cv) {
+    if (eta_ref <= mat->eta_min) {
+      possible_curves[0] = Til_region_hybrid_min;
+      possible_curves[1] = Til_region_hybrid;
+      possible_curves[2] = Til_region_cold;
+    } else if (eta_ref <= 1) {
+      possible_curves[0] = Til_region_hybrid;
+      possible_curves[1] = Til_region_cold;
+      possible_curves[2] = Til_region_hybrid_min;
+    } else {
+      possible_curves[0] = Til_region_cold;
+      possible_curves[1] = Til_region_hybrid;
+      possible_curves[2] = Til_region_hybrid_min;
+    }
+    // u REGION 3
+  } else {
+    if (eta_ref <= 1) {
+      possible_curves[0] = Til_region_hot;
+      possible_curves[1] = Til_region_cold;
+      possible_curves[2] = Til_region_none;
+    } else {
+      possible_curves[0] = Til_region_cold;
+      possible_curves[1] = Til_region_hot;
+      possible_curves[2] = Til_region_none;
+    }
+  }
+  // Newton-Raphson
+  const int max_iter = 10;
+  const float tol = 1e-5;
+
+  // loops over possible curves
+  for (int i = 0; i < 3; i++) {
+
+    enum Til_region curve = possible_curves[i];
+
+    // if there are only two possible curves, break when we get to three and
+    // haven't found a root
+    if (curve == Til_region_none) {
+      break;
+    }
+
+    // Start iteration at reference value
+    float rho_iter = rho_ref;
+
+    // Constrain our initial guess to be on the curve we're currently looking at
+    // in the first loop, this is already satisfied.
+    if (i > 0) {
+
+      // u REGION 1
+      if (u <= mat->u_iv) {
+        if (curve == Til_region_cold_min) {
+          if (rho_iter > mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+        } else if (curve == Til_region_cold) {
+          if (rho_iter < mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+        } else {
+          error("Error in Til_density_from_pressure_and_internal_energy");
+        }
+        // u REGION 2
+      } else if (u <= mat->u_cv) {
+        if (curve == Til_region_hybrid_min) {
+          if (rho_iter > mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+        } else if (curve == Til_region_hybrid) {
+          if (rho_iter < mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+          if (rho_iter > mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else if (curve == Til_region_cold) {
+          if (rho_iter < mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else {
+          error("Error in Til_density_from_pressure_and_internal_energy");
+        }
+
+        // u REGION 3
+      } else {
+        if (curve == Til_region_hot) {
+          if (rho_iter > mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else if (curve == Til_region_cold) {
+          if (rho_iter < mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else {
+          error("Error in Til_density_from_pressure_and_internal_energy");
+        }
+      }
+    }
+
+    // Set this to an arbitrary number so we definitely dont't think we converge
+    // straigt away
+    float last_rho_iter = -1e5;
+
+    // Now iterate
+    for (int j = 0; j < max_iter; j++) {
+
+      const float eta_iter = rho_iter / mat->rho_0;
+      const float eta_iter_sq = eta_iter * eta_iter;
+      const float mu_iter = eta_iter - 1.0f;
+      const float nu_iter = 1.0f / eta_iter - 1.0f;
+      const float w_iter = u / (mat->u_0 * eta_iter_sq) + 1.0f;
+      const float w_iter_inv = 1.0f / w_iter;
+      const float exp1 = expf(-mat->beta * nu_iter);
+      const float exp2 = expf(-mat->alpha * nu_iter * nu_iter);
+
+      // Derivatives
+      const float dw_inv_drho_iter =
+          (2.f * mat->u_0 * u * eta_iter / mat->rho_0) /
+          ((u + mat->u_0 * eta_iter_sq) * (u + mat->u_0 * eta_iter_sq));
+      const float dmu_drho_iter = 1.f / mat->rho_0;
+      const float dmu_sq_drho_iter =
+          2.f * rho_iter / (mat->rho_0 * mat->rho_0) - 2.f / mat->rho_0;
+      const float dexp1_drho_iter =
+          mat->beta * mat->rho_0 * exp1 / (rho_iter * rho_iter);
+      const float dexp2_drho_iter = 2.f * mat->alpha * mat->rho_0 *
+                                    (mat->rho_0 - rho_iter) * exp2 /
+                                    (rho_iter * rho_iter * rho_iter);
+
+      // Use P_fraction to determine whether we've converged on a root
+      float P_fraction;
+
+      // Newton-Raphson
+      float P_c_iter, dP_c_drho_iter, P_h_iter, dP_h_drho_iter;
+
+      // if "cold" or "hybrid"
+      if (curve == Til_region_cold || curve == Til_region_hybrid) {
+        P_c_iter = (mat->a + mat->b * w_iter_inv) * rho_iter * u +
+                   mat->A * mu_iter + mat->B * mu_iter * mu_iter - P;
+        dP_c_drho_iter = (mat->a + mat->b * w_iter_inv) * u +
+                         mat->b * u * rho_iter * dw_inv_drho_iter +
+                         mat->A * dmu_drho_iter + mat->B * dmu_sq_drho_iter;
+        P_fraction = P_c_iter / P;
+
+        // if curve is cold then we've got everything we need
+        if (curve == Til_region_cold) {
+          rho_iter -= P_c_iter / dP_c_drho_iter;
+          // Don't use these:
+          P_h_iter = 0.f;
+          dP_h_drho_iter = 0.f;
+        }
+        // if "cold_min" or "hybrid_min"
+        // Can only have one version of the cold curve, therefore either use the
+        // min version or the normal version hence "else if"
+      } else if (curve == Til_region_cold_min ||
+                 curve == Til_region_hybrid_min) {
+        P_c_iter = ((mat->a + mat->b * w_iter_inv) * rho_iter * u +
+                    mat->A * mu_iter + mat->B * mu_iter * mu_iter) *
+                       (eta_iter - mat->eta_zero) /
+                       (mat->eta_min - mat->eta_zero) -
+                   P;
+        dP_c_drho_iter =
+            ((mat->a + mat->b * w_iter_inv) * u +
+             mat->b * u * rho_iter * dw_inv_drho_iter + mat->A * dmu_drho_iter +
+             mat->B * dmu_sq_drho_iter) *
+                (eta_iter - mat->eta_zero) / (mat->eta_min - mat->eta_zero) +
+            ((mat->a + mat->b * w_iter_inv) * rho_iter * u + mat->A * mu_iter +
+             mat->B * mu_iter * mu_iter) *
+                (1 / (mat->rho_0 * (mat->eta_min - mat->eta_zero)));
+        P_fraction = P_c_iter / P;
+
+        // if curve is cold_min then we've got everything we need
+        if (curve == Til_region_cold_min) {
+          rho_iter -= P_c_iter / dP_c_drho_iter;
+          // Don't use these:
+          P_c_iter = 0.f;
+          dP_c_drho_iter = 0.f;
+        }
+      }
+
+      // if "hybrid_min" or "hybrid" or "hot"
+      if (curve == Til_region_hybrid_min || curve == Til_region_hybrid ||
+          curve == Til_region_hot) {
+        P_h_iter =
+            mat->a * rho_iter * u +
+            (mat->b * rho_iter * u * w_iter_inv + mat->A * mu_iter * exp1) *
+                exp2 -
+            P;
+        dP_h_drho_iter =
+            mat->a * u +
+            (mat->b * u * w_iter_inv +
+             mat->b * u * rho_iter * dw_inv_drho_iter +
+             mat->A * mu_iter * dexp1_drho_iter +
+             mat->A * exp1 * dmu_drho_iter) *
+                exp2 +
+            (mat->b * rho_iter * u * w_iter_inv + mat->A * mu_iter * exp1) *
+                dexp2_drho_iter;
+        P_fraction = P_h_iter / P;
+
+        // if curve is hot then we've got everything we need
+        if (curve == Til_region_hot) {
+          rho_iter -= P_h_iter / dP_h_drho_iter;
+        }
+      }
+
+      // If we are on a hybrid or hybrid_min curve, we combie hot and cold
+      // curves
+      if (curve == Til_region_hybrid_min || curve == Til_region_hybrid) {
+        const float P_hybrid_iter =
+            ((u - mat->u_iv) * P_h_iter + (mat->u_cv - u) * P_c_iter) /
+            (mat->u_cv - mat->u_iv);
+        const float dP_hybrid_drho_iter = ((u - mat->u_iv) * dP_h_drho_iter +
+                                           (mat->u_cv - u) * dP_c_drho_iter) /
+                                          (mat->u_cv - mat->u_iv);
+        rho_iter -= P_hybrid_iter / dP_hybrid_drho_iter;
+        P_fraction = P_hybrid_iter / P;
+      }
+
+      // Now we have to constrain the new rho_iter to the curve we're on
+      // u REGION 1
+      if (u <= mat->u_iv) {
+        if (curve == Til_region_cold_min) {
+          if (rho_iter > mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+        } else if (curve == Til_region_cold) {
+          if (rho_iter < mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+        } else {
+          error("Error in Til_density_from_pressure_and_internal_energy");
+        }
+        // u REGION 2
+      } else if (u <= mat->u_cv) {
+        if (curve == Til_region_hybrid_min) {
+          if (rho_iter > mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+        } else if (curve == Til_region_hybrid) {
+          if (rho_iter < mat->eta_min * mat->rho_0) {
+            rho_iter = mat->eta_min * mat->rho_0;
+          }
+          if (rho_iter > mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else if (curve == Til_region_cold) {
+          if (rho_iter < mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else {
+          error("Error in Til_density_from_pressure_and_internal_energy");
+        }
+
+        // u REGION 3
+      } else {
+        if (curve == Til_region_hot) {
+          if (rho_iter > mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else if (curve == Til_region_cold) {
+          if (rho_iter < mat->rho_0) {
+            rho_iter = mat->rho_0;
+          }
+        } else {
+          error("Error in Til_density_from_pressure_and_internal_energy");
+        }
+      }
+
+      // Either we've converged ...
+      if (fabsf(P_fraction) < tol) {
+        return rho_iter;
+
+        // ... or we're stuck at the boundary ...
+      } else if (rho_iter == last_rho_iter) {
+        break;
+      }
+
+      // ... or we loop again
+      last_rho_iter = rho_iter;
+    }
+  }
+  return rho_sph;
+}
+
 #endif /* SWIFT_TILLOTSON_EQUATION_OF_STATE_H */
diff --git a/src/extra_io/EAGLE/extra.h b/src/extra_io/EAGLE/extra.h
index 95698eca08b7380146f7d29ce177d6094a72e7a7..356a37505c4404f44eead7c94ad0926b31c6aa24 100644
--- a/src/extra_io/EAGLE/extra.h
+++ b/src/extra_io/EAGLE/extra.h
@@ -21,10 +21,11 @@
 
 #include "chemistry.h"
 #include "cooling.h"
+#include "cooling/PS2020/cooling_tables.h"
 #include "engine.h"
 #include "star_formation.h"
 
-#define xray_table_date_string 20230110
+#define xray_table_date_string 20240406
 
 #define xray_emission_N_temperature 46
 #define xray_emission_N_density 71
@@ -139,9 +140,9 @@ INLINE static void read_xray_header(struct xray_properties *xrays,
   hid_t dataset = H5Dopen(tempfile_id, "Date_String", H5P_DEFAULT);
   herr_t status = H5Dread(dataset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL,
                           H5P_DEFAULT, &datestring);
-  if (status < 0) printf("error reading the date string");
+  if (status < 0) error("error reading the date string");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
   if (datestring != xray_table_date_string)
     error(
         "The table and code version do not match, please use table version %i",
@@ -155,9 +156,9 @@ INLINE static void read_xray_header(struct xray_properties *xrays,
   dataset = H5Dopen(tempfile_id, "Bins/Temperature_bins", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->Temperatures);
-  if (status < 0) printf("error reading temperatures");
+  if (status < 0) error("error reading temperatures");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* Read density bins */
   if (posix_memalign((void **)&xrays->Densities, SWIFT_STRUCT_ALIGNMENT,
@@ -167,9 +168,9 @@ INLINE static void read_xray_header(struct xray_properties *xrays,
   dataset = H5Dopen(tempfile_id, "Bins/Density_bins", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->Densities);
-  if (status < 0) printf("error reading densities");
+  if (status < 0) error("error reading densities");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* Read Helium bins */
   if (posix_memalign((void **)&xrays->He_bins, SWIFT_STRUCT_ALIGNMENT,
@@ -179,9 +180,9 @@ INLINE static void read_xray_header(struct xray_properties *xrays,
   dataset = H5Dopen(tempfile_id, "Bins/He_bins", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->He_bins);
-  if (status < 0) printf("error reading Helium massfractions");
+  if (status < 0) error("error reading Helium massfractions");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* Read solar metallicity */
   if (posix_memalign((void **)&xrays->Log10_solar_metallicity,
@@ -192,9 +193,9 @@ INLINE static void read_xray_header(struct xray_properties *xrays,
   dataset = H5Dopen(tempfile_id, "Bins/Solar_metallicities", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->Log10_solar_metallicity);
-  if (status < 0) printf("error reading solar metalicities");
+  if (status < 0) error("error reading solar metalicities");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* Get Solar metallicities from log solar metallicities */
   if (posix_memalign((void **)&xrays->Solar_metallicity, SWIFT_STRUCT_ALIGNMENT,
@@ -212,9 +213,9 @@ INLINE static void read_xray_header(struct xray_properties *xrays,
   dataset = H5Dopen(tempfile_id, "Bins/Redshift_bins", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->Redshifts);
-  if (status < 0) printf("error reading redshift bins");
+  if (status < 0) error("error reading redshift bins");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* Read element mass */
   if (posix_memalign((void **)&xrays->element_mass, SWIFT_STRUCT_ALIGNMENT,
@@ -224,9 +225,9 @@ INLINE static void read_xray_header(struct xray_properties *xrays,
   dataset = H5Dopen(tempfile_id, "Bins/Element_masses", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->element_mass);
-  if (status < 0) printf("error reading element masses");
+  if (status < 0) error("error reading element masses");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 }
 
 /**
@@ -261,9 +262,9 @@ INLINE static void read_xray_table(struct xray_properties *xrays,
   herr_t status =
       H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
               xrays->emissivity_erosita_low_intrinsic_photons);
-  if (status < 0) printf("error reading X-Ray table\n");
+  if (status < 0) error("error reading X-Ray table\n");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* erosita-high intrinsic photons */
   if (swift_memalign("xrays_table_erosita_high_photons",
@@ -280,9 +281,9 @@ INLINE static void read_xray_table(struct xray_properties *xrays,
   dataset = H5Dopen(file_id, "erosita-high/photons_intrinsic", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->emissivity_erosita_high_intrinsic_photons);
-  if (status < 0) printf("error reading X-Ray table\n");
+  if (status < 0) error("error reading X-Ray table\n");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* ROSAT intrinsic photons */
   if (swift_memalign("xray_table_ROSAT_photons",
@@ -297,9 +298,9 @@ INLINE static void read_xray_table(struct xray_properties *xrays,
   dataset = H5Dopen(file_id, "ROSAT/photons_intrinsic", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->emissivity_ROSAT_intrinsic_photons);
-  if (status < 0) printf("error reading X-Ray table\n");
+  if (status < 0) error("error reading X-Ray table\n");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   // erosita-low intrinsic energies
   if (swift_memalign("xrays_table_erosita_low_energies",
@@ -316,9 +317,9 @@ INLINE static void read_xray_table(struct xray_properties *xrays,
   dataset = H5Dopen(file_id, "erosita-low/energies_intrinsic", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->emissivity_erosita_low_intrinsic_energies);
-  if (status < 0) printf("error reading X-Ray table\n");
+  if (status < 0) error("error reading X-Ray table\n");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* erosita-high intrinsic energies */
   if (swift_memalign(
@@ -336,9 +337,9 @@ INLINE static void read_xray_table(struct xray_properties *xrays,
   dataset = H5Dopen(file_id, "erosita-high/energies_intrinsic", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->emissivity_erosita_high_intrinsic_energies);
-  if (status < 0) printf("error reading X-Ray table\n");
+  if (status < 0) error("error reading X-Ray table\n");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* ROSAT intrinsic energies */
   if (swift_memalign("xray_table_ROSAT_energies",
@@ -354,13 +355,13 @@ INLINE static void read_xray_table(struct xray_properties *xrays,
   dataset = H5Dopen(file_id, "ROSAT/energies_intrinsic", H5P_DEFAULT);
   status = H5Dread(dataset, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT,
                    xrays->emissivity_ROSAT_intrinsic_energies);
-  if (status < 0) printf("error reading X-Ray table\n");
+  if (status < 0) error("error reading X-Ray table\n");
   status = H5Dclose(dataset);
-  if (status < 0) printf("error closing dataset");
+  if (status < 0) error("error closing dataset");
 
   /* Close file */
   status = H5Fclose(file_id);
-  if (status < 0) printf("error closing file");
+  if (status < 0) error("error closing file");
 }
 
 /**
diff --git a/src/extra_io/EAGLE/extra_io.h b/src/extra_io/EAGLE/extra_io.h
index 538e0936068aea9cee49f6646d3209ada15c0482..bdcd2e4b9820d3fedaa57453f6fcf001f388e293 100644
--- a/src/extra_io/EAGLE/extra_io.h
+++ b/src/extra_io/EAGLE/extra_io.h
@@ -63,15 +63,15 @@ INLINE static int extra_io_write_particles(const struct part *parts,
                                            struct io_props *list,
                                            const int with_cosmology) {
 
-  list[0] = io_make_output_field_convert_part(
+  list[0] = io_make_physical_output_field_convert_part(
       "XrayPhotonLuminosities", DOUBLE, 3, UNIT_CONV_PHOTONS_PER_TIME, 0.f,
-      parts, xparts, convert_part_Xray_photons,
+      parts, xparts, /*can convert to comoving=*/0, convert_part_Xray_photons,
       "Intrinsic X-ray photon luminosities in various bands. This is 0 for "
       "star-forming particles.");
 
-  list[1] = io_make_output_field_convert_part(
+  list[1] = io_make_physical_output_field_convert_part(
       "XrayLuminosities", DOUBLE, 3, UNIT_CONV_POWER, 0.f, parts, xparts,
-      convert_part_Xray_energies,
+      /*can convert to comoving=*/0, convert_part_Xray_energies,
       "Intrinsic X-ray luminosities in various bands. This is 0 for "
       "star-forming particles.");
 
diff --git a/src/feedback/AGORA/feedback.c b/src/feedback/AGORA/feedback.c
index c0b62fb2eee8c7fc29a07ec76ccbd8bee9b2bcfd..60c6d64039cadfd2f2cdfea4ce52d37f954824e5 100644
--- a/src/feedback/AGORA/feedback.c
+++ b/src/feedback/AGORA/feedback.c
@@ -159,6 +159,9 @@ void feedback_will_do_feedback(
     const struct unit_system* us, const struct phys_const* phys_const,
     const integertime_t ti_current, const double time_base) {
 
+  /* quit if the birth_scale_factor or birth_time is negative */
+  if (sp->birth_scale_factor < 0.0 || sp->birth_time < 0.0) return;
+
   /* skip if the particle is idle for feedback (it already exploded) */
   if (sp->feedback_data.idle == 1) {
     sp->feedback_data.will_do_feedback = 0;
@@ -223,6 +226,10 @@ void feedback_will_do_feedback(
  */
 int feedback_is_active(const struct spart* sp, const struct engine* e) {
 
+  /* the particle is inactive if its birth_scale_factor or birth_time is
+   * negative */
+  if (sp->birth_scale_factor < 0.0 || sp->birth_time < 0.0) return 0;
+
   return sp->feedback_data.will_do_feedback;
 }
 
@@ -237,14 +244,18 @@ void feedback_init_spart(struct spart* sp) {
 }
 
 /**
- * @brief Reset the feedback field when the spart is not
- * in a correct state for feeedback_will_do_feedback.
+ * @brief Prepare the feedback fields after a star is born.
+ *
+ * This function is called in the functions sink_copy_properties_to_star() and
+ * star_formation_copy_properties().
  *
- * This function is called in the timestep task.
+ * @param sp The #spart to act upon.
+ * @param feedback_props The feedback perties to use.
+ * @param star_type The stellar particle type.
  */
 void feedback_init_after_star_formation(
-    struct spart* sp, const struct feedback_props* feedback_props) {
-  feedback_init_spart(sp);
+    struct spart* sp, const struct feedback_props* feedback_props,
+    const enum stellar_type star_type) {
 
   /* Zero the energy of supernovae */
   sp->feedback_data.energy_ejected = 0;
@@ -254,6 +265,10 @@ void feedback_init_after_star_formation(
 
   /* The particle is not idle */
   sp->feedback_data.idle = 0;
+
+  /* Give to the star its appropriate type: single star, continuous IMF star or
+     single population star */
+  sp->star_type = star_type;
 }
 
 /**
diff --git a/src/feedback/AGORA/feedback.h b/src/feedback/AGORA/feedback.h
index c215874d4e8130968bf5192cbb2167a270c17ffb..d90be8f842eff30c18975cd57606c02d269d8a8e 100644
--- a/src/feedback/AGORA/feedback.h
+++ b/src/feedback/AGORA/feedback.h
@@ -24,6 +24,7 @@
 #include "feedback_properties.h"
 #include "hydro_properties.h"
 #include "part.h"
+#include "stars.h"
 #include "units.h"
 
 /**
@@ -59,7 +60,8 @@ __attribute__((always_inline)) INLINE static void feedback_reset_part(
 void feedback_init_spart(struct spart* sp);
 
 void feedback_init_after_star_formation(
-    struct spart* sp, const struct feedback_props* feedback_props);
+    struct spart* sp, const struct feedback_props* feedback_props,
+    enum stellar_type star_type);
 
 /**
  * @brief Should we do feedback for this star?
diff --git a/src/feedback/EAGLE_thermal/feedback.h b/src/feedback/EAGLE_thermal/feedback.h
index fa546289fca1e3921f8fb1e87996fb78eac23dd8..aed0ab73934e46577e05953264ae075512f64dee 100644
--- a/src/feedback/EAGLE_thermal/feedback.h
+++ b/src/feedback/EAGLE_thermal/feedback.h
@@ -208,7 +208,8 @@ __attribute__((always_inline)) INLINE static void feedback_prepare_feedback(
     const int with_cosmology) {
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (sp->birth_time == -1.) error("Evolving a star particle that should not!");
+  if (sp->birth_time == -1. && dt > 0.)
+    error("Evolving a star particle that should not!");
 #endif
 
   /* Start by finishing the loops over neighbours */
@@ -217,7 +218,8 @@ __attribute__((always_inline)) INLINE static void feedback_prepare_feedback(
   const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
 
   sp->feedback_data.to_collect.ngb_rho *= h_inv_dim;
-  const float rho_inv = 1.f / sp->feedback_data.to_collect.ngb_rho;
+  const float rho = sp->feedback_data.to_collect.ngb_rho;
+  const float rho_inv = rho != 0.f ? 1.f / rho : 0.f;
   sp->feedback_data.to_collect.ngb_Z *= h_inv_dim * rho_inv;
 
   /* Compute amount of enrichment and feedback that needs to be done in this
diff --git a/src/feedback/GEAR/feedback.c b/src/feedback/GEAR/feedback.c
index ba63dbf4d9c83fa6594a47b3449896e88a1d0fa0..204477d474612d95419117d6d745844454c4330a 100644
--- a/src/feedback/GEAR/feedback.c
+++ b/src/feedback/GEAR/feedback.c
@@ -21,6 +21,7 @@
 #include "feedback.h"
 
 /* Local includes */
+#include "cooling.h"
 #include "cosmology.h"
 #include "engine.h"
 #include "error.h"
@@ -49,7 +50,7 @@ void feedback_update_part(struct part* p, struct xpart* xp,
   const struct pressure_floor_props* pressure_floor = e->pressure_floor_props;
 
   /* Turn off the cooling */
-  xp->cooling_data.time_last_event = e->time;
+  cooling_set_part_time_cooling_off(p, xp, e->time);
 
   /* Update mass */
   const float old_mass = hydro_get_mass(p);
@@ -172,6 +173,24 @@ void feedback_will_do_feedback(
     const struct unit_system* us, const struct phys_const* phys_const,
     const integertime_t ti_current, const double time_base) {
 
+  /* Zero the energy of supernovae */
+  sp->feedback_data.energy_ejected = 0;
+  sp->feedback_data.will_do_feedback = 0;
+
+  /* quit if the birth_scale_factor or birth_time is negative */
+  if (sp->birth_scale_factor < 0.0 || sp->birth_time < 0.0) return;
+
+  /* Pick the correct table. (if only one table, threshold is < 0) */
+  const float metal =
+      chemistry_get_star_total_iron_mass_fraction_for_feedback(sp);
+  const float threshold = feedback_props->metallicity_max_first_stars;
+
+  /* If metal < threshold, then  sp is a first star particle. */
+  const int is_first_star = metal < threshold;
+  const struct stellar_model* model =
+      is_first_star ? &feedback_props->stellar_model_first_stars
+                    : &feedback_props->stellar_model;
+
   /* Compute the times */
   double star_age_beg_step = 0;
   double dt_enrichment = 0;
@@ -179,35 +198,43 @@ void feedback_will_do_feedback(
   compute_time(sp, with_cosmology, cosmo, &star_age_beg_step, &dt_enrichment,
                &ti_begin, ti_current, time_base, time);
 
-  /* Zero the energy of supernovae */
-  sp->feedback_data.energy_ejected = 0;
-  sp->feedback_data.will_do_feedback = 0;
-
 #ifdef SWIFT_DEBUG_CHECKS
   if (sp->birth_time == -1.) error("Evolving a star particle that should not!");
   if (star_age_beg_step + dt_enrichment < 0) {
     error("Negative age for a star");
   }
 #endif
-
   /* Ensure that the age is positive (rounding errors) */
   const double star_age_beg_step_safe =
       star_age_beg_step < 0 ? 0 : star_age_beg_step;
 
-  /* Pick the correct table. (if only one table, threshold is < 0) */
-  const float metal =
-      chemistry_get_star_total_iron_mass_fraction_for_feedback(sp);
-  const float threshold = feedback_props->metallicity_max_first_stars;
-
-  const struct stellar_model* model =
-      metal < threshold ? &feedback_props->stellar_model_first_stars
-                        : &feedback_props->stellar_model;
-
-  /* Compute the stellar evolution including SNe energy */
-  stellar_evolution_evolve_spart(sp, model, cosmo, us, phys_const, ti_begin,
-                                 star_age_beg_step_safe, dt_enrichment);
+  /* A single star */
+  if (sp->star_type == single_star) {
+    /* If the star has completely exploded, do not continue. This will also
+       avoid NaN values in the liftetime if the mass is set to 0. Correction
+       (28.04.2024): A bug fix in the mass of the star (see stellar_evolution.c
+       in stellar_evolution_compute_X_feedback_properties, X=discrete,
+       continuous) has changed the mass of the star from 0 to
+       discrete_star_minimal_gravity_mass. Hence the fix is propagated here. */
+    if (sp->mass <= model->discrete_star_minimal_gravity_mass) {
+      return;
+    }
+
+    /* Now, compute the stellar evolution state for individual star particles.
+     */
+    stellar_evolution_evolve_individual_star(sp, model, cosmo, us, phys_const,
+                                             ti_begin, star_age_beg_step_safe,
+                                             dt_enrichment);
+  } else {
+    /* Compute the stellar evolution including SNe energy. This function treats
+       the case of particles representing the whole IMF (star_type =
+       star_population) and the particles representing only the continuous part
+       of the IMF (star_type = star_population_continuous_IMF) */
+    stellar_evolution_evolve_spart(sp, model, cosmo, us, phys_const, ti_begin,
+                                   star_age_beg_step_safe, dt_enrichment);
+  }
 
-  /* apply the energy efficiency factor */
+  /* Apply the energy efficiency factor */
   sp->feedback_data.energy_ejected *= feedback_props->supernovae_efficiency;
 
   /* Set the particle as doing some feedback */
@@ -222,6 +249,10 @@ void feedback_will_do_feedback(
  */
 int feedback_is_active(const struct spart* sp, const struct engine* e) {
 
+  /* the particle is inactive if its birth_scale_factor or birth_time is
+   * negative */
+  if (sp->birth_scale_factor < 0.0 || sp->birth_time < 0.0) return 0;
+
   return sp->feedback_data.will_do_feedback;
 }
 
@@ -256,13 +287,19 @@ void feedback_init_spart(struct spart* sp) {
 }
 
 /**
- * @brief Reset the feedback field when the spart is not
- * in a correct state for feeedback_will_do_feedback.
+ * @brief Prepare the feedback fields after a star is born.
+ *
+ * This function is called in the functions sink_copy_properties_to_star() and
+ * star_formation_copy_properties().
  *
- * This function is called in the timestep task.
+ * @param sp The #spart to act upon.
+ * @param feedback_props The feedback perties to use.
+ * @param star_type The stellar particle type.
  */
 void feedback_init_after_star_formation(
-    struct spart* sp, const struct feedback_props* feedback_props) {
+    struct spart* sp, const struct feedback_props* feedback_props,
+    const enum stellar_type star_type) {
+
   feedback_init_spart(sp);
 
   /* Zero the energy of supernovae */
@@ -270,6 +307,10 @@ void feedback_init_after_star_formation(
 
   /* Activate the feedback loop for the first step */
   sp->feedback_data.will_do_feedback = 1;
+
+  /* Give to the star its appropriate type: single star, continuous IMF star or
+     single population star */
+  sp->star_type = star_type;
 }
 
 /**
diff --git a/src/feedback/GEAR/feedback.h b/src/feedback/GEAR/feedback.h
index 4a81a53cc801773ddbe4bf8abcc5b7c3e08712cc..a27408a460b9ae66ec89bdab70a72f5b5bce834b 100644
--- a/src/feedback/GEAR/feedback.h
+++ b/src/feedback/GEAR/feedback.h
@@ -24,6 +24,7 @@
 #include "feedback_properties.h"
 #include "hydro_properties.h"
 #include "part.h"
+#include "stars.h"
 #include "stellar_evolution.h"
 #include "units.h"
 
@@ -50,7 +51,8 @@ double feedback_get_enrichment_timestep(const struct spart* sp,
 void feedback_init_spart(struct spart* sp);
 
 void feedback_init_after_star_formation(
-    struct spart* sp, const struct feedback_props* feedback_props);
+    struct spart* sp, const struct feedback_props* feedback_props,
+    enum stellar_type star_type);
 void feedback_reset_feedback(struct spart* sp,
                              const struct feedback_props* feedback_props);
 void feedback_first_init_spart(struct spart* sp,
diff --git a/src/feedback/GEAR/feedback_properties.h b/src/feedback/GEAR/feedback_properties.h
index 10d18896a9e58ca72d7c89236e0c593b0bf2ec22..080e3a042009241b162ca7bb38773d5e1a940785 100644
--- a/src/feedback/GEAR/feedback_properties.h
+++ b/src/feedback/GEAR/feedback_properties.h
@@ -147,7 +147,9 @@ __attribute__((always_inline)) INLINE static void feedback_props_init(
           "The metallicity threshold for the first stars is in mass fraction. "
           "It cannot be lower than 0.");
     }
-    message("Reading the stellar model for the first stars");
+    if (engine_rank == 0) {
+      message("Reading the stellar model for the first stars");
+    }
     parser_get_param_string(params, "GEARFeedback:yields_table_first_stars",
                             fp->stellar_model_first_stars.yields_table);
     stellar_evolution_props_init(&fp->stellar_model_first_stars, phys_const, us,
diff --git a/src/feedback/GEAR/hdf5_functions.h b/src/feedback/GEAR/hdf5_functions.h
index 45025b2cefbaa13e79f3d9e7379df8727b212953..1f2c06a22cf4b83a2f8f13669042cc34268a3566 100644
--- a/src/feedback/GEAR/hdf5_functions.h
+++ b/src/feedback/GEAR/hdf5_functions.h
@@ -52,7 +52,7 @@ io_read_string_array_attribute(hid_t grp, const char *name, char *data,
     error(
         "Error found a different number of elements than expected (%lli != "
         "%lli) in attribute %s",
-        count, number_element, name);
+        (long long)count, (long long)number_element, name);
   }
 
   /* Get the string length */
@@ -63,7 +63,8 @@ io_read_string_array_attribute(hid_t grp, const char *name, char *data,
 
   /* Check if the size is correct */
   if (sdim > size_per_element) {
-    error("Cannot read string longer than %lli in %s", size_per_element, name);
+    error("Cannot read string longer than %lli in %s",
+          (long long)size_per_element, name);
   }
 
   /* Allocate the temporary array */
diff --git a/src/feedback/GEAR/initial_mass_function.c b/src/feedback/GEAR/initial_mass_function.c
index 2c18b9d10b19c9cf7ca97f54f421341ffecd30da..6706fd35c7561148c36f313f3345e3d7829e695f 100644
--- a/src/feedback/GEAR/initial_mass_function.c
+++ b/src/feedback/GEAR/initial_mass_function.c
@@ -17,10 +17,10 @@
  *
  ******************************************************************************/
 
-/* Include header */
+/* local headers */
 #include "initial_mass_function.h"
 
-/* local headers */
+#include "exp10.h"
 #include "hdf5_functions.h"
 #include "stellar_evolution_struct.h"
 
@@ -33,8 +33,13 @@ float initial_mass_function_get_exponent(
 #ifdef SWIFT_DEBUG_CHECKS
   if (mass_max > imf->mass_max)
     error("Cannot have mass larger than the largest one in the IMF");
-  if (mass_min < imf->mass_min)
-    error("Cannot have mass smaller than the smallest one in the IMF");
+
+  /* 18.05.2024: This check is ill-defined. It needs to be improved.
+     For population II stars, no problem.
+     For population III stars, the snia->mass_min_progenitor can be smaller
+     than the minimal mass of the IMF, which causes the code to crash here. */
+  /* if (mass_min < imf->mass_min) */
+  /*   error("Cannot have mass smaller than the smallest one in the IMF"); */
   if (mass_max < mass_min) error("Cannot have mass_min larger than mass_max");
 #endif
 
@@ -61,12 +66,43 @@ float initial_mass_function_get_exponent(
 /** @brief Print the initial mass function */
 void initial_mass_function_print(const struct initial_mass_function *imf) {
 
+  if (engine_rank != 0) {
+    return;
+  }
+
   message("Number of parts: %i", imf->n_parts);
+  message("Number of stars per mass units: %g", imf->N_tot);
   message("Mass interval: [%g, %g]", imf->mass_min, imf->mass_max);
   for (int i = 0; i < imf->n_parts; i++) {
-    message("[%g, %g]: %.2g * m^{%g}", imf->mass_limits[i],
+    message("[%7.3f, %7.3f]: %5.2g * m^{%g}", imf->mass_limits[i],
             imf->mass_limits[i + 1], imf->coef[i], imf->exp[i]);
   }
+
+  message("Mass fractions");
+  for (int i = 0; i < imf->n_parts + 1; i++)
+    message("m=%7.3f: x=%5.3f", imf->mass_limits[i], imf->mass_fraction[i]);
+}
+
+/** @brief Sample the initial mass function */
+float initial_mass_function_sample(const struct initial_mass_function *imf,
+                                   float f) {
+
+  for (int i = 0; i < imf->n_parts; i++)
+    if (f < imf->mass_fraction[i + 1]) {
+      float pmin = pow(imf->mass_limits[i], imf->exp[i]);
+      float exponent = 1. / imf->exp[i];
+      float base_part_1 = imf->N_tot * imf->exp[i] / imf->coef[i];
+      base_part_1 *= (f - imf->mass_fraction[i]);
+      float base = base_part_1 + pmin;
+
+      /* The mathematical expression is:
+           (N_tot * exp_{imf, i} / coeff_{imf, i} * (f - mass_fraction_{imf, i})
+             + pmin)**(1/exp_{imf, i})
+      */
+      return pow(base, exponent);
+    }
+
+  return -1;
 }
 
 /**
@@ -244,11 +280,11 @@ float initial_mass_function_get_integral_xi(
 float initial_mass_function_get_imf(const struct initial_mass_function *imf,
                                     float m) {
 
-#ifdef SWIFT_DEBUG_CHECKS
+  /* Check the mass to be within the limits */
+
   if (m > imf->mass_max || m < imf->mass_min)
     error("Mass below or above limits expecting %g < %g < %g.", imf->mass_min,
           m, imf->mass_max);
-#endif
 
   for (int i = 0; i < imf->n_parts; i++) {
     if (m <= imf->mass_limits[i + 1]) {
@@ -262,8 +298,8 @@ float initial_mass_function_get_imf(const struct initial_mass_function *imf,
 };
 
 /**
- * @brief Compute the integral of the mass fraction of the initial mass
- * function.
+ * @brief Compute the the mass fraction (of stars) between m1 and m2 per mass
+ * unit.
  *
  * @param imf The #initial_mass_function.
  * @param m1 The lower mass to evaluate.
@@ -271,35 +307,97 @@ float initial_mass_function_get_imf(const struct initial_mass_function *imf,
  *
  * @return The integral of the mass fraction.
  */
-float initial_mass_function_get_integral_imf(
+float initial_mass_function_get_imf_mass_fraction(
     const struct initial_mass_function *imf, float m1, float m2) {
 
-  /* Ensure the masses to be withing the limits */
-  m1 = min(m1, imf->mass_max);
-  m1 = max(m1, imf->mass_min);
+  /* Check that m2 is > m1 */
+  if (m1 > m2)
+    error("Mass m1 (=%g) larger or equal to m2 (=%g). This is not allowed", m1,
+          m2);
 
-  m2 = min(m2, imf->mass_max);
-  m2 = max(m2, imf->mass_min);
+  /* Check the masses to be within the limits */
 
-  for (int i = 0; i < imf->n_parts; i++) {
-    if (m1 <= imf->mass_limits[i + 1]) {
-      if (m2 < imf->mass_limits[i] || m2 > imf->mass_limits[i + 1]) {
-        error(
-            "The code does not support the integration over multiple parts of "
-            "the IMF");
-      }
-      const float exp = imf->exp[i] + 1.;
-      return imf->coef[i] * (pow(m2, exp) - pow(m1, exp)) / exp;
-    }
+  if (m1 > imf->mass_max || m1 < imf->mass_min)
+    error("Mass m1 below or above limits expecting %g < %g < %g.",
+          imf->mass_min, m1, imf->mass_max);
+
+  if (m2 > imf->mass_max || m2 < imf->mass_min)
+    error("Mass m2 below or above limits expecting %g < %g < %g.",
+          imf->mass_min, m2, imf->mass_max);
+
+  const int n = imf->n_parts;
+  float integral = 0;
+
+  /* loop over all segments */
+  for (int i = 0; i < n; i++) {
+    float mmin = max(imf->mass_limits[i], m1);
+    float mmax = min(imf->mass_limits[i + 1], m2);
+
+    if (mmin < mmax) {
+      float p = imf->exp[i] + 1;
+      integral += (imf->coef[i] / p) * (pow(mmax, p) - pow(mmin, p));
+    } else /* nothing in this segment go to the next one */
+      continue;
+
+    if (m2 == mmax) /* nothing after this segment, stop */
+      break;
   }
 
-  error("Failed to find correct function part: %g, %g larger than mass max %g.",
-        m1, m2, imf->mass_max);
-  return 0.;
+  return integral;
+};
+
+/**
+ * @brief Compute the number fraction (of stars) between m1 and m2 per mass
+ * unit.
+ *
+ * @param imf The #initial_mass_function.
+ * @param m1 The lower mass to evaluate.
+ * @param m2 The upper mass to evaluate.
+ *
+ * @return The integral of the mass fraction.
+ */
+float initial_mass_function_get_imf_number_fraction(
+    const struct initial_mass_function *imf, float m1, float m2) {
+
+  /* Check that m2 is > m1 */
+  if (m1 > m2)
+    error("Mass m1 (=%g) larger or equal to m2 (=%g). This is not allowed", m1,
+          m2);
+
+  /* Check the masses to be within the limits */
+
+  if (m1 > imf->mass_max || m1 < imf->mass_min)
+    error("Mass m1 below or above limits expecting %g < %g < %g.",
+          imf->mass_min, m1, imf->mass_max);
+
+  if (m2 > imf->mass_max || m2 < imf->mass_min)
+    error("Mass m2 below or above limits expecting %g < %g < %g.",
+          imf->mass_min, m2, imf->mass_max);
+
+  const int n = imf->n_parts;
+  float integral = 0;
+
+  /* loop over all segments */
+  for (int i = 0; i < n; i++) {
+    float mmin = max(imf->mass_limits[i], m1);
+    float mmax = min(imf->mass_limits[i + 1], m2);
+
+    if (mmin < mmax) {
+      float p = imf->exp[i];
+      integral += (imf->coef[i] / p) * (pow(mmax, p) - pow(mmin, p));
+    } else /* nothing in this segment go to the next one */
+      continue;
+
+    if (m2 == mmax) /* nothing after this segment, stop */
+      break;
+  }
+
+  return integral;
 };
 
 /**
- * @brief Compute the coefficients of the initial mass function.
+ * @brief Compute the coefficients of the initial mass function
+ * as well as the mass fraction at the interface between IMF segments.
  *
  * @param imf The #initial_mass_function.
  */
@@ -332,6 +430,21 @@ void initial_mass_function_compute_coefficients(
   for (int i = 0; i < imf->n_parts; i++) {
     imf->coef[i] /= integral;
   }
+
+  /* Compute the total number of stars per mass unit */
+  imf->N_tot = initial_mass_function_get_imf_number_fraction(imf, imf->mass_min,
+                                                             imf->mass_max);
+
+  /* Allocate the memory for the mass fraction */
+  if ((imf->mass_fraction =
+           (float *)malloc(sizeof(float) * (imf->n_parts + 1))) == NULL)
+    error("Failed to allocate the IMF mass_fraction.");
+
+  for (int i = 0; i < imf->n_parts + 1; i++) {
+    imf->mass_fraction[i] = initial_mass_function_get_imf_number_fraction(
+                                imf, imf->mass_min, imf->mass_limits[i]) /
+                            imf->N_tot;
+  }
 }
 
 /**
@@ -411,6 +524,9 @@ void initial_mass_function_init(struct initial_mass_function *imf,
 
   /* Compute the coefficients */
   initial_mass_function_compute_coefficients(imf);
+
+  /* Print info */
+  initial_mass_function_print(imf);
 }
 
 /**
@@ -500,4 +616,63 @@ void initial_mass_function_clean(struct initial_mass_function *imf) {
 
   free(imf->coef);
   imf->coef = NULL;
+
+  free(imf->mass_fraction);
+  imf->mass_fraction = NULL;
+}
+
+/** @brief Sample a power law distribution (IMF)
+ *
+ * @param min_mass : the minimal IMF mass.
+ * @param max_mass : the maximal IMF mass.
+ * @param exp : the power law slope.
+ * @param x : a random number in the range [0, 1].
+ */
+INLINE double initial_mass_function_sample_power_law(double min_mass,
+                                                     double max_mass,
+                                                     double exp, double x) {
+
+  double pmin = pow(min_mass, exp);
+  double pmax = pow(max_mass, exp);
+  return pow(x * (pmax - pmin) + pmin, 1. / exp);
+}
+
+/** @brief Compute the mass of the continuous and discrete part of the
+ * IMF. Also compute the total mass of the IMF.
+ *
+ * This function is used when we deal with an IMF split into two parts,
+ * e.g. with the sink particles or the stellar feedback.
+ *
+ * Note: This function does not verify wheter it computes the masses for the
+ * first stars or not. You need to verify this before this function and pass the
+ * correct values to 'minimal_discrete_mass_Msun' and 'stellar_particle_mass'.
+ *
+ * Note 2: This function implicitly assumes M_sun since the IMF data
+ * structures handles the masses in M_sun.
+ *
+ * @param imf The #initial_mass_function.
+ * @param (return) M_continuous Mass of the continous part of the IMF.
+ * @param (return) M_discrete Mass of the discrete part of the IMF.
+ * @param (return) M_tot Total mass of the IMF.
+ */
+void initial_mass_function_compute_Mc_Md_Mtot(
+    const struct initial_mass_function *imf, double *M_continuous,
+    double *M_discrete, double *M_tot) {
+
+  /* f_continuous is the imf mass fraction of the continuous part (of the IMF).
+   */
+  const double f_continuous = initial_mass_function_get_imf_mass_fraction(
+      imf, imf->mass_min, imf->minimal_discrete_mass_Msun);
+
+  /* Determine Mc and Md the masses of the continuous and discrete parts of the
+     IMF, as well as Mtot the total mass of the IMF. */
+  if (f_continuous > 0) {
+    *M_tot = imf->stellar_particle_mass_Msun / f_continuous;
+    *M_discrete = *M_tot - imf->stellar_particle_mass_Msun;
+    *M_continuous = imf->stellar_particle_mass_Msun;
+  } else {
+    *M_tot = imf->stellar_particle_mass_Msun;
+    *M_discrete = *M_tot;
+    *M_continuous = 0;
+  }
 }
diff --git a/src/feedback/GEAR/initial_mass_function.h b/src/feedback/GEAR/initial_mass_function.h
index dc53dfd06594959c374686358c662f2f108345ad..302f1a344b2447d9bb72cf906750bcad9732ef1a 100644
--- a/src/feedback/GEAR/initial_mass_function.h
+++ b/src/feedback/GEAR/initial_mass_function.h
@@ -25,6 +25,8 @@
 float initial_mass_function_get_exponent(
     const struct initial_mass_function *imf, float mass_min, float mass_max);
 void initial_mass_function_print(const struct initial_mass_function *imf);
+float initial_mass_function_sample(const struct initial_mass_function *imf,
+                                   float f);
 
 void initial_mass_function_integrate(const struct initial_mass_function *imf,
                                      float *data, size_t count,
@@ -35,8 +37,11 @@ float initial_mass_function_get_integral_xi(
     const struct initial_mass_function *imf, float m1, float m2);
 float initial_mass_function_get_imf(const struct initial_mass_function *imf,
                                     float m);
-float initial_mass_function_get_integral_imf(
+float initial_mass_function_get_imf_mass_fraction(
     const struct initial_mass_function *imf, const float m1, const float m2);
+float initial_mass_function_get_imf_number_fraction(
+    const struct initial_mass_function *imf, const float m1, const float m2);
+
 void initial_mass_function_compute_coefficients(
     struct initial_mass_function *imf);
 
@@ -58,4 +63,12 @@ void initial_mass_function_restore(struct initial_mass_function *imf,
                                    const struct stellar_model *sm);
 
 void initial_mass_function_clean(struct initial_mass_function *imf);
+
+double initial_mass_function_sample_power_law(double min_mass, double max_mass,
+                                              double exp, double x);
+
+void initial_mass_function_compute_Mc_Md_Mtot(
+    const struct initial_mass_function *imf, double *M_continuous,
+    double *M_discrete, double *M_tot);
+
 #endif  // SWIFT_INITIAL_MASS_FUNCTION_GEAR_H
diff --git a/src/feedback/GEAR/stellar_evolution.c b/src/feedback/GEAR/stellar_evolution.c
index 8d945f67f8bc272828dc6e44715f91b00bcdc961..52f41cf5067fa2b685f3a6b91615ab9eee401c91 100644
--- a/src/feedback/GEAR/stellar_evolution.c
+++ b/src/feedback/GEAR/stellar_evolution.c
@@ -21,6 +21,7 @@
 #include "stellar_evolution.h"
 
 /* Include local headers */
+#include "exp10.h"
 #include "hdf5_functions.h"
 #include "initial_mass_function.h"
 #include "lifetime.h"
@@ -32,6 +33,8 @@
 #include <math.h>
 #include <stddef.h>
 
+#define DEFAULT_STAR_MINIMAL_GRAVITY_MASS_MSUN 1e-1
+
 /**
  * @brief Print the stellar model.
  *
@@ -80,6 +83,81 @@ int stellar_evolution_compute_integer_number_supernovae(
   return number_supernovae_i + (rand_sn < frac_sn);
 }
 
+/**
+ * @brief Update the #spart mass from the supernovae ejected mass.
+ *
+ * This function deals with each star_type.
+ *
+ * Note: This function is called by
+ * stellar_evolution_compute_discrete_feedback_properties() and
+ * stellar_evolution_compute_continuous_feedback_properties().
+ *
+ * @param sp The particle to act upon
+ * @param sm The #stellar_model structure.
+ */
+void stellar_evolution_sn_apply_ejected_mass(struct spart* restrict sp,
+                                             const struct stellar_model* sm) {
+  /* If a star is a discrete star */
+  if (sp->star_type == single_star) {
+    const int null_mass = (sp->mass == sp->feedback_data.mass_ejected);
+    const int negative_mass = (sp->mass < sp->feedback_data.mass_ejected);
+
+    if (null_mass) {
+      message("Star %lld (m_star = %e, m_ej = %e) completely exploded!", sp->id,
+              sp->mass, sp->feedback_data.mass_ejected);
+      /* If the star ejects all its mass (for very massive stars), give it a
+         zero mass so that we know it has exploded.
+         We do not remove the star from the simulation to keep track of its
+         properties, e.g. to check the IMF sampling (with sinks).
+
+         Bug fix (28.04.2024): The mass of the star should not be set to
+         0 because of gravity. So, we give some minimal value. */
+      sp->mass = sm->discrete_star_minimal_gravity_mass;
+
+      /* If somehow the star has a negative mass, we have a problem. */
+    } else if (negative_mass) {
+      error(
+          "(Discrete star) Negative mass (m_star = %e, m_ej = %e), skipping "
+          "current star: %lli",
+          sp->mass, sp->feedback_data.mass_ejected, sp->id);
+      /* Reset everything */
+      sp->feedback_data.number_snia = 0;
+      sp->feedback_data.number_snii = 0;
+      sp->feedback_data.mass_ejected = 0;
+
+      /* Reset energy to avoid injecting anything in the
+         runner_iact_nonsym_feedback_apply() */
+      sp->feedback_data.energy_ejected = 0;
+      return;
+    } else {
+      /* Update the mass */
+      sp->mass -= sp->feedback_data.mass_ejected;
+    }
+
+    /* If the star is the continuous part of the IMF or the enteire IMF */
+  } else {
+    /* Check if we can ejected the required amount of elements. */
+    const int negative_mass = (sp->mass <= sp->feedback_data.mass_ejected);
+    if (negative_mass) {
+      warning(
+          "(Continuous star) Negative mass (m_star = %e, m_ej = %e), skipping "
+          "current star: %lli",
+          sp->mass, sp->feedback_data.mass_ejected, sp->id);
+      /* Reset everything */
+      sp->feedback_data.number_snia = 0;
+      sp->feedback_data.number_snii = 0;
+      sp->feedback_data.mass_ejected = 0;
+
+      /* Reset energy to avoid injecting anything in the
+         runner_iact_nonsym_feedback_apply() */
+      sp->feedback_data.energy_ejected = 0;
+      return;
+    }
+    /* Update the mass */
+    sp->mass -= sp->feedback_data.mass_ejected;
+  }
+}
+
 /**
  * @brief Compute the feedback properties.
  *
@@ -121,19 +199,8 @@ void stellar_evolution_compute_continuous_feedback_properties(
   sp->feedback_data.mass_ejected = mass_frac_snii * sp->sf_data.birth_mass +
                                    mass_snia * phys_const->const_solar_mass;
 
-  /* Check if we can ejected the required amount of elements. */
-  const int negative_mass = sp->mass <= sp->feedback_data.mass_ejected;
-  if (negative_mass) {
-    message("Negative mass, skipping current star: %lli", sp->id);
-    /* Reset everything */
-    sp->feedback_data.number_snia = 0;
-    sp->feedback_data.number_snii = 0;
-    sp->feedback_data.mass_ejected = 0;
-    return;
-  }
-
-  /* Update the mass */
-  sp->mass -= sp->feedback_data.mass_ejected;
+  /* Removes the ejected mass from the star */
+  stellar_evolution_sn_apply_ejected_mass(sp, sm);
 
   /* Now deal with the metals */
 
@@ -214,19 +281,8 @@ void stellar_evolution_compute_discrete_feedback_properties(
   /* Transform into internal units */
   sp->feedback_data.mass_ejected *= phys_const->const_solar_mass;
 
-  /* Check if we can ejected the required amount of elements. */
-  const int negative_mass = sp->mass <= sp->feedback_data.mass_ejected;
-  if (negative_mass) {
-    message("Negative mass, skipping current star: %lli", sp->id);
-    /* Reset everything */
-    sp->feedback_data.number_snia = 0;
-    sp->feedback_data.number_snii = 0;
-    sp->feedback_data.mass_ejected = 0;
-    return;
-  }
-
-  /* Update the mass */
-  sp->mass -= sp->feedback_data.mass_ejected;
+  /* Removes the ejected mass from the star */
+  stellar_evolution_sn_apply_ejected_mass(sp, sm);
 
   /* Get the SNIa yields */
   const float* snia_yields = supernovae_ia_get_yields(&sm->snia);
@@ -262,6 +318,110 @@ void stellar_evolution_compute_discrete_feedback_properties(
   }
 }
 
+/**
+ * @brief Evolve an individual star represented by a #spart.
+ *
+ * This function compute the SN rate and yields before sending
+ * this information to a different MPI rank.
+ * It also compute the supernovae energy to be released by the
+ * star.
+ *
+ * Here I am using Myr-solar mass units internally in order to
+ * avoid numerical errors.
+ *
+ * Note: This function treats the case of single/individual stars.
+ *
+ * @param sp The particle to act upon
+ * @param sm The #stellar_model structure.
+ * @param cosmo The current cosmological model.
+ * @param us The unit system.
+ * @param phys_const The physical constants in the internal unit system.
+ * @param ti_begin The #integertime_t at the begining of the step.
+ * @param star_age_beg_step The age of the star at the star of the time-step in
+ * internal units.
+ * @param dt The time-step size of this star in internal units.
+ */
+void stellar_evolution_evolve_individual_star(
+    struct spart* restrict sp, const struct stellar_model* sm,
+    const struct cosmology* cosmo, const struct unit_system* us,
+    const struct phys_const* phys_const, const integertime_t ti_begin,
+    const double star_age_beg_step, const double dt) {
+
+  /* Check that this function is called for single_star only. */
+  if (sp->star_type != single_star) {
+    error("This function can only be called for single/individual star!");
+  }
+
+  /* Convert the inputs */
+  const double conversion_to_myr = phys_const->const_year * 1e6;
+  const double star_age_end_step_myr =
+      (star_age_beg_step + dt) / conversion_to_myr;
+  const double star_age_beg_step_myr = star_age_beg_step / conversion_to_myr;
+
+  /* Get the metallicity */
+  const float metallicity =
+      chemistry_get_star_total_metal_mass_fraction_for_feedback(sp);
+
+  const float log_mass = log10(sp->mass / phys_const->const_solar_mass);
+  const float lifetime_myr = pow(10, lifetime_get_log_lifetime_from_mass(
+                                         &sm->lifetime, log_mass, metallicity));
+
+  /* if the lifetime is outside the interval */
+  if ((lifetime_myr < star_age_beg_step_myr) ||
+      (lifetime_myr > star_age_end_step_myr))
+    return;
+
+  message(
+      "(%lld) lifetime_myr=%g %g star_age_beg_step=%g star_age_end_step=%g "
+      "(%g)",
+      sp->id, lifetime_myr, lifetime_myr * conversion_to_myr,
+      star_age_beg_step_myr, star_age_end_step_myr,
+      sp->mass / phys_const->const_solar_mass);
+
+  /* This is needed by stellar_evolution_compute_discrete_feedback_properties(),
+     but this is not used inside the function. */
+  const float m_init = 0;
+
+  /* Get the integer number of supernovae */
+  const int number_snia = 0;
+  const int number_snii = 1;
+
+  /* Save the number of supernovae */
+  sp->feedback_data.number_snia = 0;
+  sp->feedback_data.number_snii = number_snii;
+
+  /* this is needed for  stellar_evolution_compute_discrete_feedback_properties
+   */
+  const float m_beg_step = sp->mass / phys_const->const_solar_mass;
+  const float m_end_step = sp->mass / phys_const->const_solar_mass;
+  const float m_avg = 0.5 * (m_beg_step + m_end_step);
+
+  /* Compute the yields */
+  stellar_evolution_compute_discrete_feedback_properties(
+      sp, sm, phys_const, m_beg_step, m_end_step, m_init, number_snia,
+      number_snii);
+
+  /* Compute the supernovae energy associated to the stellar particle */
+
+  const float energy_conversion =
+      units_cgs_conversion_factor(us, UNIT_CONV_ENERGY) / 1e51;
+
+  /* initialize */
+  sp->feedback_data.energy_ejected = 0;
+
+  /* snia contribution */
+  const float snia_energy = sm->snia.energy_per_supernovae;
+  sp->feedback_data.energy_ejected +=
+      sp->feedback_data.number_snia * snia_energy;
+
+  /* snii contribution */
+  const float snii_energy =
+      supernovae_ii_get_energy_from_progenitor_mass(&sm->snii, m_avg) /
+      energy_conversion;
+  sp->feedback_data.energy_ejected +=
+      sp->feedback_data.number_snii * snii_energy;
+}
+
 /**
  * @brief Evolve the stellar properties of a #spart.
  *
@@ -273,6 +433,10 @@ void stellar_evolution_compute_discrete_feedback_properties(
  * Here I am using Myr-solar mass units internally in order to
  * avoid numerical errors.
  *
+ * Note: This function treats the case of particles representing the whole IMF
+ * (star_type = star_population) and the particles representing only the
+ * continuous part of the IMF (star_type = star_population_continuous_IMF).
+ *
  * @param sp The particle to act upon
  * @param sm The #stellar_model structure.
  * @param cosmo The current cosmological model.
@@ -289,6 +453,14 @@ void stellar_evolution_evolve_spart(
     const struct phys_const* phys_const, const integertime_t ti_begin,
     const double star_age_beg_step, const double dt) {
 
+  /* Check that this function is called for populations of stars and not
+     individual stars. */
+  if (sp->star_type == single_star) {
+    error(
+        "This function can only be called for sparts representing stars "
+        "populations!");
+  }
+
   /* Convert the inputs */
   const double conversion_to_myr = phys_const->const_year * 1e6;
   const double star_age_beg_step_myr = star_age_beg_step / conversion_to_myr;
@@ -318,6 +490,30 @@ void stellar_evolution_evolve_spart(
    */
   if (m_end_step >= m_beg_step) return;
 
+  /* Star particles representing only the continuous part of the IMF need a
+  special treatment. They do not contain stars above the mass that separate the
+  IMF into two parts (variable called minimal_discrete_mass_Msun in the sink
+  module). So, if m_end_step > minimal_discrete_mass_Msun, you don't do
+  feedback. Note that the sm structure contains different information for the
+  'first stars' and the 'late stars'. The right sm data is passed to this
+  function so we do not need any special treatment here. */
+  if (sp->star_type == star_population_continuous_IMF) {
+    /* If it's not time yet for feedback, exit. Notice that both masses are in
+      solar mass. */
+    if (m_end_step > sm->imf.minimal_discrete_mass_Msun) {
+      return;
+    }
+
+    /* If we are in a case where
+                m_beg_step > minimal_discrete_mass_Msun > m_end_step,
+       then we need to be careful. We don't want feedback from the discrete
+       part, only the continuous part. Hence, we need to update m_beg_step.
+    */
+    if (m_beg_step > sm->imf.minimal_discrete_mass_Msun) {
+      m_beg_step = sm->imf.minimal_discrete_mass_Msun;
+    }
+  }
+
   /* Check if the star can produce a supernovae */
   const int can_produce_snia =
       supernovae_ia_can_explode(&sm->snia, m_end_step, m_beg_step);
@@ -327,8 +523,14 @@ void stellar_evolution_evolve_spart(
   /* Is it possible to generate a supernovae? */
   if (!can_produce_snia && !can_produce_snii) return;
 
-  /* Compute the initial mass */
-  const float m_init = sp->sf_data.birth_mass / phys_const->const_solar_mass;
+  /* Compute the initial mass. The initial mass is different if the star
+     particle is of type 'star_population' or
+     'star_population_continuous_IMF'. The function call treats both cases. */
+  const float m_init =
+      stellar_evolution_compute_initial_mass(sp, sm, phys_const);
+
+  /* Then, for 'star_population_continuous_IMF', everything remain the same as
+     with the "old" 'star_population'! */
 
   /* Compute number of SNIa */
   float number_snia_f = 0;
@@ -434,6 +636,21 @@ int stellar_evolution_get_element_index(const struct stellar_model* sm,
   return -1;
 }
 
+/**
+ * @brief Get the solar abundance of the element .
+ *
+ * @param sm The #stellar_model.
+ * @param element_name The element name.
+ */
+float stellar_evolution_get_solar_abundance(const struct stellar_model* sm,
+                                            const char* element_name) {
+
+  int element_index = stellar_evolution_get_element_index(sm, element_name);
+  float solar_abundance = sm->solar_abundances[element_index];
+
+  return solar_abundance;
+}
+
 /**
  * @brief Read the name of all the elements present in the tables.
  *
@@ -487,6 +704,46 @@ void stellar_evolution_read_elements(struct stellar_model* sm,
   }
 }
 
+/**
+ * @brief Read the solar abundances.
+ *
+ * @param parameter_file The parsed parameter file.
+ * @param data The properties to initialise.
+ */
+void stellar_evolution_read_solar_abundances(struct stellar_model* sm,
+                                             struct swift_params* params) {
+
+#if defined(HAVE_HDF5)
+
+  /* Get the yields table */
+  char filename[DESCRIPTION_BUFFER_SIZE];
+  parser_get_param_string(params, "GEARFeedback:yields_table", filename);
+
+  /* Open file. */
+  hid_t file_id = H5Fopen(filename, H5F_ACC_RDONLY, H5P_DEFAULT);
+  if (file_id < 0) error("unable to open file %s.\n", filename);
+
+  /* Open group. */
+  hid_t group_id = H5Gopen(file_id, "Data", H5P_DEFAULT);
+  if (group_id < 0) error("unable to open group Data.\n");
+
+  /* Read the data */
+  io_read_array_attribute(group_id, "SolarMassAbundances", FLOAT,
+                          sm->solar_abundances, GEAR_CHEMISTRY_ELEMENT_COUNT);
+
+  /* Close group */
+  hid_t status = H5Gclose(group_id);
+  if (status < 0) error("error closing group.");
+
+  /* Close file */
+  status = H5Fclose(file_id);
+  if (status < 0) error("error closing file.");
+
+#else
+  message("Cannot read the solar abundances without HDF5");
+#endif
+}
+
 /**
  * @brief Initialize the global properties of the stellar evolution scheme.
  *
@@ -505,6 +762,9 @@ void stellar_evolution_props_init(struct stellar_model* sm,
   /* Read the list of elements */
   stellar_evolution_read_elements(sm, params);
 
+  /* Read the solar abundances */
+  stellar_evolution_read_solar_abundances(sm, params);
+
   /* Use the discrete yields approach? */
   sm->discrete_yields =
       parser_get_param_int(params, "GEARFeedback:discrete_yields");
@@ -521,6 +781,20 @@ void stellar_evolution_props_init(struct stellar_model* sm,
 
   /* Initialize the supernovae II model */
   supernovae_ii_init(&sm->snii, params, sm, us);
+
+  /* Initialize the minimal gravity mass for the stars */
+  /* const float default_star_minimal_gravity_mass_Msun = 1e-1; */
+  sm->discrete_star_minimal_gravity_mass = parser_get_opt_param_float(
+      params, "GEARFeedback:discrete_star_minimal_gravity_mass_Msun",
+      DEFAULT_STAR_MINIMAL_GRAVITY_MASS_MSUN);
+
+  /* Convert from M_sun to internal units */
+  sm->discrete_star_minimal_gravity_mass *= phys_const->const_solar_mass;
+
+  if (engine_rank == 0) {
+    message("discrete_star_minimal_gravity_mass: (internal units)          %e",
+            sm->discrete_star_minimal_gravity_mass);
+  }
 }
 
 /**
@@ -585,3 +859,38 @@ void stellar_evolution_clean(struct stellar_model* sm) {
   supernovae_ia_clean(&sm->snia);
   supernovae_ii_clean(&sm->snii);
 }
+
+/**
+ * @brief Computes the initial mass of a #spart. This function distinguishes
+ * between the stellar particle representing a whole IMF and the stellar
+ * particles representing only the continuous part.
+ *
+ * @param sp The particle for which we compute the initial mass.
+ * @param sm The #stellar_model structure.
+ * @param phys_const the physical constants in internal units.
+ * @param (return) m_init Initial mass of the star particle (in M_sun).
+ */
+float stellar_evolution_compute_initial_mass(
+    const struct spart* restrict sp, const struct stellar_model* sm,
+    const struct phys_const* phys_const) {
+
+  const struct initial_mass_function* imf = &sm->imf;
+  switch (sp->star_type) {
+    case star_population:
+      return sp->sf_data.birth_mass / phys_const->const_solar_mass;
+    case star_population_continuous_IMF: {
+      double M_IMF_tot, M_d_dummy, M_c_dummy;
+      initial_mass_function_compute_Mc_Md_Mtot(imf, &M_c_dummy, &M_d_dummy,
+                                               &M_IMF_tot);
+      /* No need to convert from internal units to M_sun because the masses are
+         already in solar masses (to avoid numerical errors) */
+      return M_IMF_tot;
+    }
+    case single_star:
+      return sp->sf_data.birth_mass / phys_const->const_solar_mass;
+    default: {
+      error("This star_type (%d) is not implemented!", sp->star_type);
+      return -1.0;
+    }
+  }
+}
diff --git a/src/feedback/GEAR/stellar_evolution.h b/src/feedback/GEAR/stellar_evolution.h
index 66f6e3375600ee96671c04ca225f2df33c585e23..a5afdf9079c07f42d673a6ec62b0954b8d3044f8 100644
--- a/src/feedback/GEAR/stellar_evolution.h
+++ b/src/feedback/GEAR/stellar_evolution.h
@@ -46,6 +46,12 @@ void stellar_evolution_compute_discrete_feedback_properties(
     const float m_end_step, const float m_init, const int number_snia,
     const int number_snii);
 
+void stellar_evolution_evolve_individual_star(
+    struct spart* restrict sp, const struct stellar_model* sm,
+    const struct cosmology* cosmo, const struct unit_system* us,
+    const struct phys_const* phys_const, const integertime_t ti_begin,
+    const double star_age_beg_step, const double dt);
+
 void stellar_evolution_evolve_spart(
     struct spart* restrict sp, const struct stellar_model* sm,
     const struct cosmology* cosmo, const struct unit_system* us,
@@ -56,9 +62,12 @@ const char* stellar_evolution_get_element_name(const struct stellar_model* sm,
                                                int i);
 int stellar_evolution_get_element_index(const struct stellar_model* sm,
                                         const char* element_name);
-
+float stellar_evolution_get_solar_abundance(const struct stellar_model* sm,
+                                            const char* element_name);
 void stellar_evolution_read_elements(struct stellar_model* sm,
                                      struct swift_params* params);
+void stellar_evolution_read_solar_abundances(struct stellar_model* sm,
+                                             struct swift_params* params);
 void stellar_evolution_props_init(struct stellar_model* sm,
                                   const struct phys_const* phys_const,
                                   const struct unit_system* us,
@@ -70,4 +79,7 @@ void stellar_evolution_restore(struct stellar_model* sm, FILE* stream);
 
 void stellar_evolution_clean(struct stellar_model* sm);
 
+float stellar_evolution_compute_initial_mass(
+    const struct spart* restrict sp, const struct stellar_model* sm,
+    const struct phys_const* phys_consts);
 #endif  // SWIFT_STELLAR_EVOLUTION_GEAR_H
diff --git a/src/feedback/GEAR/stellar_evolution_struct.h b/src/feedback/GEAR/stellar_evolution_struct.h
index 4a629fe8e22c0dcd4e6db2ad106d2e18ad179fa2..7103c277d3f85cd528675a6e079a405e0ae14516 100644
--- a/src/feedback/GEAR/stellar_evolution_struct.h
+++ b/src/feedback/GEAR/stellar_evolution_struct.h
@@ -39,13 +39,17 @@ struct initial_mass_function {
   /*! Mass limits between IMF parts (n_parts + 1 elements). */
   float *mass_limits;
 
+  /*! Mass fraction computed at the interface between two IMF parts (n_parts + 1
+   * elements). */
+  float *mass_fraction;
+
   /*! Exponent of each IMF parts (n_parts elements). */
   float *exp;
 
   /*! Coefficient of each IMF parts (n_parts elements). */
   float *coef;
 
-  /*! Number of parts in the function. */
+  /*! Number of parts (segments) in the function. */
   int n_parts;
 
   /*! Minimal mass contained in mass_limits, copied for more clarity. */
@@ -53,6 +57,19 @@ struct initial_mass_function {
 
   /*! Maximal mass contained in mass_limits, copied for more clarity. */
   float mass_max;
+
+  /*! Total number of stars (per mass unit) in the IMF. */
+  float N_tot;
+
+  /*! Probability to generate a star out of the continuous part of the IMF. */
+  float sink_Pc;
+
+  /*! Stellar mass of the continous part of the IMF (in solar mass). */
+  float stellar_particle_mass_Msun;
+
+  /*! Minimal mass of stars represented by discrete particles (in solar mass).
+   */
+  float minimal_discrete_mass_Msun;
 };
 
 /**
@@ -174,6 +191,9 @@ struct stellar_model {
   /*! Name of the different elements */
   char elements_name[GEAR_CHEMISTRY_ELEMENT_COUNT * GEAR_LABELS_SIZE];
 
+  /* Solar mass abundances read from the chemistry table */
+  float solar_abundances[GEAR_CHEMISTRY_ELEMENT_COUNT];
+
   /*! The initial mass function */
   struct initial_mass_function imf;
 
@@ -191,6 +211,19 @@ struct stellar_model {
 
   /* Filename of the yields table */
   char yields_table[FILENAME_BUFFER_SIZE];
+
+  /* Minimal gravity mass after a discrete star has completely exploded.
+
+     This will be the mass of the gpart's friend of the star. The mass of the
+     star will be 0 after it losses all its mass.
+
+     The purpose of this is to avoid zero mass for the gravitsy
+     computations. We keep the star so that we know it *existed* and we can
+     extract its properties at the end of a run. If we remove the star, then
+     we do not have any information about its existence.
+     However, since the star is dead/inexistent, the gravity mass must be small
+     so that it does not drastically alter the dynamics of the systems. */
+  float discrete_star_minimal_gravity_mass;
 };
 
 #endif  // SWIFT_STELLAR_EVOLUTION_STRUCT_GEAR_H
diff --git a/src/feedback/GEAR/supernovae_ia.c b/src/feedback/GEAR/supernovae_ia.c
index 91a41551d3cedad822cba5163cfc0e6d0525294c..b95269bdccc8aa862b833f5f23c555f6a95a1ccc 100644
--- a/src/feedback/GEAR/supernovae_ia.c
+++ b/src/feedback/GEAR/supernovae_ia.c
@@ -192,7 +192,7 @@ void supernovae_ia_read_yields(struct supernovae_ia *snia,
   io_read_array_attribute(group_id, "data", FLOAT, yields, nval);
 
   /* Read the labels */
-  char *labels = malloc(nval * GEAR_LABELS_SIZE);
+  char *labels = (char *)calloc(nval, GEAR_LABELS_SIZE);
   io_read_string_array_attribute(group_id, "elts", labels, nval,
                                  GEAR_LABELS_SIZE);
 
diff --git a/src/fof.c b/src/fof.c
index 50c2f726cb944a6677097e96c72c9b4b525592c2..8735202ee949191cd7f1e3ed35546f8e33806d0f 100644
--- a/src/fof.c
+++ b/src/fof.c
@@ -55,8 +55,17 @@
 #define UNION_BY_SIZE_OVER_MPI (1)
 #define FOF_COMPRESS_PATHS_MIN_LENGTH (2)
 
+/* The FoF policy we are running */
+int current_fof_linking_type;
+
+/* The FoF policy for particles attached to the main type */
+int current_fof_attach_type;
+
+/* The FoF policy for particles ignored altogether */
+int current_fof_ignore_type;
+
 /* Are we timing calculating group properties in the FOF? */
-//#define WITHOUT_GROUP_PROPS
+// #define WITHOUT_GROUP_PROPS
 
 /**
  * @brief Properties of a group used for black hole seeding
@@ -84,6 +93,27 @@ size_t node_offset;
 static integertime_t ti_current;
 #endif
 
+void fof_set_current_types(const struct fof_props *props) {
+
+  /* Initialize the FoF linking mode */
+  current_fof_linking_type = 0;
+  for (int i = 0; i < swift_type_count; ++i)
+    if (props->fof_linking_types[i]) {
+      current_fof_linking_type |= (1 << (i + 1));
+    }
+
+  /* Initialize the FoF attaching mode */
+  current_fof_attach_type = 0;
+  for (int i = 0; i < swift_type_count; ++i)
+    if (props->fof_attach_types[i]) {
+      current_fof_attach_type |= (1 << (i + 1));
+    }
+
+  /* Construct the combined mask of ignored particles */
+  current_fof_ignore_type =
+      ~(current_fof_linking_type | current_fof_attach_type);
+}
+
 /**
  * @brief Initialise the properties of the FOF code.
  *
@@ -148,6 +178,47 @@ void fof_init(struct fof_props *props, struct swift_params *params,
     props->seed_halo_mass *= phys_const->const_solar_mass;
   }
 
+  /* Read what particle types we want to run FOF on */
+  parser_get_param_int_array(params, "FOF:linking_types", swift_type_count,
+                             props->fof_linking_types);
+
+  /* Read what particle types we want to attach to FOF groups */
+  parser_get_param_int_array(params, "FOF:attaching_types", swift_type_count,
+                             props->fof_attach_types);
+
+  /* Check that there is something to do */
+  int sum = 0;
+  for (int i = 0; i < swift_type_count; ++i)
+    if (props->fof_linking_types[i]) sum++;
+  if (sum == 0) error("FOF must run on at least one type of particles!");
+
+  for (int i = 0; i < swift_type_count; ++i)
+    if (props->fof_linking_types[i] && props->fof_attach_types[i])
+      error("FOF can't use a type (%s) as both linking and attaching type!",
+            part_type_names[i]);
+
+  /* Set the current FOF types */
+  fof_set_current_types(props);
+
+  /* Report what we do */
+  if (engine_rank == 0) {
+    printf("FOF using the following types for linking:");
+    for (int i = 0; i < swift_type_count; ++i)
+      if (props->fof_linking_types[i]) printf("'%s' ", part_type_names[i]);
+    printf("\n");
+
+    printf("FOF using the following types for attaching:");
+    for (int i = 0; i < swift_type_count; ++i)
+      if (props->fof_attach_types[i]) printf("'%s' ", part_type_names[i]);
+    printf("\n");
+
+    printf("FOF ignoring the following types:");
+    for (int i = 0; i < swift_type_count; ++i)
+      if (current_fof_ignore_type & (1 << (i + 1)))
+        printf("'%s' ", part_type_names[i]);
+    printf("\n");
+  }
+
 #if defined(WITH_MPI) && defined(UNION_BY_SIZE_OVER_MPI)
   if (engine_rank == 0)
     message(
@@ -210,6 +281,25 @@ void fof_set_initial_group_index_mapper(void *map_data, int num_elements,
   }
 }
 
+/**
+ * @brief Mapper function to set the initial attach group indices.
+ *
+ * @param map_data The array of attach group indices.
+ * @param num_elements Chunk size.
+ * @param extra_data Pointer to first group index.
+ */
+void fof_set_initial_attach_index_mapper(void *map_data, int num_elements,
+                                         void *extra_data) {
+  size_t *attach_index = (size_t *)map_data;
+  size_t *attach_index_start = (size_t *)extra_data;
+
+  const ptrdiff_t offset = attach_index - attach_index_start;
+
+  for (int i = 0; i < num_elements; ++i) {
+    attach_index[i] = i + offset;
+  }
+}
+
 /**
  * @brief Mapper function to set the initial group sizes.
  *
@@ -226,6 +316,22 @@ void fof_set_initial_group_size_mapper(void *map_data, int num_elements,
   }
 }
 
+/**
+ * @brief Mapper function to set the initial distances.
+ *
+ * @param map_data The array of distance.
+ * @param num_elements Chunk size.
+ * @param extra_data N/A.
+ */
+void fof_set_initial_part_distances_mapper(void *map_data, int num_elements,
+                                           void *extra_data) {
+
+  float *distance = (float *)map_data;
+  for (int i = 0; i < num_elements; ++i) {
+    distance[i] = FLT_MAX;
+  }
+}
+
 /**
  * @brief Mapper function to set the initial group IDs.
  *
@@ -249,30 +355,38 @@ void fof_set_initial_group_id_mapper(void *map_data, int num_elements,
  * @brief Allocate the memory and initialise the arrays for a FOF calculation.
  *
  * @param s The #space to act on.
- * @param total_nr_DM_particles The total number of DM particles in the
- * simulation.
  * @param props The properties of the FOF structure.
  */
-void fof_allocate(const struct space *s, const long long total_nr_DM_particles,
-                  struct fof_props *props) {
+void fof_allocate(const struct space *s, struct fof_props *props) {
 
   const int verbose = s->e->verbose;
   const ticks total_tic = getticks();
 
   /* Start by computing the mean inter DM particle separation */
 
-  /* Collect the mass of the first non-background gpart */
+  /* Collect the mean mass of the non-background gpart */
   double high_res_DM_mass = 0.;
+  long long num_high_res_DM = 0;
   for (size_t i = 0; i < s->nr_gparts; ++i) {
     const struct gpart *gp = &s->gparts[i];
     if (gp->type == swift_type_dark_matter &&
         gp->time_bin != time_bin_inhibited &&
         gp->time_bin != time_bin_not_created) {
-      high_res_DM_mass = gp->mass;
-      break;
+      high_res_DM_mass += gp->mass;
+      num_high_res_DM++;
     }
   }
 
+#ifdef WITH_MPI
+  /* Gather the information from all ranks */
+  MPI_Allreduce(MPI_IN_PLACE, &high_res_DM_mass, 1, MPI_DOUBLE, MPI_SUM,
+                MPI_COMM_WORLD);
+  MPI_Allreduce(MPI_IN_PLACE, &num_high_res_DM, 1, MPI_LONG_LONG, MPI_SUM,
+                MPI_COMM_WORLD);
+#endif
+
+  high_res_DM_mass /= (double)num_high_res_DM;
+
   /* Are we using the aboslute value or the one derived from the mean
      inter-particle sepration? */
   if (props->l_x_absolute != -1.) {
@@ -334,6 +448,24 @@ void fof_allocate(const struct space *s, const long long total_nr_DM_particles,
                      s->nr_gparts * sizeof(size_t)) != 0)
     error("Failed to allocate list of particle group indices for FOF search.");
 
+  /* Allocate and initialise a group index array for attachables. */
+  if (swift_memalign("fof_attach_index", (void **)&props->attach_index, 64,
+                     s->nr_gparts * sizeof(size_t)) != 0)
+    error(
+        "Failed to allocate list of particle distances array for FOF search.");
+
+  /* Allocate and initialise a group index array for attachables. */
+  if (swift_memalign("fof_found_attach", (void **)&props->found_attachable_link,
+                     64, s->nr_gparts * sizeof(char)) != 0)
+    error(
+        "Failed to allocate list of particle distances array for FOF search.");
+
+  /* Allocate and initialise the closest distance array. */
+  if (swift_memalign("fof_distance", (void **)&props->distance_to_link, 64,
+                     s->nr_gparts * sizeof(float)) != 0)
+    error(
+        "Failed to allocate list of particle distances array for FOF search.");
+
   /* Allocate and initialise a group size array. */
   if (swift_memalign("fof_group_size", (void **)&props->group_size, 64,
                      s->nr_gparts * sizeof(size_t)) != 0)
@@ -352,6 +484,30 @@ void fof_allocate(const struct space *s, const long long total_nr_DM_particles,
 
   tic = getticks();
 
+  /* Set initial attach index */
+  threadpool_map(&s->e->threadpool, fof_set_initial_attach_index_mapper,
+                 props->attach_index, s->nr_gparts, sizeof(size_t),
+                 threadpool_auto_chunk_size, props->attach_index);
+
+  bzero(props->found_attachable_link, s->nr_gparts * sizeof(char));
+
+  if (verbose)
+    message("Setting initial attach index took: %.3f %s.",
+            clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  tic = getticks();
+
+  /* Set initial distances */
+  threadpool_map(&s->e->threadpool, fof_set_initial_part_distances_mapper,
+                 props->distance_to_link, s->nr_gparts, sizeof(float),
+                 threadpool_auto_chunk_size, NULL);
+
+  if (verbose)
+    message("Setting initial distances took: %.3f %s.",
+            clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  tic = getticks();
+
   /* Set initial group sizes */
   threadpool_map(&s->e->threadpool, fof_set_initial_group_size_mapper,
                  props->group_size, s->nr_gparts, sizeof(size_t),
@@ -491,7 +647,6 @@ __attribute__((always_inline)) INLINE static size_t fof_find_global(
 
 #endif /* WITH_MPI */
 
-#ifndef WITHOUT_GROUP_PROPS
 /**
  * @brief   Finds the local root ID of the group a particle exists in
  * when group_index contains globally unique identifiers -
@@ -526,7 +681,33 @@ __attribute__((always_inline)) INLINE static size_t fof_find_local(
   return root;
 #endif
 }
-#endif /* #ifndef WITHOUT_GROUP_PROPS */
+
+/**
+ * @brief Returns whether a #gpart is of the 'attachable' kind.
+ */
+__attribute__((always_inline)) INLINE static int gpart_is_attachable(
+    const struct gpart *gp) {
+
+  return current_fof_attach_type & (1 << (gp->type + 1));
+}
+
+/**
+ * @brief Returns whether a #gpart is of the 'linkable' kind.
+ */
+__attribute__((always_inline)) INLINE static int gpart_is_linkable(
+    const struct gpart *gp) {
+
+  return current_fof_linking_type & (1 << (gp->type + 1));
+}
+
+/**
+ * @brief Returns whether a #gpart is to be ignored by FOF.
+ */
+__attribute__((always_inline)) INLINE static int gpart_is_ignorable(
+    const struct gpart *gp) {
+
+  return current_fof_ignore_type & (1 << (gp->type + 1));
+}
 
 /**
  * @brief Finds the local root ID of the group a particle exists in.
@@ -596,7 +777,8 @@ __attribute__((always_inline)) INLINE static int atomic_update_root(
  * @param group_index The list of group roots.
  */
 __attribute__((always_inline)) INLINE static void fof_union(
-    size_t *root_i, const size_t root_j, size_t *group_index) {
+    size_t *restrict root_i, const size_t root_j,
+    size_t *restrict group_index) {
 
   int result = 0;
 
@@ -789,34 +971,36 @@ __attribute__((always_inline)) INLINE static void fof_compute_send_recv_offsets(
  */
 void fof_search_self_cell(const struct fof_props *props, const double l_x2,
                           const struct gpart *const space_gparts,
-                          struct cell *c) {
+                          const struct cell *c) {
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (c->split) error("Performing the FOF search at a non-leaf level!");
 #endif
 
   const size_t count = c->grav.count;
-  struct gpart *gparts = c->grav.parts;
+  const struct gpart *gparts = c->grav.parts;
 
   /* Index of particles in the global group list */
-  size_t *group_index = props->group_index;
+  size_t *const group_index = props->group_index;
 
   /* Make a list of particle offsets into the global gparts array. */
   size_t *const offset = group_index + (ptrdiff_t)(gparts - space_gparts);
 
+#ifdef SWIFT_DEBUG_CHECKS
   if (c->nodeID != engine_rank)
     error("Performing self FOF search on foreign cell.");
+#endif
 
   /* Loop over particles and find which particles belong in the same group. */
   for (size_t i = 0; i < count; i++) {
 
-    struct gpart *pi = &gparts[i];
+    const struct gpart *pi = &gparts[i];
 
     /* Ignore inhibited particles */
     if (pi->time_bin >= time_bin_inhibited) continue;
 
-    /* Ignore neutrinos */
-    if (pi->type == swift_type_neutrino) continue;
+    /* Check whether we ignore this particle type altogether */
+    if (gpart_is_ignorable(pi)) continue;
 
 #ifdef SWIFT_DEBUG_CHECKS
     if (pi->ti_drift != ti_current)
@@ -830,31 +1014,40 @@ void fof_search_self_cell(const struct fof_props *props, const double l_x2,
     /* Find the root of pi. */
     size_t root_i = fof_find(offset[i], group_index);
 
+    /* Get the nature of the linking */
+    const int is_link_i = gpart_is_linkable(pi);
+
     for (size_t j = i + 1; j < count; j++) {
 
-      struct gpart *pj = &gparts[j];
+      const struct gpart *pj = &gparts[j];
 
       /* Ignore inhibited particles */
       if (pj->time_bin >= time_bin_inhibited) continue;
 
-      /* Ignore neutrinos */
-      if (pj->type == swift_type_neutrino) continue;
+      /* Check whether we ignore this particle type altogether */
+      if (gpart_is_ignorable(pj)) continue;
+
+      /* Get the nature of the linking */
+      const int is_link_j = gpart_is_linkable(pj);
+
+      /* Both particles must be of the linking kind */
+      if (!(is_link_i && is_link_j)) continue;
 
 #ifdef SWIFT_DEBUG_CHECKS
       if (pj->ti_drift != ti_current)
         error("Running FOF on an un-drifted particle!");
 #endif
 
-      const double pjx = pj->x[0];
-      const double pjy = pj->x[1];
-      const double pjz = pj->x[2];
-
       /* Find the root of pj. */
       const size_t root_j = fof_find(offset[j], group_index);
 
       /* Skip particles in the same group. */
       if (root_i == root_j) continue;
 
+      const double pjx = pj->x[0];
+      const double pjy = pj->x[1];
+      const double pjz = pj->x[2];
+
       /* Compute the pairwise distance */
       float dx[3], r2 = 0.0f;
       dx[0] = pix - pjx;
@@ -866,7 +1059,7 @@ void fof_search_self_cell(const struct fof_props *props, const double l_x2,
       /* Hit or miss? */
       if (r2 < l_x2) {
 
-        /* Merge the groups */
+        /* Merge the groups` */
         fof_union(&root_i, root_j, group_index);
       }
     }
@@ -887,15 +1080,16 @@ void fof_search_self_cell(const struct fof_props *props, const double l_x2,
 void fof_search_pair_cells(const struct fof_props *props, const double dim[3],
                            const double l_x2, const int periodic,
                            const struct gpart *const space_gparts,
-                           struct cell *restrict ci, struct cell *restrict cj) {
+                           const struct cell *restrict ci,
+                           const struct cell *restrict cj) {
 
   const size_t count_i = ci->grav.count;
   const size_t count_j = cj->grav.count;
-  struct gpart *gparts_i = ci->grav.parts;
-  struct gpart *gparts_j = cj->grav.parts;
+  const struct gpart *gparts_i = ci->grav.parts;
+  const struct gpart *gparts_j = cj->grav.parts;
 
   /* Index of particles in the global group list */
-  size_t *group_index = props->group_index;
+  size_t *const group_index = props->group_index;
 
   /* Make a list of particle offsets into the global gparts array. */
   size_t *const offset_i = group_index + (ptrdiff_t)(gparts_i - space_gparts);
@@ -928,13 +1122,13 @@ void fof_search_pair_cells(const struct fof_props *props, const double dim[3],
   /* Loop over particles and find which particles belong in the same group. */
   for (size_t i = 0; i < count_i; i++) {
 
-    struct gpart *restrict pi = &gparts_i[i];
+    const struct gpart *restrict pi = &gparts_i[i];
 
     /* Ignore inhibited particles */
     if (pi->time_bin >= time_bin_inhibited) continue;
 
-    /* Ignore neutrinos */
-    if (pi->type == swift_type_neutrino) continue;
+    /* Check whether we ignore this particle type altogether */
+    if (gpart_is_ignorable(pi)) continue;
 
 #ifdef SWIFT_DEBUG_CHECKS
     if (pi->ti_drift != ti_current)
@@ -948,15 +1142,24 @@ void fof_search_pair_cells(const struct fof_props *props, const double dim[3],
     /* Find the root of pi. */
     size_t root_i = fof_find(offset_i[i], group_index);
 
+    /* Get the nature of the linking */
+    const int is_link_i = gpart_is_linkable(pi);
+
     for (size_t j = 0; j < count_j; j++) {
 
-      struct gpart *restrict pj = &gparts_j[j];
+      const struct gpart *restrict pj = &gparts_j[j];
 
       /* Ignore inhibited particles */
       if (pj->time_bin >= time_bin_inhibited) continue;
 
-      /* Ignore neutrinos */
-      if (pj->type == swift_type_neutrino) continue;
+      /* Check whether we ignore this particle type altogether */
+      if (gpart_is_ignorable(pj)) continue;
+
+      /* Get the nature of the linking */
+      const int is_link_j = gpart_is_linkable(pj);
+
+      /* At least one of the particles has to be of linking type */
+      if (!(is_link_i && is_link_j)) continue;
 
 #ifdef SWIFT_DEBUG_CHECKS
       if (pj->ti_drift != ti_current)
@@ -973,8 +1176,8 @@ void fof_search_pair_cells(const struct fof_props *props, const double dim[3],
       const double pjy = pj->x[1];
       const double pjz = pj->x[2];
 
-      /* Compute pairwise distance, remembering to account for boundary
-       * conditions. */
+      /* Compute pairwise distance (periodic BCs were accounted
+       for by the shift vector) */
       float dx[3], r2 = 0.0f;
       dx[0] = pix - pjx;
       dx[1] = piy - pjy;
@@ -992,6 +1195,50 @@ void fof_search_pair_cells(const struct fof_props *props, const double dim[3],
   }
 }
 
+#ifdef WITH_MPI
+
+/**
+ * @brief Add a local<->foreign pair in range to the list of links
+ *
+ * Possibly reallocates the local_group_links if we run out of space.
+ */
+static INLINE void add_foreign_link_to_list(
+    int *local_link_count, int *group_links_size, struct fof_mpi **group_links,
+    struct fof_mpi **local_group_links, const size_t root_i,
+    const size_t root_j, const size_t size_i, const size_t size_j) {
+
+  /* If the group_links array is not big enough re-allocate it. */
+  if (*local_link_count + 1 > *group_links_size) {
+
+    const int new_size = 2 * (*group_links_size);
+
+    *group_links_size = new_size;
+
+    (*group_links) = (struct fof_mpi *)realloc(
+        *group_links, new_size * sizeof(struct fof_mpi));
+
+    /* Reset the local pointer */
+    (*local_group_links) = *group_links;
+
+    message("Re-allocating local group links from %d to %d elements.",
+            *local_link_count, new_size);
+
+    if (new_size < 0) error("Overflow in size of list of foreign links");
+  }
+
+  /* Store the particle group properties for communication. */
+
+  (*local_group_links)[*local_link_count].group_i = root_i;
+  (*local_group_links)[*local_link_count].group_i_size = size_i;
+
+  (*local_group_links)[*local_link_count].group_j = root_j;
+  (*local_group_links)[*local_link_count].group_j_size = size_j;
+
+  (*local_link_count)++;
+}
+
+#endif
+
 /* Perform a FOF search between a local and foreign cell using the Union-Find
  * algorithm. Store any links found between particles.*/
 void fof_search_pair_cells_foreign(
@@ -1008,15 +1255,16 @@ void fof_search_pair_cells_foreign(
   const struct gpart *gparts_j = cj->grav.parts;
 
   /* Get local pointers */
-  size_t *group_index = props->group_index;
-  size_t *group_size = props->group_size;
+  const size_t *restrict group_index = props->group_index;
+  const size_t *restrict group_size = props->group_size;
 
   /* Values local to this function to avoid dereferencing */
   struct fof_mpi *local_group_links = *group_links;
   int local_link_count = *link_count;
 
   /* Make a list of particle offsets into the global gparts array. */
-  size_t *const offset_i = group_index + (ptrdiff_t)(gparts_i - space_gparts);
+  const size_t *const offset_i =
+      group_index + (ptrdiff_t)(gparts_i - space_gparts);
 
 #ifdef SWIFT_DEBUG_CHECKS
 
@@ -1055,8 +1303,8 @@ void fof_search_pair_cells_foreign(
     /* Ignore inhibited particles */
     if (pi->time_bin >= time_bin_inhibited) continue;
 
-    /* Ignore neutrinos */
-    if (pi->type == swift_type_neutrino) continue;
+    /* Check whether we ignore this particle type altogether */
+    if (gpart_is_ignorable(pi)) continue;
 
 #ifdef SWIFT_DEBUG_CHECKS
     if (pi->ti_drift != ti_current)
@@ -1071,6 +1319,9 @@ void fof_search_pair_cells_foreign(
     const size_t root_i =
         fof_find_global(offset_i[i] - node_offset, group_index, nr_gparts);
 
+    /* Get the nature of the linking */
+    const int is_link_i = gpart_is_linkable(pi);
+
     for (size_t j = 0; j < count_j; j++) {
 
       const struct gpart *pj = &gparts_j[j];
@@ -1078,8 +1329,14 @@ void fof_search_pair_cells_foreign(
       /* Ignore inhibited particles */
       if (pj->time_bin >= time_bin_inhibited) continue;
 
-      /* Ignore neutrinos */
-      if (pj->type == swift_type_neutrino) continue;
+      /* Check whether we ignore this particle type altogether */
+      if (gpart_is_ignorable(pj)) continue;
+
+      /* Get the nature of the linking */
+      const int is_link_j = gpart_is_linkable(pj);
+
+      /* Only consider linkable<->linkable pairs */
+      if (!(is_link_i && is_link_j)) continue;
 
 #ifdef SWIFT_DEBUG_CHECKS
       if (pj->ti_drift != ti_current)
@@ -1090,8 +1347,8 @@ void fof_search_pair_cells_foreign(
       const double pjy = pj->x[1];
       const double pjz = pj->x[2];
 
-      /* Compute pairwise distance, remembering to account for boundary
-       * conditions. */
+      /* Compute pairwise distance (periodic BCs were accounted
+       for by the shift vector) */
       float dx[3], r2 = 0.0f;
       dx[0] = pix - pjx;
       dx[1] = piy - pjy;
@@ -1102,47 +1359,19 @@ void fof_search_pair_cells_foreign(
       /* Hit or miss? */
       if (r2 < l_x2) {
 
-        int found = 0;
-
         /* Check that the links have not already been added to the list. */
         for (int l = 0; l < local_link_count; l++) {
-          if ((local_group_links)[l].group_i == root_i &&
-              (local_group_links)[l].group_j == pj->fof_data.group_id) {
-            found = 1;
-            break;
+          if (local_group_links[l].group_i == root_i &&
+              local_group_links[l].group_j == pj->fof_data.group_id) {
+            continue;
           }
         }
 
-        if (!found) {
-
-          /* If the group_links array is not big enough re-allocate it. */
-          if (local_link_count + 1 > *group_links_size) {
-
-            const int new_size = 2 * (*group_links_size);
-
-            *group_links_size = new_size;
-
-            (*group_links) = (struct fof_mpi *)realloc(
-                *group_links, new_size * sizeof(struct fof_mpi));
-
-            /* Reset the local pointer */
-            local_group_links = *group_links;
-
-            message("Re-allocating local group links from %d to %d elements.",
-                    local_link_count, new_size);
-          }
-
-          /* Store the particle group properties for communication. */
-          local_group_links[local_link_count].group_i = root_i;
-          local_group_links[local_link_count].group_i_size =
-              group_size[root_i - node_offset];
-
-          local_group_links[local_link_count].group_j = pj->fof_data.group_id;
-          local_group_links[local_link_count].group_j_size =
-              pj->fof_data.group_size;
-
-          local_link_count++;
-        }
+        /* Add a possible link to the list */
+        add_foreign_link_to_list(
+            &local_link_count, group_links_size, group_links,
+            &local_group_links, root_i, pj->fof_data.group_id,
+            group_size[root_i - node_offset], pj->fof_data.group_size);
       }
     }
   }
@@ -1313,120 +1542,658 @@ void rec_fof_search_self(const struct fof_props *props, const double dim[3],
     fof_search_self_cell(props, search_r2, space_gparts, c);
 }
 
-/* Mapper function to atomically update the group size array. */
-void fof_update_group_size_mapper(hashmap_key_t key, hashmap_value_t *value,
-                                  void *data) {
-
-  size_t *group_size = (size_t *)data;
-
-  /* Use key to index into group size array. */
-  atomic_add(&group_size[key], value->value_st);
-}
-
 /**
- * @brief Mapper function to calculate the group sizes.
+ * @brief Perform the attaching operation using union-find on a given leaf-cell
  *
- * @param map_data An array of #gpart%s.
- * @param num_elements Chunk size.
- * @param extra_data Pointer to a #space.
+ * @param props The properties fof the FOF scheme.
+ * @param l_x2 The square of the FOF linking length.
+ * @param space_gparts The start of the #gpart array in the #space structure.
+ * @param nr_gparts The number of #gpart in the local #space structure.
+ * @param c The #cell in which to perform FOF.
  */
-void fof_calc_group_size_mapper(void *map_data, int num_elements,
-                                void *extra_data) {
+void fof_attach_self_cell(const struct fof_props *props, const double l_x2,
+                          const struct gpart *const space_gparts,
+                          const size_t nr_gparts, const struct cell *c) {
 
-  /* Retrieve mapped data. */
-  struct space *s = (struct space *)extra_data;
-  struct gpart *gparts = (struct gpart *)map_data;
-  size_t *group_index = s->e->fof_properties->group_index;
-  size_t *group_size = s->e->fof_properties->group_size;
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->split) error("Performing the FOF search at a non-leaf level!");
+#endif
 
-  /* Offset into gparts array. */
-  ptrdiff_t gparts_offset = (ptrdiff_t)(gparts - s->gparts);
-  size_t *const group_index_offset = group_index + gparts_offset;
+  const size_t count = c->grav.count;
+  struct gpart *gparts = (struct gpart *)c->grav.parts;
 
-  /* Create hash table. */
-  hashmap_t map;
-  hashmap_init(&map);
+  /* Make a list of particle offsets into the global gparts array. */
+  size_t *const group_index = props->group_index;
+#ifndef WITH_MPI
+  size_t *const index_offset = group_index + (ptrdiff_t)(gparts - space_gparts);
+#endif
 
-  /* Loop over particles and find which cells are in range of each other to
-   * perform the FOF search. */
-  for (int ind = 0; ind < num_elements; ind++) {
+  size_t *const attach_index = props->attach_index;
+  size_t *const attach_offset =
+      attach_index + (ptrdiff_t)(gparts - space_gparts);
 
-    hashmap_key_t root =
-        (hashmap_key_t)fof_find(group_index_offset[ind], group_index);
-    const size_t gpart_index = gparts_offset + ind;
+  char *const found_attach_index = props->found_attachable_link;
+  char *const found_attach_offset =
+      found_attach_index + (ptrdiff_t)(gparts - space_gparts);
 
-    /* Only add particles which aren't the root of a group. Stops groups of size
-     * 1 being added to the hash table. */
-    if (root != gpart_index) {
-      hashmap_value_t *size = hashmap_get(&map, root);
+  /* Distances of particles in the global list */
+  float *const offset_dist =
+      props->distance_to_link + (ptrdiff_t)(gparts - space_gparts);
 
-      if (size != NULL)
-        (*size).value_st++;
-      else
-        error("Couldn't find key (%zu) or create new one.", root);
-    }
-  }
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->nodeID != engine_rank)
+    error("Performing self FOF search on foreign cell.");
+#endif
 
-  /* Update the group size array. */
-  if (map.size > 0)
-    hashmap_iterate(&map, fof_update_group_size_mapper, group_size);
+  /* Loop over particles and find which particles belong in the same group. */
+  for (size_t i = 0; i < count; i++) {
 
-  hashmap_free(&map);
-}
+    struct gpart *pi = &gparts[i];
 
-/* Mapper function to atomically update the group mass array. */
-static INLINE void fof_update_group_mass_mapper(hashmap_key_t key,
-                                                hashmap_value_t *value,
-                                                void *data) {
+    /* Ignore inhibited particles */
+    if (pi->time_bin >= time_bin_inhibited) continue;
 
-  double *group_mass = (double *)data;
+    /* Check whether we ignore this particle type altogether */
+    if (gpart_is_ignorable(pi)) continue;
 
-  /* Use key to index into group mass array. */
-  atomic_add_d(&group_mass[key], value->value_dbl);
-}
+#ifdef SWIFT_DEBUG_CHECKS
+    if (pi->ti_drift != ti_current)
+      error("Running FOF on an un-drifted particle!");
+#endif
 
-/**
- * @brief Mapper function to calculate the group masses.
- *
- * @param map_data An array of #gpart%s.
- * @param num_elements Chunk size.
- * @param extra_data Pointer to a #space.
- */
-void fof_calc_group_mass_mapper(void *map_data, int num_elements,
-                                void *extra_data) {
+    const double pix = pi->x[0];
+    const double piy = pi->x[1];
+    const double piz = pi->x[2];
 
-  /* Retrieve mapped data. */
-  struct space *s = (struct space *)extra_data;
-  struct gpart *gparts = (struct gpart *)map_data;
-  double *group_mass = s->e->fof_properties->group_mass;
-  const size_t group_id_default = s->e->fof_properties->group_id_default;
-  const size_t group_id_offset = s->e->fof_properties->group_id_offset;
+    /* Find the root of pi. */
+#ifdef WITH_MPI
+    const size_t root_i = fof_find_global(
+        i + (ptrdiff_t)(gparts - space_gparts), group_index, nr_gparts);
+#else
+    const size_t root_i = fof_find(index_offset[i], group_index);
+#endif
 
-  /* Create hash table. */
-  hashmap_t map;
-  hashmap_init(&map);
+    /* Get the nature of the linking */
+    const int is_link_i = gpart_is_linkable(pi);
+    const int is_attach_i = gpart_is_attachable(pi);
 
-  /* Loop over particles and increment the group mass for groups above
-   * min_group_size. */
-  for (int ind = 0; ind < num_elements; ind++) {
+#ifdef SWIFT_DEBUG_CHECKS
+    if (is_link_i && is_attach_i)
+      error("Particle cannot be both linkable and attachable!");
+#endif
 
-    /* Only check groups above the minimum size. */
-    if (gparts[ind].fof_data.group_id != group_id_default) {
+    for (size_t j = i + 1; j < count; j++) {
 
-      hashmap_key_t index = gparts[ind].fof_data.group_id - group_id_offset;
-      hashmap_value_t *data = hashmap_get(&map, index);
+      struct gpart *pj = &gparts[j];
 
-      /* Update group mass */
-      if (data != NULL)
-        (*data).value_dbl += gparts[ind].mass;
-      else
-        error("Couldn't find key (%zu) or create new one.", index);
+      /* Ignore inhibited particles */
+      if (pj->time_bin >= time_bin_inhibited) continue;
+
+      /* Check whether we ignore this particle type altogether */
+      if (gpart_is_ignorable(pj)) continue;
+
+      /* Get the nature of the linking */
+      const int is_link_j = gpart_is_linkable(pj);
+      const int is_attach_j = gpart_is_attachable(pj);
+
+#ifdef SWIFT_DEBUG_CHECKS
+      if (is_link_j && is_attach_j)
+        error("Particle cannot be both linkable and attachable!");
+#endif
+
+      /* We only want link<->attach pairs */
+      if (is_attach_i && is_attach_j) continue;
+      if (is_link_i && is_link_j) continue;
+
+#ifdef SWIFT_DEBUG_CHECKS
+      if (pj->ti_drift != ti_current)
+        error("Running FOF on an un-drifted particle!");
+#endif
+
+        /* Find the root of pi. */
+#ifdef WITH_MPI
+      const size_t root_j = fof_find_global(
+          j + (ptrdiff_t)(gparts - space_gparts), group_index, nr_gparts);
+#else
+      const size_t root_j = fof_find(index_offset[j], group_index);
+#endif
+
+      const double pjx = pj->x[0];
+      const double pjy = pj->x[1];
+      const double pjz = pj->x[2];
+
+      /* Compute the pairwise distance */
+      float dx[3], r2 = 0.0f;
+      dx[0] = pix - pjx;
+      dx[1] = piy - pjy;
+      dx[2] = piz - pjz;
+
+      for (int k = 0; k < 3; k++) r2 += dx[k] * dx[k];
+
+      /* Hit or miss? */
+      if (r2 < l_x2) {
+
+        /* Now that we are within the linking length,
+         * decide what to do based on linking types */
+
+        if (is_link_i && is_link_j) {
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Fundamental logic error!");
+#endif
+        } else if (is_link_i && is_attach_j) {
+
+          /* We got a linkable and an attachable.
+           * See whether it is closer and if so re-link.
+           * This is safe to do as the attachables are never roots and
+           * nothing is attached to them */
+          const float dist = sqrtf(r2);
+          if (dist < offset_dist[j]) {
+
+            /* Store the new min dist */
+            offset_dist[j] = dist;
+
+            /* Store the current best root */
+            attach_offset[j] = root_i;
+            found_attach_offset[j] = 1;
+          }
+
+        } else if (is_link_j && is_attach_i) {
+
+          /* We got a linkable and an attachable.
+           * See whether it is closer and if so re-link.
+           * This is safe to do as the attachables are never roots and
+           * nothing is attached to them */
+          const float dist = sqrtf(r2);
+          if (dist < offset_dist[i]) {
+
+            /* Store the new min dist */
+            offset_dist[i] = dist;
+
+            /* Store the current best root */
+            attach_offset[i] = root_j;
+            found_attach_offset[i] = 1;
+          }
+
+        } else {
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Fundamental logic error!");
+#endif
+        }
+      }
+    }
+  }
+}
+
+/**
+ * @brief Perform the attaching operation using union-find between two cells
+ *
+ * @param props The properties fof the FOF scheme.
+ * @param dim The dimension of the simulation volume.
+ * @param l_x2 The square of the FOF linking length.
+ * @param periodic Are we using periodic BCs?
+ * @param space_gparts The start of the #gpart array in the #space structure.
+ * @param nr_gparts The number of #gpart in the local #space structure.
+ * @param ci The first #cell in which to perform FOF.
+ * @param cj The second #cell in which to perform FOF.
+ * @param ci_local Is the #cell ci on the local MPI rank?
+ * @param cj_local Is the #cell cj on the local MPI rank?
+ */
+void fof_attach_pair_cells(const struct fof_props *props, const double dim[3],
+                           const double l_x2, const int periodic,
+                           const struct gpart *const space_gparts,
+                           const size_t nr_gparts,
+                           const struct cell *restrict ci,
+                           const struct cell *restrict cj, const int ci_local,
+                           const int cj_local) {
+
+  const size_t count_i = ci->grav.count;
+  const size_t count_j = cj->grav.count;
+  struct gpart *gparts_i = (struct gpart *)ci->grav.parts;
+  struct gpart *gparts_j = (struct gpart *)cj->grav.parts;
+
+  /* Index of particles in the global group list */
+  size_t *const group_index = props->group_index;
+
+  /* Make a list of particle offsets into the global gparts array. */
+  size_t *const index_offset_i =
+      group_index + (ptrdiff_t)(gparts_i - space_gparts);
+  size_t *const index_offset_j =
+      group_index + (ptrdiff_t)(gparts_j - space_gparts);
+
+  size_t *const attach_offset_i =
+      props->attach_index + (ptrdiff_t)(gparts_i - space_gparts);
+  size_t *const attach_offset_j =
+      props->attach_index + (ptrdiff_t)(gparts_j - space_gparts);
+
+  char *const found_attach_offset_i =
+      props->found_attachable_link + (ptrdiff_t)(gparts_i - space_gparts);
+  char *const found_attach_offset_j =
+      props->found_attachable_link + (ptrdiff_t)(gparts_j - space_gparts);
+
+  /* Distances of particles in the global list */
+  float *const offset_dist_i =
+      props->distance_to_link + (ptrdiff_t)(gparts_i - space_gparts);
+  float *const offset_dist_j =
+      props->distance_to_link + (ptrdiff_t)(gparts_j - space_gparts);
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (index_offset_j > index_offset_i &&
+      (index_offset_j < index_offset_i + count_i) && (ci->nodeID == cj->nodeID))
+    error("Overlapping cells");
+  if (index_offset_i > index_offset_j &&
+      (index_offset_i < index_offset_j + count_j) && (ci->nodeID == cj->nodeID))
+    error("Overlapping cells");
+#endif
+
+  /* Account for boundary conditions.*/
+  double shift[3] = {0.0, 0.0, 0.0};
+
+  /* Get the relative distance between the pairs, wrapping. */
+  double diff[3];
+  for (int k = 0; k < 3; k++) {
+    diff[k] = cj->loc[k] - ci->loc[k];
+    if (periodic && diff[k] < -dim[k] * 0.5)
+      shift[k] = dim[k];
+    else if (periodic && diff[k] > dim[k] * 0.5)
+      shift[k] = -dim[k];
+    else
+      shift[k] = 0.0;
+    diff[k] += shift[k];
+  }
+
+  /* Loop over particles and find which particles belong in the same group. */
+  for (size_t i = 0; i < count_i; i++) {
+
+    struct gpart *restrict pi = &gparts_i[i];
+
+    /* Ignore inhibited particles */
+    if (pi->time_bin >= time_bin_inhibited) continue;
+
+    /* Check whether we ignore this particle type altogether */
+    if (gpart_is_ignorable(pi)) continue;
+
+#ifdef SWIFT_DEBUG_CHECKS
+    if (pi->ti_drift != ti_current)
+      error("Running FOF on an un-drifted particle!");
+#endif
+
+    const double pix = pi->x[0] - shift[0];
+    const double piy = pi->x[1] - shift[1];
+    const double piz = pi->x[2] - shift[2];
+
+    /* Find the root of pi. */
+#ifdef WITH_MPI
+    size_t root_i;
+    if (ci_local) {
+      root_i = fof_find_global(index_offset_i[i] - node_offset, group_index,
+                               nr_gparts);
+    } else {
+      root_i = pi->fof_data.group_id;
+    }
+#else
+    const size_t root_i = fof_find(index_offset_i[i], group_index);
+#endif
+
+    /* Get the nature of the linking */
+    const int is_link_i = gpart_is_linkable(pi);
+    const int is_attach_i = gpart_is_attachable(pi);
+
+#ifdef SWIFT_DEBUG_CHECKS
+    if (is_link_i && is_attach_i)
+      error("Particle cannot be both linkable and attachable!");
+#endif
+
+    for (size_t j = 0; j < count_j; j++) {
+
+      struct gpart *restrict pj = &gparts_j[j];
+
+      /* Ignore inhibited particles */
+      if (pj->time_bin >= time_bin_inhibited) continue;
+
+      /* Check whether we ignore this particle type altogether */
+      if (gpart_is_ignorable(pj)) continue;
+
+      /* Get the nature of the linking */
+      const int is_link_j = gpart_is_linkable(pj);
+      const int is_attach_j = gpart_is_attachable(pj);
+
+#ifdef SWIFT_DEBUG_CHECKS
+      if (is_link_j && is_attach_j)
+        error("Particle cannot be both linkable and attachable!");
+#endif
+
+      /* We only want link<->attach pairs */
+      if (is_attach_i && is_attach_j) continue;
+      if (is_link_i && is_link_j) continue;
+
+#ifdef SWIFT_DEBUG_CHECKS
+      if (pj->ti_drift != ti_current)
+        error("Running FOF on an un-drifted particle!");
+#endif
+
+        /* Find the root of pj. */
+#ifdef WITH_MPI
+      size_t root_j;
+      if (cj_local) {
+        root_j = fof_find_global(index_offset_j[j] - node_offset, group_index,
+                                 nr_gparts);
+      } else {
+        root_j = pj->fof_data.group_id;
+      }
+#else
+      const size_t root_j = fof_find(index_offset_j[j], group_index);
+#endif
+
+      const double pjx = pj->x[0];
+      const double pjy = pj->x[1];
+      const double pjz = pj->x[2];
+
+      /* Compute pairwise distance (periodic BCs were accounted
+       for by the shift vector) */
+      float dx[3], r2 = 0.0f;
+      dx[0] = pix - pjx;
+      dx[1] = piy - pjy;
+      dx[2] = piz - pjz;
+
+      for (int k = 0; k < 3; k++) r2 += dx[k] * dx[k];
+
+      /* Hit or miss? */
+      if (r2 < l_x2) {
+
+        /* Now that we are within the linking length,
+         * decide what to do based on linking types */
+
+        if (is_link_i && is_link_j) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Fundamental logic error!");
+#endif
+
+        } else if (is_link_i && is_attach_j) {
+
+          /* We got a linkable and an attachable.
+           * See whether it is closer and if so re-link.
+           * This is safe to do as the attachables are never roots and
+           * nothing is attached to them */
+          if (cj_local) {
+
+            const float dist = sqrtf(r2);
+
+            if (dist < offset_dist_j[j]) {
+
+              /* Store the new min dist */
+              offset_dist_j[j] = dist;
+
+              /* Store the current best root */
+              attach_offset_j[j] = root_i;
+              found_attach_offset_j[j] = 1;
+            }
+          }
+
+        } else if (is_link_j && is_attach_i) {
+
+          /* We got a linkable and an attachable.
+           * See whether it is closer and if so re-link.
+           * This is safe to do as the attachables are never roots and
+           * nothing is attached to them */
+          if (ci_local) {
+
+            const float dist = sqrtf(r2);
+
+            if (dist < offset_dist_i[i]) {
+
+              /* Store the new min dist */
+              offset_dist_i[i] = dist;
+
+              /* Store the current best root */
+              attach_offset_i[i] = root_j;
+              found_attach_offset_i[i] = 1;
+            }
+          }
+
+        } else {
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Fundamental logic error!");
+#endif
+        }
+      }
+    }
+  }
+}
+
+/**
+ * @brief Recursively perform a union-find attaching between two cells.
+ *
+ * If cells are more distant than the linking length, we abort early.
+ *
+ * @param props The properties fof the FOF scheme.
+ * @param dim The dimension of the space.
+ * @param attach_r2 the square of the FOF linking length.
+ * @param periodic Are we using periodic BCs?
+ * @param space_gparts The start of the #gpart array in the #space structure.
+ * @param nr_gparts The number of #gpart in the local #space structure.
+ * @param ci The first #cell in which to perform FOF.
+ * @param cj The second #cell in which to perform FOF.
+ * @param ci_local Is the #cell ci on the local MPI rank?
+ * @param cj_local Is the #cell cj on the local MPI rank?
+ */
+void rec_fof_attach_pair(const struct fof_props *props, const double dim[3],
+                         const double attach_r2, const int periodic,
+                         const struct gpart *const space_gparts,
+                         const size_t nr_gparts, struct cell *restrict ci,
+                         struct cell *restrict cj, const int ci_local,
+                         const int cj_local) {
+
+  /* Find the shortest distance between cells, remembering to account for
+   * boundary conditions. */
+  const double r2 = cell_min_dist(ci, cj, dim);
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci == cj) error("Pair FOF called on same cell!!!");
+#endif
+
+  /* Return if cells are out of range of each other. */
+  if (r2 > attach_r2) return;
+
+  /* Recurse on both cells if they are both split. */
+  if (ci->split && cj->split) {
+    for (int k = 0; k < 8; k++) {
+      if (ci->progeny[k] != NULL) {
+
+        for (int l = 0; l < 8; l++)
+          if (cj->progeny[l] != NULL)
+            rec_fof_attach_pair(props, dim, attach_r2, periodic, space_gparts,
+                                nr_gparts, ci->progeny[k], cj->progeny[l],
+                                ci_local, cj_local);
+      }
+    }
+  } else if (ci->split) {
+    for (int k = 0; k < 8; k++) {
+      if (ci->progeny[k] != NULL)
+        rec_fof_attach_pair(props, dim, attach_r2, periodic, space_gparts,
+                            nr_gparts, ci->progeny[k], cj, ci_local, cj_local);
+    }
+  } else if (cj->split) {
+    for (int k = 0; k < 8; k++) {
+      if (cj->progeny[k] != NULL)
+        rec_fof_attach_pair(props, dim, attach_r2, periodic, space_gparts,
+                            nr_gparts, ci, cj->progeny[k], ci_local, cj_local);
+    }
+  } else {
+    /* Perform FOF attach between pairs of cells that are within the linking
+     * length and not the same cell. */
+    fof_attach_pair_cells(props, dim, attach_r2, periodic, space_gparts,
+                          nr_gparts, ci, cj, ci_local, cj_local);
+  }
+}
+
+/**
+ * @brief Recursively perform a the attaching operation on a cell.
+ *
+ * @param props The properties fof the FOF scheme.
+ * @param dim The dimension of the space.
+ * @param attach_r2 the square of the FOF linking length.
+ * @param periodic Are we using periodic BCs?
+ * @param space_gparts The start of the #gpart array in the #space structure.
+ * @param nr_gparts The number of #gpart in the local #space structure.
+ * @param c The #cell in which to perform FOF.
+ */
+void rec_fof_attach_self(const struct fof_props *props, const double dim[3],
+                         const double attach_r2, const int periodic,
+                         const struct gpart *const space_gparts,
+                         const size_t nr_gparts, struct cell *c) {
+
+  /* Recurse? */
+  if (c->split) {
+
+    /* Loop over all progeny. Perform pair and self recursion on progenies.*/
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL) {
+
+        rec_fof_attach_self(props, dim, attach_r2, periodic, space_gparts,
+                            nr_gparts, c->progeny[k]);
+
+        for (int l = k + 1; l < 8; l++)
+          if (c->progeny[l] != NULL)
+            rec_fof_attach_pair(props, dim, attach_r2, periodic, space_gparts,
+                                nr_gparts, c->progeny[k], c->progeny[l],
+                                /*ci_local=*/1,
+                                /*cj_local=*/1);
+      }
+    }
+  } else {
+
+    /* Otherwise, compute self-interaction. */
+    fof_attach_self_cell(props, attach_r2, space_gparts, nr_gparts, c);
+  }
+}
+
+/* Mapper function to atomically update the group size array. */
+void fof_update_group_size_mapper(hashmap_key_t key, hashmap_value_t *value,
+                                  void *data) {
+
+  size_t *group_size = (size_t *)data;
+
+  /* Use key to index into group size array. */
+  atomic_add(&group_size[key], value->value_st);
+}
+
+/**
+ * @brief Mapper function to calculate the group sizes.
+ *
+ * @param map_data An array of #gpart%s.
+ * @param num_elements Chunk size.
+ * @param extra_data Pointer to a #space.
+ */
+void fof_calc_group_size_mapper(void *map_data, int num_elements,
+                                void *extra_data) {
+
+  /* Retrieve mapped data. */
+  struct space *s = (struct space *)extra_data;
+  struct gpart *gparts = (struct gpart *)map_data;
+  size_t *restrict group_index = s->e->fof_properties->group_index;
+  size_t *restrict group_size = s->e->fof_properties->group_size;
+
+  /* Offset into gparts array. */
+  const ptrdiff_t gparts_offset = (ptrdiff_t)(gparts - s->gparts);
+  size_t *const group_index_offset = group_index + gparts_offset;
+
+  /* Create hash table. */
+  hashmap_t map;
+  hashmap_init(&map);
+
+  for (int ind = 0; ind < num_elements; ind++) {
+
+    const hashmap_key_t root =
+        (hashmap_key_t)fof_find(group_index_offset[ind], group_index);
+    const size_t gpart_index = gparts_offset + ind;
+
+    /* Only add particles which aren't the root of a group. Stops groups of size
+     * 1 being added to the hash table. */
+    if (root != gpart_index) {
+      hashmap_value_t *size = hashmap_get(&map, root);
+
+      if (size != NULL)
+        (*size).value_st++;
+      else
+        error("Couldn't find key (%zu) or create new one.", root);
+    }
+  }
+
+  /* Update the group size array. */
+  if (map.size > 0)
+    hashmap_iterate(&map, fof_update_group_size_mapper, group_size);
+
+  hashmap_free(&map);
+}
+
+/* Mapper function to atomically update the group mass array. */
+static INLINE void fof_update_group_mass_iterator(hashmap_key_t key,
+                                                  hashmap_value_t *value,
+                                                  void *data) {
+
+  double *group_mass = (double *)data;
+
+  /* Use key to index into group mass array. */
+  atomic_add_d(&group_mass[key], value->value_dbl);
+}
+
+/* Mapper function to atomically update the group size array. */
+static INLINE void fof_update_group_size_iterator(hashmap_key_t key,
+                                                  hashmap_value_t *value,
+                                                  void *data) {
+  long long *group_size = (long long *)data;
+
+  /* Use key to index into group mass array. */
+  atomic_add(&group_size[key], value->value_st);
+}
+
+/**
+ * @brief Mapper function to calculate the group masses.
+ *
+ * @param map_data An array of #gpart%s.
+ * @param num_elements Chunk size.
+ * @param extra_data Pointer to a #space.
+ */
+void fof_calc_group_mass_mapper(void *map_data, int num_elements,
+                                void *extra_data) {
+
+  /* Retrieve mapped data. */
+  struct space *s = (struct space *)extra_data;
+  struct gpart *gparts = (struct gpart *)map_data;
+  double *group_mass = s->e->fof_properties->group_mass;
+  long long *group_size = s->e->fof_properties->final_group_size;
+  const size_t group_id_default = s->e->fof_properties->group_id_default;
+  const size_t group_id_offset = s->e->fof_properties->group_id_offset;
+
+  /* Create hash table. */
+  hashmap_t map;
+  hashmap_init(&map);
+
+  /* Loop over particles and increment the group mass for groups above
+   * min_group_size. */
+  for (int ind = 0; ind < num_elements; ind++) {
+
+    /* Only check groups above the minimum size. */
+    if (gparts[ind].fof_data.group_id != group_id_default) {
+
+      hashmap_key_t index = gparts[ind].fof_data.group_id - group_id_offset;
+      hashmap_value_t *data = hashmap_get(&map, index);
+
+      /* Update group mass */
+      if (data != NULL) {
+        (*data).value_dbl += gparts[ind].mass;
+        (*data).value_st += 1;
+      } else
+        error("Couldn't find key (%zu) or create new one.", index);
     }
   }
 
   /* Update the group mass array. */
   if (map.size > 0)
-    hashmap_iterate(&map, fof_update_group_mass_mapper, group_mass);
+    hashmap_iterate(&map, fof_update_group_mass_iterator, group_mass);
+  if (map.size > 0)
+    hashmap_iterate(&map, fof_update_group_size_iterator, group_size);
 
   hashmap_free(&map);
 }
@@ -1444,6 +2211,7 @@ void fof_unpack_group_mass_mapper(hashmap_key_t key, hashmap_value_t *value,
   /* Store elements from hash table in array. */
   mass_send[*nsend].global_root = key;
   mass_send[*nsend].group_mass = value->value_dbl;
+  mass_send[*nsend].final_group_size = value->value_ll;
   mass_send[*nsend].first_position[0] = value->value_array2_dbl[0];
   mass_send[*nsend].first_position[1] = value->value_array2_dbl[1];
   mass_send[*nsend].first_position[2] = value->value_array2_dbl[2];
@@ -1488,6 +2256,7 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
   float *max_part_density = props->max_part_density;
   double *centre_of_mass = props->group_centre_of_mass;
   double *first_position = props->group_first_position;
+  long long *final_group_size = props->final_group_size;
 
   /* Start the hash map */
   hashmap_t map;
@@ -1500,8 +2269,8 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
     /* Ignore inhibited particles */
     if (gparts[i].time_bin >= time_bin_inhibited) continue;
 
-    /* Ignore neutrinos */
-    if (gparts[i].type == swift_type_neutrino) continue;
+    /* Check whether we ignore this particle type altogether */
+    if (gpart_is_ignorable(&gparts[i])) continue;
 
     /* Check if the particle is in a group above the threshold. */
     if (gparts[i].fof_data.group_id != group_id_default) {
@@ -1518,6 +2287,9 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
         /* Update group mass */
         group_mass[index] += gparts[i].mass;
 
+        /* Update group size */
+        final_group_size[index]++;
+
       } else {
 
         /* The root is *not* local */
@@ -1534,6 +2306,9 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
         /* Add mass fragments of groups */
         data->value_dbl += mass;
 
+        /* Increase fragment size */
+        data->value_ll++;
+
         /* Record the first particle of this fragment that we encounter so we
          * we can use it as reference frame for the centre of mass calculation
          */
@@ -1575,8 +2350,8 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
         }
 
       } /* Foreign root */
-    }   /* Particle is in a group */
-  }     /* Loop over particles */
+    } /* Particle is in a group */
+  } /* Loop over particles */
 
   size_t nsend = map.size;
   struct fof_mass_send_hashmap hashmap_mass_send = {NULL, 0};
@@ -1648,6 +2423,7 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
     const size_t index =
         gparts[local_root_index].fof_data.group_id - local_group_offset;
     group_mass[index] += fof_mass_recv[i].group_mass;
+    final_group_size[index] += fof_mass_recv[i].final_group_size;
   }
 
   /* Loop over particles, densest particle in each *local* group.
@@ -1657,8 +2433,8 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
     /* Ignore inhibited particles */
     if (gparts[i].time_bin >= time_bin_inhibited) continue;
 
-    /* Ignore neutrinos */
-    if (gparts[i].type == swift_type_neutrino) continue;
+    /* Check whether we ignore this particle type altogether */
+    if (current_fof_ignore_type & (1 << (gparts[i].type + 1))) continue;
 
     /* Only check groups above the minimum mass threshold. */
     if (gparts[i].fof_data.group_id != group_id_default) {
@@ -1879,8 +2655,8 @@ void fof_calc_group_mass(struct fof_props *props, const struct space *s,
     /* Ignore inhibited particles */
     if (gparts[i].time_bin >= time_bin_inhibited) continue;
 
-    /* Ignore neutrinos */
-    if (gparts[i].type == swift_type_neutrino) continue;
+    /* Check whether we ignore this particle type altogether */
+    if (gpart_is_ignorable(&gparts[i])) continue;
 
     const size_t index = gparts[i].fof_data.group_id - group_id_offset;
 
@@ -1982,8 +2758,8 @@ void fof_find_foreign_links_mapper(void *map_data, int num_elements,
   for (int ind = 0; ind < num_elements; ind++) {
 
     /* Get the local and foreign cells to recurse on. */
-    struct cell *restrict local_cell = cell_pairs[ind].local;
-    struct cell *restrict foreign_cell = cell_pairs[ind].foreign;
+    const struct cell *restrict local_cell = cell_pairs[ind].local;
+    const struct cell *restrict foreign_cell = cell_pairs[ind].foreign;
 
     rec_fof_search_pair_foreign(props, dim, search_r2, periodic, gparts,
                                 nr_gparts, local_cell, foreign_cell,
@@ -1996,8 +2772,8 @@ void fof_find_foreign_links_mapper(void *map_data, int num_elements,
   if (lock_lock(&s->lock) == 0) {
 
     /* Get pointers to global arrays. */
-    int *group_links_size = &props->group_links_size;
-    int *group_link_count = &props->group_link_count;
+    int *restrict group_links_size = &props->group_links_size;
+    int *restrict group_link_count = &props->group_link_count;
     struct fof_mpi **group_links = &props->group_links;
 
     /* If the global group_links array is not big enough re-allocate it. */
@@ -2210,6 +2986,9 @@ void fof_seed_black_holes(const struct fof_props *props,
       /* Save the ID */
       bp->id = p->id;
 
+      /* Save the tree depth */
+      bp->depth_h = p->depth_h;
+
 #ifdef SWIFT_DEBUG_CHECKS
       bp->ti_kick = p->ti_kick;
       bp->ti_drift = p->ti_drift;
@@ -2244,7 +3023,7 @@ void fof_dump_group_data(const struct fof_props *props, const int my_rank,
   FILE *file = NULL;
 
   struct part *parts = s->parts;
-  size_t *group_size = props->group_size;
+  long long *final_group_size = props->final_group_size;
   size_t *group_index = props->group_index;
   double *group_mass = props->group_mass;
   double *group_centre_of_mass = props->group_centre_of_mass;
@@ -2287,8 +3066,8 @@ void fof_dump_group_data(const struct fof_props *props, const int my_rank,
         const long long part_id = props->max_part_density_index[i] >= 0
                                       ? parts[max_part_density_index[i]].id
                                       : -1;
-        fprintf(file, "  %8zu %12zu %12e %12e %12e %12e %12e %24lld %24lld\n",
-                group_index[i], group_size[i], group_mass[i],
+        fprintf(file, "  %8zu %12lld %12e %12e %12e %12e %12e %24lld %24lld\n",
+                group_index[i], final_group_size[i], group_mass[i],
                 group_centre_of_mass[i * 3 + 0],
                 group_centre_of_mass[i * 3 + 1],
                 group_centre_of_mass[i * 3 + 2], max_part_density[i],
@@ -2313,6 +3092,7 @@ void fof_dump_group_data(const struct fof_props *props, const int my_rank,
 struct mapper_data {
   size_t *group_index;
   size_t *group_size;
+  float *distance_to_link;
   size_t nr_gparts;
   struct gpart *space_gparts;
 };
@@ -2349,9 +3129,19 @@ void fof_set_outgoing_root_mapper(void *map_data, int num_elements,
 
     /* Set each particle's root and group properties found in the local FOF.*/
     for (int k = 0; k < local_cell->grav.count; k++) {
+
+      /* TODO: Can we skip ignorable particles here?
+       * Likely makes no difference */
+
+      /* Recall we did alter the group_index with a global_offset.
+       * We need to remove that here as we want the *local* root */
       const size_t root =
           fof_find_global(offset[k] - node_offset, group_index, nr_gparts);
 
+      /* TODO: Could we call fof_find() here instead?
+       * Likely yes but we  don't want path compression at this stage.
+       * So, probably not */
+
       gparts[k].fof_data.group_id = root;
       gparts[k].fof_data.group_size = group_size[root - node_offset];
     }
@@ -2372,223 +3162,511 @@ void fof_set_outgoing_root_mapper(void *map_data, int num_elements,
 void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
 
 #ifdef WITH_MPI
-
   struct engine *e = s->e;
   const int verbose = e->verbose;
+
+  /* Abort if only one node */
+  if (e->nr_nodes == 1) return;
+
   size_t *restrict group_index = props->group_index;
   size_t *restrict group_size = props->group_size;
   const size_t nr_gparts = s->nr_gparts;
   const double dim[3] = {s->dim[0], s->dim[1], s->dim[2]};
   const double search_r2 = props->l_x2;
 
-  ticks tic = getticks();
+  const ticks tic_total = getticks();
+  ticks tic = getticks();
+
+  /* Make group IDs globally unique. */
+  for (size_t i = 0; i < nr_gparts; i++) group_index[i] += node_offset;
+
+  struct cell_pair_indices *cell_pairs = NULL;
+  int cell_pair_count = 0;
+
+  props->group_links_size = fof_props_default_group_link_size;
+
+  int num_cells_out = 0;
+  int num_cells_in = 0;
+
+  /* Find the maximum no. of cell pairs that can communicate. */
+  for (int i = 0; i < e->nr_proxies; i++) {
+
+    for (int j = 0; j < e->proxies[i].nr_cells_out; j++) {
+
+      /* Only include gravity cells. */
+      if (e->proxies[i].cells_out_type[j] & proxy_cell_type_gravity)
+        num_cells_out++;
+    }
+
+    for (int j = 0; j < e->proxies[i].nr_cells_in; j++) {
+
+      /* Only include gravity cells. */
+      if (e->proxies[i].cells_in_type[j] & proxy_cell_type_gravity)
+        num_cells_in++;
+    }
+  }
+
+  if (verbose)
+    message(
+        "Finding max no. of cells + offset IDs"
+        "took: %.3f %s.",
+        clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  const int cell_pair_size = num_cells_in * num_cells_out;
+
+  /* Allocate memory for all the possible cell links */
+  if (swift_memalign("fof_group_links", (void **)&props->group_links,
+                     SWIFT_STRUCT_ALIGNMENT,
+                     props->group_links_size * sizeof(struct fof_mpi)) != 0)
+    error("Error while allocating memory for FOF links over an MPI domain");
+
+  if (swift_memalign("fof_cell_pairs", (void **)&cell_pairs,
+                     SWIFT_STRUCT_ALIGNMENT,
+                     cell_pair_size * sizeof(struct cell_pair_indices)) != 0)
+    error("Error while allocating memory for FOF cell pair indices");
+
+  ticks tic_pairs = getticks();
+
+  /* Loop over cells_in and cells_out for each proxy and find which cells are in
+   * range of each other to perform the FOF search. Store local cells that are
+   * touching foreign cells in a list. */
+  for (int i = 0; i < e->nr_proxies; i++) {
+
+    /* Only find links across an MPI domain on one rank. */
+    if (engine_rank == min(engine_rank, e->proxies[i].nodeID)) {
+
+      for (int j = 0; j < e->proxies[i].nr_cells_out; j++) {
+
+        /* Skip non-gravity cells. */
+        if (!(e->proxies[i].cells_out_type[j] & proxy_cell_type_gravity))
+          continue;
+
+        struct cell *restrict local_cell = e->proxies[i].cells_out[j];
+
+        /* Skip empty cells. */
+        if (local_cell->grav.count == 0) continue;
+
+        for (int k = 0; k < e->proxies[i].nr_cells_in; k++) {
+
+          /* Skip non-gravity cells. */
+          if (!(e->proxies[i].cells_in_type[k] & proxy_cell_type_gravity))
+            continue;
+
+          struct cell *restrict foreign_cell = e->proxies[i].cells_in[k];
+
+          /* Skip empty cells. */
+          if (foreign_cell->grav.count == 0) continue;
+
+          /* Add candidates in range to the list of pairs of cells to treat. */
+          const double r2 = cell_min_dist(local_cell, foreign_cell, dim);
+          if (r2 < search_r2) {
+            cell_pairs[cell_pair_count].local = local_cell;
+            cell_pairs[cell_pair_count].foreign = foreign_cell;
+
+            cell_pair_count++;
+          }
+        }
+      }
+    }
+  }
+
+  if (verbose)
+    message("Finding local/foreign cell pairs took: %.3f %s.",
+            clocks_from_ticks(getticks() - tic_pairs), clocks_getunit());
+
+  const ticks tic_set_roots = getticks();
+
+  /* Set the root of outgoing particles. */
+
+  /* Allocate array of outgoing cells and populate it */
+  struct cell **local_cells =
+      (struct cell **)malloc(num_cells_out * sizeof(struct cell *));
+  int count = 0;
+  for (int i = 0; i < e->nr_proxies; i++) {
+    for (int j = 0; j < e->proxies[i].nr_cells_out; j++) {
+
+      /* Only include gravity cells. */
+      if (e->proxies[i].cells_out_type[j] & proxy_cell_type_gravity) {
+
+        local_cells[count] = e->proxies[i].cells_out[j];
+        ++count;
+      }
+    }
+  }
+
+  /* Now set the *local* roots of all the gparts we are sending */
+  struct mapper_data data;
+  data.group_index = group_index;
+  data.group_size = group_size;
+  data.nr_gparts = nr_gparts;
+  data.space_gparts = s->gparts;
+  threadpool_map(&e->threadpool, fof_set_outgoing_root_mapper, local_cells,
+                 num_cells_out, sizeof(struct cell **),
+                 threadpool_auto_chunk_size, &data);
+
+  if (verbose)
+    message("Initialising particle roots took: %.3f %s.",
+            clocks_from_ticks(getticks() - tic_set_roots), clocks_getunit());
+
+  free(local_cells);
+
+  if (verbose)
+    message(
+        "Finding local/foreign cell pairs and initialising particle roots "
+        "took: %.3f %s.",
+        clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  /* Activate the tasks exchanging all the required gparts */
+  engine_activate_gpart_comms(e);
+
+  ticks local_fof_tic = getticks();
+
+  /* Wait for all the communication tasks to be ready */
+  MPI_Barrier(MPI_COMM_WORLD);
+
+  if (verbose)
+    message("Local FOF imbalance: %.3f %s.",
+            clocks_from_ticks(getticks() - local_fof_tic), clocks_getunit());
+
+  tic = getticks();
+
+  /* Perform send and receive tasks. */
+  engine_launch(e, "fof comms");
+
+  if (verbose)
+    message("MPI send/recv comms took: %.3f %s.",
+            clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  /* We have now recevied the foreign particles. Each particle received
+   * carries information about its own *foreign* (to us) root and the
+   * size of the group fragment it belongs too its original foreign rank. */
+
+  tic = getticks();
+
+  props->group_link_count = 0;
+
+  /* Perform search of group links between local and foreign cells with the
+   * threadpool. */
+  threadpool_map(&s->e->threadpool, fof_find_foreign_links_mapper, cell_pairs,
+                 cell_pair_count, sizeof(struct cell_pair_indices), 1,
+                 (struct space *)s);
+
+  /* Clean up memory used by foreign particles. */
+  swift_free("fof_cell_pairs", cell_pairs);
+
+  if (verbose)
+    message("Searching for foreign links took: %.3f %s.",
+            clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+  tic = getticks();
+
+  const ticks comms_tic = getticks();
+
+  MPI_Barrier(MPI_COMM_WORLD);
+
+  if (verbose)
+    message("Imbalance took: %.3f %s.",
+            clocks_from_ticks(getticks() - comms_tic), clocks_getunit());
+
+  if (verbose)
+    message("fof_search_foreign_cells() took (FOF SCALING): %.3f %s.",
+            clocks_from_ticks(getticks() - tic_total), clocks_getunit());
+
+#endif /* WITH_MPI */
+}
+
+/**
+ * @brief Run all the tasks attaching the attachables to their
+ * nearest linkable particle.
+ *
+ * @param props The properties fof the FOF scheme.
+ * @param s The #space we work with.
+ */
+void fof_link_attachable_particles(struct fof_props *props,
+                                   const struct space *s) {
+
+  /* Is there anything to attach? */
+  if (!current_fof_attach_type) return;
+
+  const ticks tic_total = getticks();
+
+  /* Activate the tasks attaching attachable particles to the linkable ones */
+  engine_activate_fof_attach_tasks(s->e);
+
+  /* Perform FOF tasks for attachable particles. */
+  engine_launch(s->e, "fof");
+
+  if (s->e->verbose)
+    message("fof_link_attachable_particles() took (FOF SCALING): %.3f %s.",
+            clocks_from_ticks(getticks() - tic_total), clocks_getunit());
+}
+
+/**
+ * @brief Construct an array indicating whether a given root does not appear
+ * in the global list of fragments to link.
+ *
+ * Nothing to do here if not running with MPI or if there are no attacheables.
+ *
+ * @param props The properties fof the FOF scheme.
+ * @param s The #space we work with.
+ */
+void fof_build_list_of_purely_local_groups(struct fof_props *props,
+                                           const struct space *s) {
+
+  /* Is there anything to attach?
+   * (The array we construct here is only useful with attacheables)*/
+  if (!current_fof_attach_type) return;
+
+#ifdef WITH_MPI
+
+  struct engine *e = s->e;
+
+  /* Abort if only one node */
+  if (e->nr_nodes == 1) return;
+
+  /* Local copy of the variable set in the mapper */
+  const size_t nr_gparts = s->nr_gparts;
+  size_t *restrict group_index = props->group_index;
+  const int group_link_count = props->group_link_count;
+
+  /* Sum the total number of links across MPI domains over each MPI rank. */
+  int global_group_link_count = 0;
+  MPI_Allreduce(&group_link_count, &global_group_link_count, 1, MPI_INT,
+                MPI_SUM, MPI_COMM_WORLD);
+
+  if (global_group_link_count < 0)
+    error("Overflow of the size of the global list of foregin links");
+
+  struct fof_mpi *global_group_links = NULL;
+  int *displ = NULL, *group_link_counts = NULL;
+
+  if (swift_memalign("fof_global_group_links", (void **)&global_group_links,
+                     SWIFT_STRUCT_ALIGNMENT,
+                     global_group_link_count * sizeof(struct fof_mpi)) != 0)
+    error("Error while allocating memory for the global list of group links");
+
+  if (posix_memalign((void **)&group_link_counts, SWIFT_STRUCT_ALIGNMENT,
+                     e->nr_nodes * sizeof(int)) != 0)
+    error(
+        "Error while allocating memory for the number of group links on each "
+        "MPI rank");
+
+  if (posix_memalign((void **)&displ, SWIFT_STRUCT_ALIGNMENT,
+                     e->nr_nodes * sizeof(int)) != 0)
+    error(
+        "Error while allocating memory for the displacement in memory for the "
+        "global group link list");
+
+  /* Gather the total number of links on each rank. */
+  MPI_Allgather(&group_link_count, 1, MPI_INT, group_link_counts, 1, MPI_INT,
+                MPI_COMM_WORLD);
+
+  /* Set the displacements into the global link list using the link counts from
+   * each rank */
+  displ[0] = 0;
+  for (int i = 1; i < e->nr_nodes; i++) {
+    displ[i] = displ[i - 1] + group_link_counts[i - 1];
+    if (displ[i] < 0) error("Number of group links overflowing!");
+  }
+
+  /* Gather the global link list on all ranks. */
+  MPI_Allgatherv(props->group_links, group_link_count, fof_mpi_type,
+                 global_group_links, group_link_counts, displ, fof_mpi_type,
+                 MPI_COMM_WORLD);
+
+  /* Clean up memory. */
+  free(group_link_counts);
+  free(displ);
 
-  /* Make group IDs globally unique. */
-  for (size_t i = 0; i < nr_gparts; i++) group_index[i] += node_offset;
+  /* We now have a list of all the fragment connections.
+   * We can iterate over the *local* groups to identify the ones which
+   * are *not* appearing in the list */
 
-  struct cell_pair_indices *cell_pairs = NULL;
-  int group_link_count = 0;
-  int cell_pair_count = 0;
+  if (posix_memalign((void **)&props->is_purely_local, SWIFT_STRUCT_ALIGNMENT,
+                     nr_gparts * sizeof(char)) != 0)
+    error("Error while allocating memory for the list of purely local groups");
 
-  props->group_links_size = fof_props_default_group_link_size;
-  props->group_link_count = 0;
+  /* Start by pretending every group is purely local */
+  for (size_t i = 0; i < nr_gparts; ++i) props->is_purely_local[i] = 1;
 
-  int num_cells_out = 0;
-  int num_cells_in = 0;
+  /* Now loop over the list of inter-rank connections and flag each halo present
+   * in the list */
+  for (int k = 0; k < global_group_link_count; ++k) {
 
-  /* Find the maximum no. of cell pairs. */
-  for (int i = 0; i < e->nr_proxies; i++) {
+    const size_t group_i = global_group_links[k].group_i;
+    const size_t group_j = global_group_links[k].group_j;
 
-    for (int j = 0; j < e->proxies[i].nr_cells_out; j++) {
+    const size_t root_i =
+        fof_find_global(group_i - node_offset, group_index, nr_gparts);
+    const size_t root_j =
+        fof_find_global(group_j - node_offset, group_index, nr_gparts);
 
-      /* Only include gravity cells. */
-      if (e->proxies[i].cells_out_type[j] & proxy_cell_type_gravity)
-        num_cells_out++;
+    if (is_local(root_i, nr_gparts)) {
+      const size_t local_root = root_i - node_offset;
+      props->is_purely_local[local_root] = 0;
     }
 
-    for (int j = 0; j < e->proxies[i].nr_cells_in; j++) {
-
-      /* Only include gravity cells. */
-      if (e->proxies[i].cells_in_type[j] & proxy_cell_type_gravity)
-        num_cells_in++;
+    if (is_local(root_j, nr_gparts)) {
+      const size_t local_root = root_j - node_offset;
+      props->is_purely_local[local_root] = 0;
     }
   }
 
-  if (verbose)
-    message(
-        "Finding max no. of cells + offset IDs"
-        "took: %.3f %s.",
-        clocks_from_ticks(getticks() - tic), clocks_getunit());
-
-  const int cell_pair_size = num_cells_in * num_cells_out;
-
-  if (swift_memalign("fof_group_links", (void **)&props->group_links,
-                     SWIFT_STRUCT_ALIGNMENT,
-                     props->group_links_size * sizeof(struct fof_mpi)) != 0)
-    error("Error while allocating memory for FOF links over an MPI domain");
+  /* Clean up the last allocated array */
+  swift_free("fof_global_group_links", global_group_links);
+#endif
+}
 
-  if (swift_memalign("fof_cell_pairs", (void **)&cell_pairs,
-                     SWIFT_STRUCT_ALIGNMENT,
-                     cell_pair_size * sizeof(struct cell_pair_indices)) != 0)
-    error("Error while allocating memory for FOF cell pair indices");
+/**
+ * @brief Process all the attachable-linkable connections to add the
+ * attachables to the groups they belong to.
+ *
+ * @param props The properties fof the FOF scheme.
+ * @param s The #space we work with.
+ */
+void fof_finalise_attachables(struct fof_props *props, const struct space *s) {
 
-  ticks tic_pairs = getticks();
+  /* Is there anything to attach? */
+  if (!current_fof_attach_type) return;
 
-  /* Loop over cells_in and cells_out for each proxy and find which cells are in
-   * range of each other to perform the FOF search. Store local cells that are
-   * touching foreign cells in a list. */
-  for (int i = 0; i < e->nr_proxies; i++) {
+  const ticks tic_total = getticks();
 
-    /* Only find links across an MPI domain on one rank. */
-    if (engine_rank == min(engine_rank, e->proxies[i].nodeID)) {
+  const size_t nr_gparts = s->nr_gparts;
 
-      for (int j = 0; j < e->proxies[i].nr_cells_out; j++) {
+  char *restrict found_attachable_link = props->found_attachable_link;
+  size_t *restrict attach_index = props->attach_index;
+  size_t *restrict group_index = props->group_index;
+  size_t *restrict group_size = props->group_size;
 
-        /* Skip non-gravity cells. */
-        if (!(e->proxies[i].cells_out_type[j] & proxy_cell_type_gravity))
-          continue;
+#ifdef WITH_MPI
 
-        struct cell *restrict local_cell = e->proxies[i].cells_out[j];
+  /* Get pointers to global arrays. */
+  char *restrict is_purely_local = props->is_purely_local;
+  int *restrict group_links_size = &props->group_links_size;
+  int *restrict group_link_count = &props->group_link_count;
+  struct fof_mpi **group_links = &props->group_links;
 
-        /* Skip empty cells. */
-        if (local_cell->grav.count == 0) continue;
+  /* Loop over all the attachables and added them to the group they belong to */
+  for (size_t i = 0; i < nr_gparts; ++i) {
 
-        for (int k = 0; k < e->proxies[i].nr_cells_in; k++) {
+    const struct gpart *gp = &s->gparts[i];
 
-          /* Skip non-gravity cells. */
-          if (!(e->proxies[i].cells_in_type[k] & proxy_cell_type_gravity))
-            continue;
+    if (gpart_is_attachable(gp) && found_attachable_link[i]) {
 
-          struct cell *restrict foreign_cell = e->proxies[i].cells_in[k];
+      /* Update its root */
+      const size_t root_j = attach_index[i];
+      const size_t root_i =
+          fof_find_global(group_index[i] - node_offset, group_index, nr_gparts);
 
-          /* Skip empty cells. */
-          if (foreign_cell->grav.count == 0) continue;
+      /* Update the size of the group the particle belongs to.
+       * The strategy emploed depends on where the root and particles are. */
+      if (is_local(root_j, nr_gparts)) {
 
-          /* Check if local cell has already been added to the local list of
-           * cells. */
-          const double r2 = cell_min_dist(local_cell, foreign_cell, dim);
-          if (r2 < search_r2) {
-            cell_pairs[cell_pair_count].local = local_cell;
-            cell_pairs[cell_pair_count++].foreign = foreign_cell;
-          }
-        }
-      }
-    }
-  }
+        const size_t local_root = root_j - node_offset;
 
-  if (verbose)
-    message(
-        "Finding local/foreign cell pairs"
-        "took: %.3f %s.",
-        clocks_from_ticks(getticks() - tic_pairs), clocks_getunit());
+        if (is_purely_local[local_root]) { /* All parties involved are local */
 
-  ticks tic_set_roots = getticks();
+          /* We can directly attack the list of groups */
+          group_index[i] = local_root + node_offset;
+          group_size[local_root]++;
 
-  /* Set the root of outgoing particles. */
+        } else { /* Group is involved in some cross-boundaries mixing */
 
-  /* Allocate array of outgoing cells and populate it */
-  struct cell **local_cells =
-      (struct cell **)malloc(num_cells_out * sizeof(struct cell *));
-  int count = 0;
-  for (int i = 0; i < e->nr_proxies; i++) {
-    for (int j = 0; j < e->proxies[i].nr_cells_out; j++) {
+          /* Add to the list of links to be resolved globally later */
+          add_foreign_link_to_list(group_link_count, group_links_size,
+                                   group_links, group_links, root_i, root_j,
+                                   /*size_i=*/1,
+                                   /*size_j=*/2);
+        }
 
-      /* Only include gravity cells. */
-      if (e->proxies[i].cells_out_type[j] & proxy_cell_type_gravity) {
+      } else { /* Root is foreign */
 
-        local_cells[count] = e->proxies[i].cells_out[j];
-        ++count;
+        /* Add to the list of links to be resolved globally later */
+        add_foreign_link_to_list(group_link_count, group_links_size,
+                                 group_links, group_links, root_i, root_j,
+                                 /*size_i=*/1,
+                                 /*size_j=*/2);
       }
     }
   }
 
-  /* Now set the roots */
-  struct mapper_data data;
-  data.group_index = group_index;
-  data.group_size = group_size;
-  data.nr_gparts = nr_gparts;
-  data.space_gparts = s->gparts;
-  threadpool_map(&e->threadpool, fof_set_outgoing_root_mapper, local_cells,
-                 num_cells_out, sizeof(struct cell **),
-                 threadpool_auto_chunk_size, &data);
-
-  if (verbose)
-    message(
-        "Initialising particle roots "
-        "took: %.3f %s.",
-        clocks_from_ticks(getticks() - tic_set_roots), clocks_getunit());
-
-  free(local_cells);
-
-  if (verbose)
-    message(
-        "Finding local/foreign cell pairs and initialising particle roots "
-        "took: %.3f %s.",
-        clocks_from_ticks(getticks() - tic), clocks_getunit());
-
-  /* Allocate buffers to receive the gpart fof information */
-  engine_allocate_foreign_particles(e, /*fof=*/1);
-
-  /* Activate the tasks exchanging all the required gparts */
-  engine_activate_gpart_comms(e);
+  /* We can free the list of purely local groups */
+  free(props->is_purely_local);
 
-  ticks local_fof_tic = getticks();
+#else /* not WITH_MPI */
 
-  MPI_Barrier(MPI_COMM_WORLD);
+  /* Loop over all the attachables and added them to the group they belong to */
+  for (size_t i = 0; i < nr_gparts; ++i) {
 
-  if (verbose)
-    message("Local FOF imbalance: %.3f %s.",
-            clocks_from_ticks(getticks() - local_fof_tic), clocks_getunit());
+    const struct gpart *gp = &s->gparts[i];
 
-  tic = getticks();
+    if (gpart_is_attachable(gp) && found_attachable_link[i]) {
 
-  /* Perform send and receive tasks. */
-  engine_launch(e, "fof comms");
+      const size_t root = attach_index[i];
 
-  if (verbose)
-    message("MPI send/recv comms took: %.3f %s.",
-            clocks_from_ticks(getticks() - tic), clocks_getunit());
+      /* Update its root */
+      group_index[i] = root;
 
-  tic = getticks();
+      /* Update the size of the group the particle belongs to */
+      group_size[root]++;
+    }
+  }
 
-  /* Perform search of group links between local and foreign cells with the
-   * threadpool. */
-  threadpool_map(&s->e->threadpool, fof_find_foreign_links_mapper, cell_pairs,
-                 cell_pair_count, sizeof(struct cell_pair_indices), 1,
-                 (struct space *)s);
+#endif /* WITH_MPI */
 
-  group_link_count = props->group_link_count;
+  if (s->e->verbose)
+    message("fof_finalise_attachables() took (FOF SCALING): %.3f %s.",
+            clocks_from_ticks(getticks() - tic_total), clocks_getunit());
+}
 
-  /* Clean up memory. */
-  swift_free("fof_cell_pairs", cell_pairs);
-  space_free_foreign_parts(e->s, /*clear pointers=*/1);
+/**
+ * @brief Process all the group fragments spanning more than
+ * one rank to link them.
+ *
+ * This is the final global union-find pass which concludes
+ * the MPI-FOF-algorithm.
+ *
+ * @param props The properties fof the FOF scheme.
+ * @param s The #space we work with.
+ */
+void fof_link_foreign_fragments(struct fof_props *props,
+                                const struct space *s) {
 
-  if (verbose)
-    message("Searching for foreign links took: %.3f %s.",
-            clocks_from_ticks(getticks() - tic), clocks_getunit());
+#ifdef WITH_MPI
 
-  tic = getticks();
+  struct engine *e = s->e;
+  const int verbose = e->verbose;
 
-  struct fof_mpi *global_group_links = NULL;
-  int *displ = NULL, *group_link_counts = NULL;
-  int global_group_link_count = 0;
+  /* Abort if only one node */
+  if (e->nr_nodes == 1) return;
 
-  ticks comms_tic = getticks();
+  const size_t nr_gparts = s->nr_gparts;
+  size_t *restrict group_index = props->group_index;
+  size_t *restrict group_size = props->group_size;
 
-  MPI_Barrier(MPI_COMM_WORLD);
+  const ticks tic_total = getticks();
+  ticks tic = getticks();
+  const ticks comms_tic = getticks();
 
   if (verbose)
-    message("Imbalance took: %.3f %s.",
-            clocks_from_ticks(getticks() - comms_tic), clocks_getunit());
+    message(
+        "Searching %zu gravity particles for cross-node links with l_x: %lf",
+        nr_gparts, sqrt(props->l_x2));
 
-  comms_tic = getticks();
+  /* Local copy of the variable set in the mapper */
+  const int group_link_count = props->group_link_count;
 
   /* Sum the total number of links across MPI domains over each MPI rank. */
+  int global_group_link_count = 0;
   MPI_Allreduce(&group_link_count, &global_group_link_count, 1, MPI_INT,
                 MPI_SUM, MPI_COMM_WORLD);
 
-  /* Unique set of links is half of all group links as each link is found twice
-   * by opposing MPI ranks. */
+  if (global_group_link_count < 0)
+    error("Overflow of the size of the global list of foreign links");
+
+  struct fof_mpi *global_group_links = NULL;
+  int *displ = NULL, *group_link_counts = NULL;
+
   if (swift_memalign("fof_global_group_links", (void **)&global_group_links,
                      SWIFT_STRUCT_ALIGNMENT,
                      global_group_link_count * sizeof(struct fof_mpi)) != 0)
@@ -2613,8 +3691,10 @@ void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
   /* Set the displacements into the global link list using the link counts from
    * each rank */
   displ[0] = 0;
-  for (int i = 1; i < e->nr_nodes; i++)
+  for (int i = 1; i < e->nr_nodes; i++) {
     displ[i] = displ[i - 1] + group_link_counts[i - 1];
+    if (displ[i] < 0) error("Number of group links overflowing!");
+  }
 
   /* Gather the global link list on all ranks. */
   MPI_Allgatherv(props->group_links, group_link_count, fof_mpi_type,
@@ -2638,11 +3718,11 @@ void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
   tic = getticks();
 
   /* Transform the group IDs to a local list going from 0-group_count so a
-   * union-find can be performed. */
+   * union-find can be performed.
+   * Each member of a link is stored separately --> Need 2x as many entries */
   size_t *global_group_index = NULL, *global_group_id = NULL,
          *global_group_size = NULL;
   const int global_group_list_size = 2 * global_group_link_count;
-  int group_count = 0;
 
   if (swift_memalign("fof_global_group_index", (void **)&global_group_index,
                      SWIFT_STRUCT_ALIGNMENT,
@@ -2672,18 +3752,21 @@ void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
   hashmap_init(&map);
 
   /* Store each group ID and its properties. */
-  for (int i = 0; i < global_group_link_count; i++) {
+  int group_count = 0;
+  for (int k = 0; k < global_group_link_count; k++) {
 
-    size_t group_i = global_group_links[i].group_i;
-    size_t group_j = global_group_links[i].group_j;
+    const size_t group_i = global_group_links[k].group_i;
+    const size_t group_j = global_group_links[k].group_j;
 
-    global_group_size[group_count] += global_group_links[i].group_i_size;
+    global_group_size[group_count] += global_group_links[k].group_i_size;
     global_group_id[group_count] = group_i;
-    hashmap_add_group(group_i, group_count++, &map);
+    hashmap_add_group(group_i, group_count, &map);
+    group_count++;
 
-    global_group_size[group_count] += global_group_links[i].group_j_size;
+    global_group_size[group_count] += global_group_links[k].group_j_size;
     global_group_id[group_count] = group_j;
-    hashmap_add_group(group_j, group_count++, &map);
+    hashmap_add_group(group_j, group_count, &map);
+    group_count++;
   }
 
   if (verbose)
@@ -2693,8 +3776,8 @@ void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
   tic = getticks();
 
   /* Create a global_group_index list of groups across MPI domains so that you
-   * can perform a union-find locally on each node. */
-  /* The value of which is an offset into global_group_id, which is the actual
+   * can perform a union-find locally on each node.
+   * The value of which is an offset into global_group_id, which is the actual
    * root. */
   for (int i = 0; i < group_count; i++) global_group_index[i] = i;
 
@@ -2708,30 +3791,30 @@ void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
         "Error while allocating memory for the displacement in memory for the "
         "global group link list");
 
-  for (int i = 0; i < group_count; i++)
-    orig_global_group_size[i] = global_group_size[i];
+  memcpy(orig_global_group_size, global_group_size,
+         group_count * sizeof(size_t));
 
   /* Perform a union-find on the group links. */
-  for (int i = 0; i < global_group_link_count; i++) {
+  for (int k = 0; k < global_group_link_count; k++) {
 
     /* Use the hash table to find the group offsets in the index array. */
-    size_t find_i =
-        hashmap_find_group_offset(global_group_links[i].group_i, &map);
-    size_t find_j =
-        hashmap_find_group_offset(global_group_links[i].group_j, &map);
+    const size_t find_i =
+        hashmap_find_group_offset(global_group_links[k].group_i, &map);
+    const size_t find_j =
+        hashmap_find_group_offset(global_group_links[k].group_j, &map);
 
     /* Use the offset to find the group's root. */
-    size_t root_i = fof_find(find_i, global_group_index);
-    size_t root_j = fof_find(find_j, global_group_index);
+    const size_t root_i = fof_find(find_i, global_group_index);
+    const size_t root_j = fof_find(find_j, global_group_index);
 
-    size_t group_i = global_group_id[root_i];
-    size_t group_j = global_group_id[root_j];
+    const size_t group_i = global_group_id[root_i];
+    const size_t group_j = global_group_id[root_j];
 
     if (group_i == group_j) continue;
 
     /* Update roots accordingly. */
-    size_t size_i = global_group_size[root_i];
-    size_t size_j = global_group_size[root_j];
+    const size_t size_i = global_group_size[root_i];
+    const size_t size_j = global_group_size[root_j];
 #ifdef UNION_BY_SIZE_OVER_MPI
     if (size_i < size_j) {
       global_group_index[root_i] = root_j;
@@ -2762,9 +3845,9 @@ void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
   /* Update each group locally with new root information. */
   for (int i = 0; i < group_count; i++) {
 
-    size_t group_id = global_group_id[i];
-    size_t offset = fof_find(global_group_index[i], global_group_index);
-    size_t new_root = global_group_id[offset];
+    const size_t group_id = global_group_id[i];
+    const size_t offset = fof_find(global_group_index[i], global_group_index);
+    const size_t new_root = global_group_id[offset];
 
     /* If the group is local update its root and size. */
     if (is_local(group_id, nr_gparts) && new_root != group_id) {
@@ -2792,67 +3875,39 @@ void fof_search_foreign_cells(struct fof_props *props, const struct space *s) {
   swift_free("fof_global_group_id", global_group_id);
   swift_free("fof_orig_global_group_size", orig_global_group_size);
 
+  if (verbose) {
+    message("link_foreign_fragmens() took (FOF SCALING): %.3f %s.",
+            clocks_from_ticks(getticks() - tic_total), clocks_getunit());
+  }
+
 #endif /* WITH_MPI */
 }
 
 /**
- * @brief Perform a FOF search on gravity particles using the cells and applying
- * the Union-Find algorithm.
+ * @brief Compute the local size of each FOF group fragment.
  *
  * @param props The properties of the FOF scheme.
- * @param bh_props The properties of the black hole scheme.
- * @param constants The physical constants in internal units.
- * @param cosmo The current cosmological model.
  * @param s The #space containing the particles.
- * @param dump_debug_results Are we writing txt-file debug catalogues including
- * BH-seeding info?
- * @param dump_results Do we want to write the group catalogue to a hdf5 file?
- * @param seed_black_holes Do we want to seed black holes in haloes?
  */
-void fof_search_tree(struct fof_props *props,
-                     const struct black_holes_props *bh_props,
-                     const struct phys_const *constants,
-                     const struct cosmology *cosmo, struct space *s,
-                     const int dump_results, const int dump_debug_results,
-                     const int seed_black_holes) {
-
-  const size_t nr_gparts = s->nr_gparts;
-  const size_t min_group_size = props->min_group_size;
-#ifndef WITHOUT_GROUP_PROPS
-  const size_t group_id_offset = props->group_id_offset;
-  const size_t group_id_default = props->group_id_default;
-#endif
+void fof_compute_local_sizes(struct fof_props *props, struct space *s) {
 
-#ifdef WITH_MPI
-  const int nr_nodes = s->e->nr_nodes;
-#endif
-  struct gpart *gparts = s->gparts;
-  size_t *group_index, *group_size;
-  long long num_groups = 0, num_parts_in_groups = 0, max_group_size = 0;
   const int verbose = s->e->verbose;
-  const ticks tic_total = getticks();
 
-  char output_file_name[PARSER_MAX_LINE_SIZE];
-  snprintf(output_file_name, PARSER_MAX_LINE_SIZE, "%s", props->base_name);
+  struct gpart *gparts = s->gparts;
+  const size_t nr_gparts = s->nr_gparts;
 
-  if (verbose)
-    message("Searching %zu gravity particles for links with l_x: %lf",
-            nr_gparts, sqrt(props->l_x2));
+  const ticks tic_total = getticks();
 
   if (engine_rank == 0 && verbose)
     message("Size of hash table element: %ld", sizeof(hashmap_element_t));
 
 #ifdef WITH_MPI
 
-  /* Reset global variable */
-  node_offset = 0;
+  const ticks comms_tic = getticks();
 
   /* Determine number of gparts on lower numbered MPI ranks */
+  const long long nr_gparts_local = s->nr_gparts;
   long long nr_gparts_cumulative;
-  long long nr_gparts_local = s->nr_gparts;
-
-  const ticks comms_tic = getticks();
-
   MPI_Scan(&nr_gparts_local, &nr_gparts_cumulative, 1, MPI_LONG_LONG, MPI_SUM,
            MPI_COMM_WORLD);
 
@@ -2860,13 +3915,12 @@ void fof_search_tree(struct fof_props *props,
     message("MPI_Scan Imbalance took: %.3f %s.",
             clocks_from_ticks(getticks() - comms_tic), clocks_getunit());
 
+  /* Reset global variable containing the rank particle count offset */
   node_offset = nr_gparts_cumulative - nr_gparts_local;
 #endif
 
-  /* Local copy of the arrays */
-  group_index = props->group_index;
-  group_size = props->group_size;
-
+  /* Compute the group sizes of the local fragments
+   * (in non-MPI land that is the final group size of the haloes) */
   const ticks tic_calc_group_size = getticks();
 
   threadpool_map(&s->e->threadpool, fof_calc_group_size_mapper, gparts,
@@ -2877,31 +3931,52 @@ void fof_search_tree(struct fof_props *props,
             clocks_from_ticks(getticks() - tic_calc_group_size),
             clocks_getunit());
 
-#ifdef WITH_MPI
-  if (nr_nodes > 1) {
+  if (verbose)
+    message("took %.3f %s.", clocks_from_ticks(getticks() - tic_total),
+            clocks_getunit());
+}
 
-    const ticks tic_mpi = getticks();
+/**
+ * @brief Compute all the group properties
+ *
+ * @param props The properties of the FOF scheme.
+ * @param bh_props The properties of the black hole scheme.
+ * @param constants The physical constants in internal units.
+ * @param cosmo The current cosmological model.
+ * @param s The #space containing the particles.
+ * @param dump_debug_results Are we writing txt-file debug catalogues including
+ * BH-seeding info?
+ * @param dump_results Do we want to write the group catalogue to a hdf5 file?
+ * @param seed_black_holes Do we want to seed black holes in haloes?
+ */
+void fof_compute_group_props(struct fof_props *props,
+                             const struct black_holes_props *bh_props,
+                             const struct phys_const *constants,
+                             const struct cosmology *cosmo, struct space *s,
+                             const int dump_results,
+                             const int dump_debug_results,
+                             const int seed_black_holes) {
 
-    /* Search for group links across MPI domains. */
-    fof_search_foreign_cells(props, s);
+  const int verbose = s->e->verbose;
+#ifdef WITH_MPI
+  const int nr_nodes = s->e->nr_nodes;
+#endif
+  const ticks tic_total = getticks();
 
-    if (verbose) {
-      message("fof_search_foreign_cells() took (FOF SCALING): %.3f %s.",
-              clocks_from_ticks(getticks() - tic_mpi), clocks_getunit());
+  struct gpart *gparts = s->gparts;
+  const size_t nr_gparts = s->nr_gparts;
 
-      message(
-          "fof_search_foreign_cells() + calc_group_size took (FOF SCALING): "
-          "%.3f %s.",
-          clocks_from_ticks(getticks() - tic_total), clocks_getunit());
-    }
-  }
-#endif
+  const size_t min_group_size = props->min_group_size;
+  const size_t group_id_offset = props->group_id_offset;
+  const size_t group_id_default = props->group_id_default;
 
   size_t num_groups_local = 0;
-#ifndef WITHOUT_GROUP_PROPS
   size_t num_parts_in_groups_local = 0;
   size_t max_group_size_local = 0;
-#endif
+
+  /* Local copy of the arrays */
+  size_t *restrict group_index = props->group_index;
+  size_t *restrict group_size = props->group_size;
 
   const ticks tic_num_groups_calc = getticks();
 
@@ -2917,7 +3992,6 @@ void fof_search_tree(struct fof_props *props,
       num_groups_local++;
 #endif
 
-#ifndef WITHOUT_GROUP_PROPS
     /* Find the total number of particles in groups. */
     if (group_size[i] >= min_group_size)
       num_parts_in_groups_local += group_size[i];
@@ -2925,7 +3999,6 @@ void fof_search_tree(struct fof_props *props,
     /* Find the largest group. */
     if (group_size[i] > max_group_size_local)
       max_group_size_local = group_size[i];
-#endif
   }
 
   if (verbose)
@@ -2934,9 +4007,8 @@ void fof_search_tree(struct fof_props *props,
         "%s.",
         clocks_from_ticks(getticks() - tic_num_groups_calc), clocks_getunit());
 
-    /* Sort the groups in descending order based upon size and re-label their
-     * IDs 0-num_groups. */
-#ifndef WITHOUT_GROUP_PROPS
+  /* Sort the groups in descending order based upon size and re-label their
+   * IDs 0-num_groups. */
   struct group_length *high_group_sizes = NULL;
   int group_count = 0;
 
@@ -2961,9 +4033,9 @@ void fof_search_tree(struct fof_props *props,
   }
 
   ticks tic = getticks();
-#endif /* #ifndef WITHOUT_GROUP_PROPS */
 
   /* Find global properties. */
+  long long num_groups = 0, num_parts_in_groups = 0, max_group_size = 0;
 #ifdef WITH_MPI
   MPI_Allreduce(&num_groups_local, &num_groups, 1, MPI_LONG_LONG_INT, MPI_SUM,
                 MPI_COMM_WORLD);
@@ -2973,24 +4045,18 @@ void fof_search_tree(struct fof_props *props,
             clocks_from_ticks(getticks() - tic_num_groups_calc),
             clocks_getunit());
 
-#ifndef WITHOUT_GROUP_PROPS
   MPI_Reduce(&num_parts_in_groups_local, &num_parts_in_groups, 1,
              MPI_LONG_LONG_INT, MPI_SUM, 0, MPI_COMM_WORLD);
   MPI_Reduce(&max_group_size_local, &max_group_size, 1, MPI_LONG_LONG_INT,
              MPI_MAX, 0, MPI_COMM_WORLD);
-#endif /* #ifndef WITHOUT_GROUP_PROPS */
 #else
   num_groups = num_groups_local;
 
-#ifndef WITHOUT_GROUP_PROPS
   num_parts_in_groups = num_parts_in_groups_local;
   max_group_size = max_group_size_local;
-#endif /* #ifndef WITHOUT_GROUP_PROPS */
 #endif /* WITH_MPI */
   props->num_groups = num_groups;
 
-#ifndef WITHOUT_GROUP_PROPS
-
   /* Find number of groups on lower numbered MPI ranks */
 #ifdef WITH_MPI
   long long nglocal = num_groups_local;
@@ -3158,6 +4224,9 @@ void fof_search_tree(struct fof_props *props,
   if (swift_memalign("fof_group_mass", (void **)&props->group_mass, 32,
                      num_groups_local * sizeof(double)) != 0)
     error("Failed to allocate list of group masses for FOF search.");
+  if (swift_memalign("fof_group_size", (void **)&props->final_group_size, 32,
+                     num_groups_local * sizeof(long long)) != 0)
+    error("Failed to allocate list of group masses for FOF search.");
   if (swift_memalign("fof_group_centre_of_mass",
                      (void **)&props->group_centre_of_mass, 32,
                      num_groups_local * 3 * sizeof(double)) != 0)
@@ -3168,6 +4237,7 @@ void fof_search_tree(struct fof_props *props,
     error("Failed to allocate list of group first positions for FOF search.");
 
   bzero(props->group_mass, num_groups_local * sizeof(double));
+  bzero(props->final_group_size, num_groups_local * sizeof(long long));
   bzero(props->group_centre_of_mass, num_groups_local * 3 * sizeof(double));
   for (size_t i = 0; i < 3 * num_groups_local; i++) {
     props->group_first_position[i] = -FLT_MAX;
@@ -3202,8 +4272,9 @@ void fof_search_tree(struct fof_props *props,
   free(num_on_node);
   free(first_on_node);
 #else
-  fof_calc_group_mass(props, s, seed_black_holes, num_groups_local, 0, NULL,
-                      NULL, props->group_mass);
+  fof_calc_group_mass(props, s, seed_black_holes, num_groups_local,
+                      /*num_groups_prev=*/0, /*num_on_node=*/NULL,
+                      /*first_on_node=*/NULL, props->group_mass);
 #endif
 
   /* Finalise the group data before dump */
@@ -3217,13 +4288,17 @@ void fof_search_tree(struct fof_props *props,
   /* Dump group data. */
   if (dump_results) {
 #ifdef HAVE_HDF5
-    write_fof_hdf5_catalogue(props, num_groups_local, s->e);
+    write_fof_hdf5_catalogue(props, (long long)num_groups_local, s->e);
 #else
     error("Can't dump hdf5 catalogues with hdf5 switched off!");
 #endif
   }
 
   if (dump_debug_results) {
+
+    char output_file_name[PARSER_MAX_LINE_SIZE];
+    snprintf(output_file_name, PARSER_MAX_LINE_SIZE, "%s", props->base_name);
+
 #ifdef WITH_MPI
     snprintf(output_file_name + strlen(output_file_name), FILENAME_BUFFER_SIZE,
              "_mpi.dat");
@@ -3244,17 +4319,21 @@ void fof_search_tree(struct fof_props *props,
   /* Free the left-overs */
   swift_free("fof_high_group_sizes", high_group_sizes);
   swift_free("fof_group_mass", props->group_mass);
+  swift_free("fof_group_size", props->final_group_size);
   swift_free("fof_group_centre_of_mass", props->group_centre_of_mass);
   swift_free("fof_group_first_position", props->group_first_position);
   swift_free("fof_max_part_density_index", props->max_part_density_index);
   swift_free("fof_max_part_density", props->max_part_density);
   props->group_mass = NULL;
+  props->final_group_size = NULL;
   props->group_centre_of_mass = NULL;
   props->max_part_density_index = NULL;
   props->max_part_density = NULL;
 
-#endif /* #ifndef WITHOUT_GROUP_PROPS */
+  swift_free("fof_distance", props->distance_to_link);
   swift_free("fof_group_index", props->group_index);
+  swift_free("fof_attach_index", props->attach_index);
+  swift_free("fof_found_attach", props->found_attachable_link);
   swift_free("fof_group_size", props->group_size);
   props->group_index = NULL;
   props->group_size = NULL;
@@ -3286,6 +4365,7 @@ void fof_struct_dump(const struct fof_props *props, FILE *stream) {
   temp.group_index = NULL;
   temp.group_size = NULL;
   temp.group_mass = NULL;
+  temp.final_group_size = NULL;
   temp.group_centre_of_mass = NULL;
   temp.max_part_density_index = NULL;
   temp.max_part_density = NULL;
@@ -3299,6 +4379,8 @@ void fof_struct_restore(struct fof_props *props, FILE *stream) {
 
   restart_read_blocks((void *)props, sizeof(struct fof_props), 1, stream, NULL,
                       "fof_props");
+
+  fof_set_current_types(props);
 }
 
 #endif /* WITH_FOF */
diff --git a/src/fof.h b/src/fof.h
index bff0c0a3d0f0933866b7d3949ebf5b5f6bdc8b30..4496c233a2cf6b5fbb32ee6eabc2a5b7c1b1cd02 100644
--- a/src/fof.h
+++ b/src/fof.h
@@ -25,6 +25,7 @@
 /* Local headers */
 #include "align.h"
 #include "parser.h"
+#include "part_type.h"
 
 /* Avoid cyclic inclusions */
 struct cell;
@@ -68,6 +69,12 @@ struct fof_props {
   /*! The base name of the output file */
   char base_name[PARSER_MAX_LINE_SIZE];
 
+  /*! The types of particles to use for linking */
+  int fof_linking_types[swift_type_count];
+
+  /*! The types of particles to use for attaching */
+  int fof_attach_types[swift_type_count];
+
   /* ------------  Group properties ----------------- */
 
   /*! Number of groups */
@@ -80,9 +87,24 @@ struct fof_props {
   /*! Index of the root particle of the group a given gpart belongs to. */
   size_t *group_index;
 
+  /*! Index of the root particle of the group a given gpart is attached to. */
+  size_t *attach_index;
+
+  /*! Has the particle found a linkable to attach to? */
+  char *found_attachable_link;
+
+  /*! Is the group purely local after linking the foreign particles? */
+  char *is_purely_local;
+
+  /*! For attachable particles: distance to the current nearest linkable part */
+  float *distance_to_link;
+
   /*! Size of the group a given gpart belongs to. */
   size_t *group_size;
 
+  /*! Final size of the group a given gpart belongs to. */
+  long long *final_group_size;
+
   /*! Mass of the group a given gpart belongs to. */
   double *group_mass;
 
@@ -147,6 +169,7 @@ struct fof_final_index {
 struct fof_final_mass {
   size_t global_root;
   double group_mass;
+  long long final_group_size;
   double first_position[3];
   double centre_of_mass[3];
   long long max_part_density_index;
@@ -171,14 +194,22 @@ void fof_init(struct fof_props *props, struct swift_params *params,
               const struct phys_const *phys_const, const struct unit_system *us,
               const int stand_alone_fof);
 void fof_create_mpi_types(void);
-void fof_allocate(const struct space *s, const long long total_nr_DM_particles,
-                  struct fof_props *props);
-void fof_search_tree(struct fof_props *props,
-                     const struct black_holes_props *bh_props,
-                     const struct phys_const *constants,
-                     const struct cosmology *cosmo, struct space *s,
-                     const int dump_results, const int dump_debug_results,
-                     const int seed_black_holes);
+void fof_allocate(const struct space *s, struct fof_props *props);
+void fof_compute_local_sizes(struct fof_props *props, struct space *s);
+void fof_search_foreign_cells(struct fof_props *props, const struct space *s);
+void fof_link_attachable_particles(struct fof_props *props,
+                                   const struct space *s);
+void fof_finalise_attachables(struct fof_props *props, const struct space *s);
+void fof_link_foreign_fragments(struct fof_props *props, const struct space *s);
+void fof_build_list_of_purely_local_groups(struct fof_props *props,
+                                           const struct space *s);
+void fof_compute_group_props(struct fof_props *props,
+                             const struct black_holes_props *bh_props,
+                             const struct phys_const *constants,
+                             const struct cosmology *cosmo, struct space *s,
+                             const int dump_results,
+                             const int dump_debug_results,
+                             const int seed_black_holes);
 void rec_fof_search_self(const struct fof_props *props, const double dim[3],
                          const double search_r2, const int periodic,
                          const struct gpart *const space_gparts,
@@ -187,6 +218,16 @@ void rec_fof_search_pair(const struct fof_props *props, const double dim[3],
                          const double search_r2, const int periodic,
                          const struct gpart *const space_gparts,
                          struct cell *restrict ci, struct cell *restrict cj);
+void rec_fof_attach_self(const struct fof_props *props, const double dim[3],
+                         const double search_r2, const int periodic,
+                         const struct gpart *const space_gparts,
+                         const size_t nr_gparts, struct cell *c);
+void rec_fof_attach_pair(const struct fof_props *props, const double dim[3],
+                         const double search_r2, const int periodic,
+                         const struct gpart *const space_gparts,
+                         const size_t nr_gparts, struct cell *restrict ci,
+                         struct cell *restrict cj, const int ci_local,
+                         const int cj_local);
 void fof_struct_dump(const struct fof_props *props, FILE *stream);
 void fof_struct_restore(struct fof_props *props, FILE *stream);
 
diff --git a/src/fof_catalogue_io.c b/src/fof_catalogue_io.c
index 12308fb75102469890ba095b30627e1ab85c9026..e290341deb5d6af668c925ea413e66b984f49699 100644
--- a/src/fof_catalogue_io.c
+++ b/src/fof_catalogue_io.c
@@ -35,7 +35,8 @@
 void write_fof_hdf5_header(hid_t h_file, const struct engine* e,
                            const long long num_groups_total,
                            const long long num_groups_this_file,
-                           const struct fof_props* props) {
+                           const struct fof_props* props,
+                           const int virtual_file) {
 
   /* Open header to write simulation properties */
   hid_t h_grp =
@@ -64,7 +65,7 @@ void write_fof_hdf5_header(hid_t h_file, const struct engine* e,
   char systemname[256] = {0};
   if (e->nodeID == 0) sprintf(systemname, "%s", hostname());
 #ifdef WITH_MPI
-  MPI_Bcast(systemname, 256, MPI_CHAR, 0, MPI_COMM_WORLD);
+  if (!virtual_file) MPI_Bcast(systemname, 256, MPI_CHAR, 0, MPI_COMM_WORLD);
 #endif
   io_write_attribute_s(h_grp, "System", systemname);
   io_write_attribute(h_grp, "Shift", DOUBLE, e->s->initial_shift, 3);
@@ -117,7 +118,7 @@ void write_fof_hdf5_header(hid_t h_file, const struct engine* e,
   io_write_attribute_i(h_grp, "NumFilesPerSnapshot", e->nr_nodes);
   io_write_attribute_i(h_grp, "ThisFile", e->nodeID);
   io_write_attribute_s(h_grp, "SelectOutput", "Default");
-  io_write_attribute_i(h_grp, "Virtual", 0);
+  io_write_attribute_i(h_grp, "Virtual", virtual_file);
   const int to_write[swift_type_count] = {0};
   io_write_attribute(h_grp, "CanHaveTypes", INT, to_write, swift_type_count);
   io_write_attribute_s(h_grp, "OutputType", "FOF");
@@ -133,7 +134,236 @@ void write_fof_hdf5_header(hid_t h_file, const struct engine* e,
   ic_info_write_hdf5(e->ics_metadata, h_file);
 
   /* Write all the meta-data */
-  io_write_meta_data(h_file, e, e->internal_units, e->snapshot_units);
+  io_write_meta_data(h_file, e, e->internal_units, e->snapshot_units,
+                     /*fof=*/1);
+}
+
+void write_virtual_fof_hdf5_array(
+    const struct engine* e, hid_t grp, const char* fileName_base,
+    const char* partTypeGroupName, const struct io_props props,
+    const size_t N_total, const long long* N_counts,
+    const enum lossy_compression_schemes lossy_compression,
+    const struct unit_system* internal_units,
+    const struct unit_system* snapshot_units) {
+
+#if H5_VERSION_GE(1, 10, 0)
+
+  /* Create data space */
+  hid_t h_space;
+  if (N_total > 0)
+    h_space = H5Screate(H5S_SIMPLE);
+  else
+    h_space = H5Screate(H5S_NULL);
+
+  if (h_space < 0)
+    error("Error while creating data space for field '%s'.", props.name);
+
+  int rank = 0;
+  hsize_t shape[2];
+  hsize_t source_shape[2];
+  hsize_t start[2] = {0, 0};
+  hsize_t count[2];
+  if (props.dimension > 1) {
+    rank = 2;
+    shape[0] = N_total;
+    shape[1] = props.dimension;
+    source_shape[0] = 0;
+    source_shape[1] = props.dimension;
+    count[0] = 0;
+    count[1] = props.dimension;
+
+  } else {
+    rank = 1;
+    shape[0] = N_total;
+    shape[1] = 0;
+    source_shape[0] = 0;
+    source_shape[1] = 0;
+    count[0] = 0;
+    count[1] = 0;
+  }
+
+  /* Change shape of data space */
+  hid_t h_err = H5Sset_extent_simple(h_space, rank, shape, NULL);
+  if (h_err < 0)
+    error("Error while changing data space shape for field '%s'.", props.name);
+
+  /* Dataset type */
+  hid_t h_type = H5Tcopy(io_hdf5_type(props.type));
+
+  /* Dataset properties */
+  hid_t h_prop = H5Pcreate(H5P_DATASET_CREATE);
+
+  /* Create filters and set compression level if we have something to write */
+  char comp_buffer[32] = "None";
+
+  /* The name of the dataset to map to in the other files */
+  char source_dataset_name[256];
+  sprintf(source_dataset_name, "Groups/%s", props.name);
+
+  /* Construct a relative base name */
+  char fileName_relative_base[256];
+  int pos_last_slash = strlen(fileName_base) - 1;
+  for (/* */; pos_last_slash >= 0; --pos_last_slash)
+    if (fileName_base[pos_last_slash] == '/') break;
+
+  sprintf(fileName_relative_base, "%s", &fileName_base[pos_last_slash + 1]);
+
+  /* Create all the virtual mappings */
+  for (int i = 0; i < e->nr_nodes; ++i) {
+
+    /* Get the number of particles of this type written on this rank */
+    count[0] = N_counts[i];
+
+    /* Select the space in the virtual file */
+    h_err = H5Sselect_hyperslab(h_space, H5S_SELECT_SET, start, /*stride=*/NULL,
+                                count, /*block=*/NULL);
+    if (h_err < 0) error("Error selecting hyper-slab in the virtual file");
+
+    /* Select the space in the (already existing) source file */
+    source_shape[0] = count[0];
+    hid_t h_source_space = H5Screate_simple(rank, source_shape, NULL);
+    if (h_source_space < 0) error("Error creating space in the source file");
+
+    char fileName[1024];
+    sprintf(fileName, "%s.%d.hdf5", fileName_relative_base, i);
+
+    /* Make the virtual link */
+    h_err = H5Pset_virtual(h_prop, h_space, fileName, source_dataset_name,
+                           h_source_space);
+    if (h_err < 0) error("Error setting the virtual properties");
+
+    H5Sclose(h_source_space);
+
+    /* Move to the next slab (i.e. next file) */
+    start[0] += count[0];
+  }
+
+  /* Create virtual dataset */
+  const hid_t h_data = H5Dcreate(grp, props.name, h_type, h_space, H5P_DEFAULT,
+                                 h_prop, H5P_DEFAULT);
+  if (h_data < 0) error("Error while creating dataspace '%s'.", props.name);
+
+  /* Write unit conversion factors for this data set */
+  char buffer[FIELD_BUFFER_SIZE] = {0};
+  units_cgs_conversion_string(buffer, snapshot_units, props.units,
+                              props.scale_factor_exponent);
+  float baseUnitsExp[5];
+  units_get_base_unit_exponents_array(baseUnitsExp, props.units);
+  io_write_attribute_f(h_data, "U_M exponent", baseUnitsExp[UNIT_MASS]);
+  io_write_attribute_f(h_data, "U_L exponent", baseUnitsExp[UNIT_LENGTH]);
+  io_write_attribute_f(h_data, "U_t exponent", baseUnitsExp[UNIT_TIME]);
+  io_write_attribute_f(h_data, "U_I exponent", baseUnitsExp[UNIT_CURRENT]);
+  io_write_attribute_f(h_data, "U_T exponent", baseUnitsExp[UNIT_TEMPERATURE]);
+  io_write_attribute_f(h_data, "h-scale exponent", 0.f);
+  io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
+  io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
+  io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
+
+  /* Write the actual number this conversion factor corresponds to */
+  const double factor =
+      units_cgs_conversion_factor(snapshot_units, props.units);
+  io_write_attribute_d(
+      h_data,
+      "Conversion factor to CGS (not including cosmological corrections)",
+      factor);
+  io_write_attribute_d(
+      h_data,
+      "Conversion factor to physical CGS (including cosmological corrections)",
+      factor * pow(e->cosmology->a, props.scale_factor_exponent));
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (strlen(props.description) == 0)
+    error("Invalid (empty) description of the field '%s'", props.name);
+#endif
+
+  /* Write the full description */
+  io_write_attribute_s(h_data, "Description", props.description);
+
+  /* Close everything */
+  H5Tclose(h_type);
+  H5Pclose(h_prop);
+  H5Dclose(h_data);
+  H5Sclose(h_space);
+
+#endif
+}
+
+void write_fof_virtual_file(const struct fof_props* props,
+                            const size_t num_groups_total,
+                            const long long* N_counts, const struct engine* e) {
+#if H5_VERSION_GE(1, 10, 0)
+
+  /* Create the filename */
+  char file_name[512];
+  char file_name_base[512];
+  char subdir_name[265];
+  sprintf(subdir_name, "%s_%04i", props->base_name, e->snapshot_output_count);
+  const char* base = basename(subdir_name);
+  sprintf(file_name_base, "%s/%s", subdir_name, base);
+  sprintf(file_name, "%s/%s.hdf5", subdir_name, base);
+
+  /* Set the minimal API version to avoid issues with advanced features */
+  hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+  herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                    HDF5_HIGHEST_FILE_FORMAT_VERSION);
+  if (err < 0) error("Error setting the hdf5 API version");
+
+  /* Open HDF5 file with the chosen parameters */
+  hid_t h_file = H5Fcreate(file_name, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
+  if (h_file < 0) error("Error while opening file '%s'.", file_name);
+
+  /* Start by writing the header */
+  write_fof_hdf5_header(h_file, e, num_groups_total, num_groups_total, props,
+                        /*virtual_file=*/1);
+  hid_t h_grp =
+      H5Gcreate(h_file, "/Groups", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
+  if (h_grp < 0) error("Error while creating groups group.\n");
+
+  struct io_props output_prop;
+  output_prop = io_make_output_field_("Masses", DOUBLE, 1, UNIT_CONV_MASS, 0.f,
+                                      (char*)props->group_mass, sizeof(double),
+                                      "FOF group masses", /*physical=*/0,
+                                      /*convertible_to_comoving=*/1);
+  write_virtual_fof_hdf5_array(e, h_grp, file_name_base, "Groups", output_prop,
+                               num_groups_total, N_counts,
+                               compression_write_lossless, e->internal_units,
+                               e->snapshot_units);
+  output_prop =
+      io_make_output_field_("Centres", DOUBLE, 3, UNIT_CONV_LENGTH, 1.f,
+                            (char*)props->group_centre_of_mass,
+                            3 * sizeof(double), "FOF group centres of mass",
+                            /*physical=*/0, /*convertible_to_comoving=*/1);
+  write_virtual_fof_hdf5_array(e, h_grp, file_name_base, "Groups", output_prop,
+                               num_groups_total, N_counts,
+                               compression_write_lossless, e->internal_units,
+                               e->snapshot_units);
+  output_prop =
+      io_make_output_field_("GroupIDs", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
+                            (char*)props->group_index, sizeof(size_t),
+                            "FOF group IDs", /*physical=*/1,
+                            /*convertible_to_comoving=*/0);
+  write_virtual_fof_hdf5_array(e, h_grp, file_name_base, "Groups", output_prop,
+                               num_groups_total, N_counts,
+                               compression_write_lossless, e->internal_units,
+                               e->snapshot_units);
+  output_prop =
+      io_make_output_field_("Sizes", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
+                            (char*)props->final_group_size, sizeof(long long),
+                            "FOF group length (number of particles)",
+                            /*physical=*/1, /*convertible_to_comoving=*/0);
+  write_virtual_fof_hdf5_array(e, h_grp, file_name_base, "Groups", output_prop,
+                               num_groups_total, N_counts,
+                               compression_write_lossless, e->internal_units,
+                               e->snapshot_units);
+
+  /* Close everything */
+  H5Gclose(h_grp);
+  H5Fclose(h_file);
+  H5Pclose(h_props);
+#endif
 }
 
 void write_fof_hdf5_array(
@@ -261,6 +491,9 @@ void write_fof_hdf5_array(
   io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
   io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
   io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -291,7 +524,7 @@ void write_fof_hdf5_array(
 }
 
 void write_fof_hdf5_catalogue(const struct fof_props* props,
-                              const size_t num_groups, const struct engine* e) {
+                              long long num_groups, const struct engine* e) {
 
   char file_name[512];
 #ifdef WITH_MPI
@@ -307,7 +540,13 @@ void write_fof_hdf5_catalogue(const struct fof_props* props,
           e->snapshot_output_count);
 #endif
 
-  hid_t h_file = H5Fcreate(file_name, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+  /* Set the minimal API version to avoid issues with advanced features */
+  hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+  herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                    HDF5_HIGHEST_FILE_FORMAT_VERSION);
+  if (err < 0) error("Error setting the hdf5 API version");
+
+  hid_t h_file = H5Fcreate(file_name, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
   if (h_file < 0) error("Error while opening file '%s'.", file_name);
 
   /* Compute the number of groups */
@@ -316,10 +555,16 @@ void write_fof_hdf5_catalogue(const struct fof_props* props,
 #ifdef WITH_MPI
   MPI_Allreduce(&num_groups, &num_groups_total, 1, MPI_LONG_LONG, MPI_SUM,
                 MPI_COMM_WORLD);
+
+  /* Rank 0 collects the number of groups written by each rank */
+  long long* N_counts = (long long*)malloc(e->nr_nodes * sizeof(long long));
+  MPI_Gather(&num_groups_local, 1, MPI_LONG_LONG_INT, N_counts, 1,
+             MPI_LONG_LONG_INT, 0, MPI_COMM_WORLD);
 #endif
 
   /* Start by writing the header */
-  write_fof_hdf5_header(h_file, e, num_groups_total, num_groups_local, props);
+  write_fof_hdf5_header(h_file, e, num_groups_total, num_groups_local, props,
+                        /*virtual_file=*/0);
 
   hid_t h_grp =
       H5Gcreate(h_file, "/Groups", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
@@ -328,26 +573,32 @@ void write_fof_hdf5_catalogue(const struct fof_props* props,
   struct io_props output_prop;
   output_prop = io_make_output_field_("Masses", DOUBLE, 1, UNIT_CONV_MASS, 0.f,
                                       (char*)props->group_mass, sizeof(double),
-                                      "FOF group masses");
+                                      "FOF group masses", /*physical=*/0,
+                                      /*convertible_to_comoving=*/1);
   write_fof_hdf5_array(e, h_grp, file_name, "Groups", output_prop,
                        num_groups_local, compression_write_lossless,
                        e->internal_units, e->snapshot_units);
   output_prop =
       io_make_output_field_("Centres", DOUBLE, 3, UNIT_CONV_LENGTH, 1.f,
                             (char*)props->group_centre_of_mass,
-                            3 * sizeof(double), "FOF group centres of mass");
+                            3 * sizeof(double), "FOF group centres of mass",
+                            /*physical=*/0, /*convertible_to_comoving=*/1);
   write_fof_hdf5_array(e, h_grp, file_name, "Groups", output_prop,
                        num_groups_local, compression_write_lossless,
                        e->internal_units, e->snapshot_units);
-  output_prop = io_make_output_field_(
-      "GroupIDs", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-      (char*)props->group_index, sizeof(size_t), "FOF group IDs");
+  output_prop =
+      io_make_output_field_("GroupIDs", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
+                            (char*)props->group_index, sizeof(size_t),
+                            "FOF group IDs", /*physical=*/1,
+                            /*convertible_to_comoving=*/0);
   write_fof_hdf5_array(e, h_grp, file_name, "Groups", output_prop,
                        num_groups_local, compression_write_lossless,
                        e->internal_units, e->snapshot_units);
-  output_prop = io_make_output_field_(
-      "Sizes", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, (char*)props->group_size,
-      sizeof(size_t), "FOF group length (number of particles)");
+  output_prop =
+      io_make_output_field_("Sizes", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
+                            (char*)props->final_group_size, sizeof(long long),
+                            "FOF group length (number of particles)",
+                            /*physical=*/1, /*convertible_to_comoving=*/0);
   write_fof_hdf5_array(e, h_grp, file_name, "Groups", output_prop,
                        num_groups_local, compression_write_lossless,
                        e->internal_units, e->snapshot_units);
@@ -355,8 +606,19 @@ void write_fof_hdf5_catalogue(const struct fof_props* props,
   /* Close everything */
   H5Gclose(h_grp);
   H5Fclose(h_file);
+  H5Pclose(h_props);
 
 #ifdef WITH_MPI
+#if H5_VERSION_GE(1, 10, 0)
+
+  /* Write the virtual meta-file */
+  if (e->nodeID == 0)
+    write_fof_virtual_file(props, num_groups_total, N_counts, e);
+#endif
+
+  /* Free the counts-per-rank array */
+  free(N_counts);
+
   MPI_Barrier(MPI_COMM_WORLD);
 #endif
 }
diff --git a/src/fof_catalogue_io.h b/src/fof_catalogue_io.h
index 8162a52a596af37a9f582a28c01a95901ea1deb6..5050511bb0b97de21a60603801493afe630e6938 100644
--- a/src/fof_catalogue_io.h
+++ b/src/fof_catalogue_io.h
@@ -25,7 +25,7 @@
 #ifdef WITH_FOF
 
 void write_fof_hdf5_catalogue(const struct fof_props *props,
-                              const size_t num_groups, const struct engine *e);
+                              long long num_groups, const struct engine *e);
 
 #endif /* WITH_FOF */
 
diff --git a/src/forcing.c b/src/forcing.c
new file mode 100644
index 0000000000000000000000000000000000000000..2a1cfbea97315dfbb542ae4490b00ce4f0874ed0
--- /dev/null
+++ b/src/forcing.c
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+/* Config parameters. */
+#include <config.h>
+
+/* This object's header. */
+#include "forcing.h"
+#include "restart.h"
+
+/**
+ * @brief Write an forcing_terms struct to the given FILE as a stream of
+ * bytes.
+ *
+ * @param terms the struct
+ * @param stream the file stream
+ */
+void forcing_terms_struct_dump(const struct forcing_terms* terms,
+                               FILE* stream) {
+  restart_write_blocks((void*)terms, sizeof(struct forcing_terms), 1, stream,
+                       "forcingterms", "forcing terms");
+}
+
+/**
+ * @brief Restore a forcing_terms struct from the given FILE as a stream of
+ * bytes.
+ *
+ * @param terms the struct
+ * @param stream the file stream
+ */
+void forcing_terms_struct_restore(const struct forcing_terms* terms,
+                                  FILE* stream) {
+  restart_read_blocks((void*)terms, sizeof(struct forcing_terms), 1, stream,
+                      NULL, "forcing terms");
+}
diff --git a/src/forcing.h b/src/forcing.h
new file mode 100644
index 0000000000000000000000000000000000000000..a153fc318f941ff1be86ce5363626c06a0eb6ab6
--- /dev/null
+++ b/src/forcing.h
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023  Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_FORCING_H
+#define SWIFT_FORCING_H
+
+/**
+ * @file src/potential.h
+ * @brief Branches between the different external gravitational potentials.
+ */
+
+/* Config parameters. */
+#include <config.h>
+
+/* Import the right external potential definition */
+#if defined(FORCING_NONE)
+#include "./forcing/none/forcing.h"
+#elif defined(FORCING_ROBERTS_FLOW)
+#include "./forcing/roberts_flow/forcing.h"
+#elif defined(FORCING_ROBERTS_FLOW_ACCELERATION)
+#include "./forcing/roberts_flow_acceleration/forcing.h"
+#elif defined(FORCING_ABC_FLOW)
+#include "./forcing/ABC_flow/forcing.h"
+#else
+#error "Invalid choice of forcing terms"
+#endif
+
+void forcing_terms_struct_dump(const struct forcing_terms* terms, FILE* stream);
+void forcing_terms_struct_restore(const struct forcing_terms* terms,
+                                  FILE* stream);
+
+#endif /* SWIFT_FORCING_H */
diff --git a/src/forcing/ABC_flow/forcing.h b/src/forcing/ABC_flow/forcing.h
new file mode 100644
index 0000000000000000000000000000000000000000..148542cacdb307eb36f6f74d8e453e1d0f1eb0a8
--- /dev/null
+++ b/src/forcing/ABC_flow/forcing.h
@@ -0,0 +1,152 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ * Copyright (c) 2024 Nikyta Shchutksyi (shchutskyi@lorentz.leidenuniv.nl)
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_FORCING_ABC_FLOW_H
+#define SWIFT_FORCING_ABC_FLOW_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Standard includes. */
+#include <float.h>
+
+/* Local includes. */
+#include "error.h"
+#include "parser.h"
+#include "part.h"
+#include "physical_constants.h"
+#include "space.h"
+#include "units.h"
+
+/**
+ * @brief Forcing Term Properties
+ */
+struct forcing_terms {
+
+  /*! Reference velocity (internal units) */
+  float u0;
+
+  /*! Velocity scaling along the z direction */
+  float Vz_factor;
+
+  /*! Wavenumber of the flow */
+  float kv;
+
+  /*! ABC flow coefficients */
+  float A, B, C;
+};
+
+/**
+ * @brief Computes the forcing terms.
+ *
+ * Based on David Galloway (2012) ABC flows then and now, Geophysical &
+ * Astrophysical Fluid Dynamics, 106:4-5, 450-467 This version differs from the
+ * paper by imposing normalized velocity
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param s The #space we act on.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static void forcing_terms_apply(
+    const double time, const struct forcing_terms* terms, const struct space* s,
+    const struct phys_const* phys_const, struct part* p, struct xpart* xp) {
+
+  const double L = s->dim[0];
+  const float u0 = terms->u0;
+  const float A = terms->A;
+  const float B = terms->B;
+  const float C = terms->C;
+  const float Norm = 1 / sqrt(A * A + B * B + C * C);
+  const float Vz_factor = terms->Vz_factor;
+  const double k0 = (2. * M_PI / L) * terms->kv;
+  double v_ABC[3];
+
+  /* Eq. 2 of David Galloway (2012) ABC flows then and now, Geophysical &
+   * Astrophysical Fluid Dynamics, 106:4-5, 450-467 */
+  // Velocity normalized such that <v>rms = u0
+  v_ABC[0] = u0 * Norm * (A * sin(k0 * p->x[2]) + C * cos(k0 * p->x[1]));
+  v_ABC[1] = u0 * Norm * (B * sin(k0 * p->x[0]) + A * cos(k0 * p->x[2]));
+  v_ABC[2] = u0 * Norm * (C * sin(k0 * p->x[1]) + B * cos(k0 * p->x[0]));
+
+  /* Force the velocity and possibly scale the z-direction */
+  xp->v_full[0] = v_ABC[0];
+  xp->v_full[1] = v_ABC[1];
+  xp->v_full[2] = v_ABC[2] * Vz_factor;
+}
+
+/**
+ * @brief Computes the time-step condition due to the forcing terms.
+ *
+ * Nothing to do here. --> Return FLT_MAX.
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static float forcing_terms_timestep(
+    double time, const struct forcing_terms* terms,
+    const struct phys_const* phys_const, const struct part* p,
+    const struct xpart* xp) {
+
+  return FLT_MAX;
+}
+
+/**
+ * @brief Prints the properties of the forcing terms to stdout.
+ *
+ * @param terms The #forcing_terms properties of the run.
+ */
+static INLINE void forcing_terms_print(const struct forcing_terms* terms) {
+
+  message("Forcing terms is 'ABC flow'. U0: %.5f / Vz factor: %.5f.", terms->u0,
+          terms->Vz_factor);
+  message("Run using ABC parameters: A = %.5f, B = %.5f, C = %.5f", terms->A,
+          terms->B, terms->C);
+}
+
+/**
+ * @brief Initialises the forcing term properties
+ *
+ * @param parameter_file The parsed parameter file
+ * @param phys_const Physical constants in internal units
+ * @param us The current internal system of units
+ * @param s The #space object.
+ * @param terms The forcing term properties to initialize
+ */
+static INLINE void forcing_terms_init(struct swift_params* parameter_file,
+                                      const struct phys_const* phys_const,
+                                      const struct unit_system* us,
+                                      const struct space* s,
+                                      struct forcing_terms* terms) {
+
+  terms->u0 = parser_get_param_double(parameter_file, "ABC_Flow_Forcing:u0");
+  terms->Vz_factor = parser_get_opt_param_float(
+      parameter_file, "ABC_Flow_Forcing:Vz_factor", 1.f);
+  terms->kv = parser_get_param_double(parameter_file, "ABC_Flow_Forcing:kv");
+
+  terms->A = parser_get_param_double(parameter_file, "ABC_Flow_Forcing:A");
+  terms->B = parser_get_param_double(parameter_file, "ABC_Flow_Forcing:B");
+  terms->C = parser_get_param_double(parameter_file, "ABC_Flow_Forcing:C");
+}
+
+#endif /* SWIFT_FORCING_ABC_FLOW_H */
diff --git a/src/forcing/none/forcing.h b/src/forcing/none/forcing.h
new file mode 100644
index 0000000000000000000000000000000000000000..52b811ae9071476cdb38b1df758cfd491ff0d2c5
--- /dev/null
+++ b/src/forcing/none/forcing.h
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_FORCING_NONE_H
+#define SWIFT_FORCING_NONE_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Standard includes. */
+#include <float.h>
+
+/* Local includes. */
+#include "error.h"
+#include "parser.h"
+#include "part.h"
+#include "physical_constants.h"
+#include "space.h"
+#include "units.h"
+
+/**
+ * @brief Forcing Term Properties
+ */
+struct forcing_terms {};
+
+/**
+ * @brief Computes the forcing terms.
+ *
+ * We do nothing in this 'none' scheme.
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param s The #space we act on.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static void forcing_terms_apply(
+    const double time, const struct forcing_terms* terms, const struct space* s,
+    const struct phys_const* phys_const, struct part* p, struct xpart* xp) {
+  /* Nothing to do here */
+}
+
+/**
+ * @brief Computes the time-step condition due to the forcing terms.
+ *
+ * Nothing to do here. --> Return FLT_MAX.
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static float forcing_terms_timestep(
+    double time, const struct forcing_terms* terms,
+    const struct phys_const* phys_const, const struct part* p,
+    const struct xpart* xp) {
+
+  /* No time-step size limit */
+  return FLT_MAX;
+}
+
+/**
+ * @brief Prints the properties of the forcing terms to stdout.
+ *
+ * @param terms The #forcing_terms properties of the run.
+ */
+static INLINE void forcing_terms_print(const struct forcing_terms* terms) {
+
+  message("Forcing terms is 'No forcing terms'.");
+}
+
+/**
+ * @brief Initialises the forcing term properties
+ *
+ * Nothing to do here.
+ *
+ * @param parameter_file The parsed parameter file
+ * @param phys_const Physical constants in internal units
+ * @param us The current internal system of units
+ * @param s The #space object.
+ * @param terms The forcing term properties to initialize
+ */
+static INLINE void forcing_terms_init(struct swift_params* parameter_file,
+                                      const struct phys_const* phys_const,
+                                      const struct unit_system* us,
+                                      const struct space* s,
+                                      struct forcing_terms* terms) {
+
+  /* Nothing to do here */
+}
+
+#endif /* SWIFT_FORCING_NONE_H */
diff --git a/src/forcing/roberts_flow/forcing.h b/src/forcing/roberts_flow/forcing.h
new file mode 100644
index 0000000000000000000000000000000000000000..d525f55cf22fc51c74843b4025cbc9dd492a865d
--- /dev/null
+++ b/src/forcing/roberts_flow/forcing.h
@@ -0,0 +1,208 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_FORCING_ROBERTS_FLOW_H
+#define SWIFT_FORCING_ROBERTS_FLOW_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Standard includes. */
+#include <float.h>
+
+/* Local includes. */
+#include "error.h"
+#include "parser.h"
+#include "part.h"
+#include "physical_constants.h"
+#include "space.h"
+#include "units.h"
+
+/* Type of flow */
+enum flow {
+  Roberts_flow_1 = 1,
+  Roberts_flow_2 = 2,
+  Roberts_flow_3 = 3,
+  Roberts_flow_4 = 4
+};
+
+/**
+ * @brief Forcing Term Properties
+ */
+struct forcing_terms {
+
+  /*! Reference velocity (internal units) */
+  float u0;
+
+  /*! Velocity scaling along the z direction */
+  float Vz_factor;
+
+  /*! Kind of RobertsFlow */
+  enum flow Flow_kind;
+
+  /*! Wavenumber of the flow*/
+  float kv;
+};
+
+/**
+ * @brief Computes the forcing terms.
+ *
+ * Based on G.O.Roberts, Dynamo action of fluid motions with two-dimensional
+ * periodicity, Royal Society, Feb. 3, 1972;
+ * Tilgner & Brandenburg, 2008, MNRAS, 391, 1477;
+ * Brandenburg & Ntormousi, arxiv 2211.03476.
+ * This version differs from the paper by imposing the velocity directly rather
+ * than by giving the particles an acceleration.
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param s The #space we act on.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static void forcing_terms_apply(
+    const double time, const struct forcing_terms* terms, const struct space* s,
+    const struct phys_const* phys_const, struct part* p, struct xpart* xp) {
+
+  enum flow Flow_kind = terms->Flow_kind;
+  const double L = s->dim[0];
+  const float u0 = terms->u0;
+  const float Vz_factor = terms->Vz_factor;
+  const double k0 = (2. * M_PI / L) * terms->kv;
+  const double kf = M_SQRT2 * k0;
+  double v_Rob[3];
+
+  /* Switching between different kinds of flows */
+  /* Here we use definitions based on Brandenburg & Ntormousi, arxiv 2211.03476,
+   * SB3 and SB4 */
+  /* The difference between Roberts definitions and the definitions used here
+   * are the following: Roberts worked in yz plane, we work in xy plane just for
+   * convenience, so our formulas differ from Robets article by several
+   * rotations. Theese rotations are equivalent to yzx -> xyz permutation. Also
+   * Roberts cells are tilted 45 degrees and are sqrt(2) times larger We
+   * normalize flow in such way that the rms velocity is u0 (when Vzfactor=1)*/
+
+  switch (Flow_kind) {
+
+    case Roberts_flow_1:
+      /* Based on Brandenburg & Ntormousi, arxiv 2211.03476, SB4, flow 1 */
+
+      v_Rob[0] = u0 * sin(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      v_Rob[1] = -u0 * cos(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      v_Rob[2] = u0 * M_SQRT2 * sin(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      break;
+
+    case Roberts_flow_2:
+      /* Based on Brandenburg & Ntormousi, arxiv 2211.03476, SB4, flow 2 */
+
+      v_Rob[0] = u0 * sin(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      v_Rob[1] = -u0 * cos(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      v_Rob[2] = u0 * M_SQRT2 * cos(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      break;
+
+    case Roberts_flow_3:
+      /* Based on Brandenburg & Ntormousi, arxiv 2211.03476, SB4, flow 3 */
+
+      v_Rob[0] = u0 * sin(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      v_Rob[1] = -u0 * cos(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      v_Rob[2] =
+          u0 * M_SQRT1_2 * (cos(2 * k0 * p->x[0]) + cos(2 * k0 * p->x[1]));
+      break;
+
+    case Roberts_flow_4:
+      /* Based on Brandenburg & Ntormousi, arxiv 2211.03476, SB4, flow 4 */
+
+      v_Rob[0] = u0 * sin(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      v_Rob[1] = -u0 * cos(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      v_Rob[2] = u0 * sin(k0 * p->x[0]);
+      break;
+
+    default:
+
+      v_Rob[0] = 0.f;
+      v_Rob[1] = 0.f;
+      v_Rob[2] = 0.f;
+  }
+
+  /* Force the velocity and possibly scale the z-direction */
+  xp->v_full[0] = v_Rob[0];
+  xp->v_full[1] = v_Rob[1];
+  xp->v_full[2] = v_Rob[2] * Vz_factor;
+}
+
+/**
+ * @brief Computes the time-step condition due to the forcing terms.
+ *
+ * Nothing to do here. --> Return FLT_MAX.
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static float forcing_terms_timestep(
+    double time, const struct forcing_terms* terms,
+    const struct phys_const* phys_const, const struct part* p,
+    const struct xpart* xp) {
+
+  return FLT_MAX;
+}
+
+/**
+ * @brief Prints the properties of the forcing terms to stdout.
+ *
+ * @param terms The #forcing_terms properties of the run.
+ */
+static INLINE void forcing_terms_print(const struct forcing_terms* terms) {
+
+  message("Forcing terms is 'Roberts flow'. U0: %.5f / Vz factor: %.5f.",
+          terms->u0, terms->Vz_factor);
+  message("Forcing 'Roberts flow' Kind: %i .", terms->Flow_kind);
+}
+
+/**
+ * @brief Initialises the forcing term properties
+ *
+ * @param parameter_file The parsed parameter file
+ * @param phys_const Physical constants in internal units
+ * @param us The current internal system of units
+ * @param s The #space object.
+ * @param terms The forcing term properties to initialize
+ */
+static INLINE void forcing_terms_init(struct swift_params* parameter_file,
+                                      const struct phys_const* phys_const,
+                                      const struct unit_system* us,
+                                      const struct space* s,
+                                      struct forcing_terms* terms) {
+
+  terms->u0 = parser_get_param_double(parameter_file, "RobertsFlowForcing:u0");
+  terms->Vz_factor = parser_get_opt_param_float(
+      parameter_file, "RobertsFlowForcing:Vz_factor", 1.f);
+  terms->Flow_kind =
+      parser_get_param_int(parameter_file, "RobertsFlowForcing:Flow_kind");
+  terms->kv = parser_get_param_double(parameter_file, "RobertsFlowForcing:kv");
+
+  if (terms->Flow_kind > 4 || terms->Flow_kind < 1)
+    error(
+        "Error: Flow_kind variable can take integer values from [1,4] "
+        "interval.");
+}
+
+#endif /* SWIFT_FORCING_ROBERTS_FLOW_H */
diff --git a/src/forcing/roberts_flow_acceleration/forcing.h b/src/forcing/roberts_flow_acceleration/forcing.h
new file mode 100644
index 0000000000000000000000000000000000000000..d739e7e3a9ed3538dd84c1fc59f933fec80d6353
--- /dev/null
+++ b/src/forcing/roberts_flow_acceleration/forcing.h
@@ -0,0 +1,231 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_FORCING_ROBERTS_FLOW_ACCELERATION_H
+#define SWIFT_FORCING_ROBERTS_FLOW_ACCELERATION_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Standard includes. */
+#include <float.h>
+
+/* Local includes. */
+#include "error.h"
+#include "hydro.h"
+#include "parser.h"
+#include "part.h"
+#include "physical_constants.h"
+#include "space.h"
+#include "units.h"
+
+/* Type of flow */
+enum flow {
+  Brandenburg_flow,
+  Roberts_flow_1,
+  Roberts_flow_2,
+  Roberts_flow_3,
+  Roberts_flow_4
+};
+
+/**
+ * @brief Forcing Term Properties
+ */
+struct forcing_terms {
+
+  /*! Reference velocity (internal units) */
+  float u0;
+
+  /*! 'viscosity' to convert from velocity to acceleration (internal units) */
+  float nu;
+
+  /*! Velocity scaling along the z direction */
+  float Vz_factor;
+
+  /*! Kind of RobertsFlow */
+  enum flow Flow_kind;
+
+  /*! Wavenumber of the flow*/
+  float kv;
+};
+
+/**
+ * @brief Computes the forcing terms.
+ *
+ * Based on Tilgner & Brandenburg, 2008, MNRAS, 391, 1477
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param s The #space we act on.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static void forcing_terms_apply(
+    const double time, const struct forcing_terms* terms, const struct space* s,
+    const struct phys_const* phys_const, struct part* p, struct xpart* xp) {
+
+  enum flow Flow_kind = terms->Flow_kind;
+  const double L = s->dim[0];
+  const float u0 = terms->u0;
+  const float c_s = hydro_get_comoving_soundspeed(p);
+
+  /* Effective viscosity from artificial viscosity, as in eq. 100 from
+   * arXiv:1012.1885 */
+  const float nu = terms->nu * p->viscosity.alpha * c_s * p->h /
+                   (2.f * (hydro_dimension + 2.f));
+
+  const float Vz_factor = terms->Vz_factor;
+  const double k0 = (2. * M_PI / L) * terms->kv;
+  const double kf = M_SQRT2 * k0;
+  double v_Rob[3];
+  double Psi;
+
+  switch (Flow_kind) {
+
+    case Brandenburg_flow:
+      /* Eq. 8 of Tilgner & Brandenburg, 2008, MNRAS, 391, 1477 */
+      // Psi = (u0 / k0) * cos(k0 * p->x[0]) * cos(k0 * p->x[1]);
+
+      /* Eq. 7 of Tilgner & Brandenburg, 2008, MNRAS, 391, 1477 */
+      // v_Rob[0] = u0 * cos(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      // v_Rob[1] = -u0 * sin(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      // v_Rob[2] = kf * Psi;
+
+      // Velocity used to compare with A.B. runs (from overleaf)
+      Psi = (u0 / k0) * sin(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      v_Rob[0] = u0 * sin(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      v_Rob[1] = -u0 * cos(k0 * p->x[0]) * sin(k0 * p->x[1]);
+      v_Rob[2] = kf * Psi;
+
+      break;
+
+    case Roberts_flow_1:
+      /* Eq. 5.1 of Roberts, Feb. 3, 1972, Vol. 271, No. 1216 (Feb. 3, 1972),
+       * pp. 411-454.*/
+      v_Rob[0] = u0 * sin(k0 * p->x[0]);
+      v_Rob[1] = u0 * sin(k0 * p->x[1]);
+      v_Rob[2] = u0 * (cos(k0 * p->x[0]) - cos(k0 * p->x[1]));
+      break;
+
+    case Roberts_flow_2:
+      /* Eq. 6.1 of Roberts, Feb. 3, 1972, Vol. 271, No. 1216 (Feb. 3, 1972),
+       * pp. 411-454.*/
+      v_Rob[0] = u0 * sin(k0 * p->x[0]);
+      v_Rob[1] = u0 * sin(k0 * p->x[1]);
+      v_Rob[2] = u0 * (cos(k0 * p->x[0]) + cos(k0 * p->x[1]));
+      break;
+
+    case Roberts_flow_3:
+      /* Eq. 6.2 of Roberts, Feb. 3, 1972, Vol. 271, No. 1216 (Feb. 3, 1972),
+       * pp. 411-454.*/
+      v_Rob[0] = u0 * sin(k0 * p->x[0]);
+      v_Rob[1] = u0 * sin(k0 * p->x[1]);
+      v_Rob[2] = u0 * 2 * cos(k0 * p->x[0]) * cos(k0 * p->x[1]);
+      break;
+
+    case Roberts_flow_4:
+      /* Eq. 6.3 of Roberts, Feb. 3, 1972, Vol. 271, No. 1216 (Feb. 3, 1972),
+       * pp. 411-454.*/
+      v_Rob[0] = u0 * sin(k0 * p->x[0]);
+      v_Rob[1] = u0 * sin(k0 * p->x[1]);
+      v_Rob[2] = u0 * sin(k0 * (p->x[0] + p->x[1]));
+      break;
+
+    default:
+
+      v_Rob[0] = 0.f;
+      v_Rob[1] = 0.f;
+      v_Rob[2] = 0.f;
+  }
+
+  /* Eq. 6 of Tilgner & Brandenburg, 2008, MNRAS, 391, 1477 */
+  const double f[3] = {nu * kf * kf * v_Rob[0], nu * kf * kf * v_Rob[1],
+                       nu * kf * kf * v_Rob[2]};
+
+  /* Update the accelerations */
+  p->a_hydro[0] += f[0];
+  p->a_hydro[1] += f[1];
+  p->a_hydro[2] += f[2] * Vz_factor;
+}
+
+/**
+ * @brief Computes the time-step condition due to the forcing terms.
+ *
+ * Nothing to do here. --> Return FLT_MAX.
+ *
+ * @param time The current time.
+ * @param terms The properties of the forcing terms.
+ * @param phys_const The physical constants in internal units.
+ * @param p Pointer to the particle data.
+ * @param xp Pointer to the extended particle data.
+ */
+__attribute__((always_inline)) INLINE static float forcing_terms_timestep(
+    double time, const struct forcing_terms* terms,
+    const struct phys_const* phys_const, const struct part* p,
+    const struct xpart* xp) {
+
+  return FLT_MAX;
+}
+
+/**
+ * @brief Prints the properties of the forcing terms to stdout.
+ *
+ * @param terms The #forcing_terms properties of the run.
+ */
+static INLINE void forcing_terms_print(const struct forcing_terms* terms) {
+
+  message(
+      "Forcing terms is 'Roberts flow using accelerations'. U0: %.5f. nu: "
+      "%.5f. Vz factor: %.5f.",
+      terms->u0, terms->nu, terms->Vz_factor);
+  message("Forcing 'Roberts flow' Kind: %i .", terms->Flow_kind);
+}
+
+/**
+ * @brief Initialises the forcing term properties
+ *
+ * @param parameter_file The parsed parameter file
+ * @param phys_const Physical constants in internal units
+ * @param us The current internal system of units
+ * @param s The #space object.
+ * @param terms The forcing term properties to initialize
+ */
+static INLINE void forcing_terms_init(struct swift_params* parameter_file,
+                                      const struct phys_const* phys_const,
+                                      const struct unit_system* us,
+                                      const struct space* s,
+                                      struct forcing_terms* terms) {
+
+  terms->u0 = parser_get_param_double(parameter_file,
+                                      "RobertsFlowAccelerationForcing:u0");
+  terms->nu = parser_get_param_double(parameter_file,
+                                      "RobertsFlowAccelerationForcing:nu");
+  terms->Vz_factor = parser_get_opt_param_float(
+      parameter_file, "RobertsFlowAccelerationForcing:Vz_factor", 1.f);
+  terms->Flow_kind =
+      parser_get_param_int(parameter_file, "RobertsFlowForcing:Flow_kind");
+  terms->kv = parser_get_param_double(parameter_file, "RobertsFlowForcing:kv");
+
+  if (terms->Flow_kind > 4 || terms->Flow_kind < 0)
+    error(
+        "Error: Flow_kind variable can take integer values from [0,4] "
+        "interval.");
+}
+
+#endif /* SWIFT_FORCING_ROBERTS_FLOW_ACCELERATION_H */
diff --git a/src/hydro/Shadowswift/voronoi_algorithm.h b/src/fvpm_geometry.h
similarity index 66%
rename from src/hydro/Shadowswift/voronoi_algorithm.h
rename to src/fvpm_geometry.h
index 19ecc723741e6c91b19e85f6457311a80c6faf34..847e241501fae64f5f001d9a0b936de8487059d2 100644
--- a/src/hydro/Shadowswift/voronoi_algorithm.h
+++ b/src/fvpm_geometry.h
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
+ * Copyright (c) 2024 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,18 +16,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
+#ifndef SWIFT_FVPM_GEOMETRY_H
+#define SWIFT_FVPM_GEOMETRY_H
 
-#ifndef SWIFT_VORONOI_ALGORITHM_H
-#define SWIFT_VORONOI_ALGORITHM_H
+/* Config parameters. */
+#include <config.h>
 
-#if defined(HYDRO_DIMENSION_1D)
-#include "voronoi1d_algorithm.h"
-#elif defined(HYDRO_DIMENSION_2D)
-#include "voronoi2d_algorithm.h"
-#elif defined(HYDRO_DIMENSION_3D)
-#include "voronoi3d_algorithm.h"
+/* Import the right FVPM geometry functions */
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(RT_GEAR)
+#include "./fvpm_geometry/Gizmo/fvpm_geometry.h"
 #else
-#error "You have to select a dimension for the hydro!"
+#include "./fvpm_geometry/None/fvpm_geometry.h"
 #endif
 
-#endif  // SWIFT_VORONOI_ALGORITHM_H
+#endif /* SWIFT_FVPM_GEOMETRY_H */
diff --git a/src/fvpm_geometry/Gizmo/MFM/fvpm_geometry.h b/src/fvpm_geometry/Gizmo/MFM/fvpm_geometry.h
new file mode 100644
index 0000000000000000000000000000000000000000..8dedc8b88bebc99684de4568390ff281f0787b07
--- /dev/null
+++ b/src/fvpm_geometry/Gizmo/MFM/fvpm_geometry.h
@@ -0,0 +1,46 @@
+#ifndef SWIFT_FVPM_GEOMETRY_GIZMO_MFM_H
+#define SWIFT_FVPM_GEOMETRY_GIZMO_MFM_H
+
+/**
+ * @brief Reset the variables used to store the centroid; used for the velocity
+ * correction.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_reset_centroids(
+    struct part* restrict p) {}
+
+/**
+ * @brief Normalise the centroids after the density loop.
+ *
+ * @param p Particle.
+ * @param wcount Wcount for the particle. This is an explicit argument, so that
+ * it is clear from the code that wcount needs to be normalised by the time it
+ * is used here.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_normalise_centroid(
+    struct part* restrict p, const float wcount) {}
+
+/**
+ * @brief Update the centroid with the given contribution, assuming the particle
+ * acts as the left particle in the neighbour interaction.
+ *
+ * @param p Particle (pi).
+ * @param dx Distance vector between the particle and its neighbour (dx = pi->x
+ * - pj->x).
+ * @param w Kernel value at position pj->x.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_update_centroid_left(
+    struct part* restrict p, const float* dx, const float w) {}
+
+/**
+ * @brief Update the centroid with the given contribution, assuming the particle
+ * acts as the right particle in the neighbour interaction.
+ *
+ * @param p Particle (pj).
+ * @param dx Distance vector between the particle and its neighbour (dx = pi->x
+ * - pj->x).
+ * @param w Kernel value at position pi->x.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_update_centroid_right(
+    struct part* restrict p, const float* dx, const float w) {}
+
+#endif /* SWIFT_FVPM_GEOMETRY_GIZMO_MFM_H */
diff --git a/src/fvpm_geometry/Gizmo/MFV/fvpm_geometry.h b/src/fvpm_geometry/Gizmo/MFV/fvpm_geometry.h
new file mode 100644
index 0000000000000000000000000000000000000000..f5976bd201683735ac63a99be0baa7d98eb17bc1
--- /dev/null
+++ b/src/fvpm_geometry/Gizmo/MFV/fvpm_geometry.h
@@ -0,0 +1,69 @@
+#ifndef SWIFT_FVPM_GEOMETRY_GIZMO_MFV_H
+#define SWIFT_FVPM_GEOMETRY_GIZMO_MFV_H
+
+#include "const.h"
+
+/**
+ * @brief Reset the variables used to store the centroid; used for the velocity
+ * correction.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_reset_centroids(
+    struct part* restrict p) {
+
+  p->geometry.centroid[0] = 0.0f;
+  p->geometry.centroid[1] = 0.0f;
+  p->geometry.centroid[2] = 0.0f;
+}
+
+/**
+ * @brief Normalise the centroids after the density loop.
+ *
+ * @param p Particle.
+ * @param wcount Wcount for the particle. This is an explicit argument, so that
+ * it is clear from the code that wcount needs to be normalised by the time it
+ * is used here.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_normalise_centroid(
+    struct part* restrict p, const float wcount) {
+
+  const float norm = kernel_norm / wcount;
+  p->geometry.centroid[0] *= norm;
+  p->geometry.centroid[1] *= norm;
+  p->geometry.centroid[2] *= norm;
+}
+
+/**
+ * @brief Update the centroid with the given contribution, assuming the particle
+ * acts as the left particle in the neighbour interaction.
+ *
+ * @param p Particle (pi).
+ * @param dx Distance vector between the particle and its neighbour (dx = pi->x
+ * - pj->x).
+ * @param w Kernel value at position pj->x.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_update_centroid_left(
+    struct part* restrict p, const float* dx, const float w) {
+
+  p->geometry.centroid[0] -= dx[0] * w;
+  p->geometry.centroid[1] -= dx[1] * w;
+  p->geometry.centroid[2] -= dx[2] * w;
+}
+
+/**
+ * @brief Update the centroid with the given contribution, assuming the particle
+ * acts as the right particle in the neighbour interaction.
+ *
+ * @param p Particle (pj).
+ * @param dx Distance vector between the particle and its neighbour (dx = pi->x
+ * - pj->x).
+ * @param w Kernel value at position pi->x.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_update_centroid_right(
+    struct part* restrict p, const float* dx, const float w) {
+
+  p->geometry.centroid[0] += dx[0] * w;
+  p->geometry.centroid[1] += dx[1] * w;
+  p->geometry.centroid[2] += dx[2] * w;
+}
+
+#endif /* SWIFT_FVPM_GEOMETRY_GIZMO_MFV_H */
diff --git a/src/fvpm_geometry/Gizmo/fvpm_geometry.h b/src/fvpm_geometry/Gizmo/fvpm_geometry.h
new file mode 100644
index 0000000000000000000000000000000000000000..951a3c7fb2ec5185034a4a3cdac83436f7456b53
--- /dev/null
+++ b/src/fvpm_geometry/Gizmo/fvpm_geometry.h
@@ -0,0 +1,153 @@
+#ifndef SWIFT_FVPM_GEOMETRY_GIZMO_H
+#define SWIFT_FVPM_GEOMETRY_GIZMO_H
+
+#include "const.h"
+#include "part.h"
+
+#include <config.h>
+
+/**
+ * @file Gizmo/fvpm_geometry.h
+ * @brief Functions related to the Gizmo FVPM geometry struct collection,
+ * in particular the collection of the data required for the matrix needed
+ * for gradients.
+ * This was moved here so we can cleanly couple GEAR-RT on top of SPH
+ * hydrodynamics while avoiding code replication.
+ */
+
+#if defined(RT_GEAR) && defined(GIZMO_MFM_SPH)
+/* Some functions clash here. MFM resets and does some geometry centroid
+ * stuff, while GEAR-RT, which uses MFV, doesn't. So we'd need to split the
+ * functions for RT and for hydro use.
+ * However, it is very unlikely we'll ever actually use that combination,
+ * so leaving it as-is for now. */
+#error "Combining GIZMO MFM and GEAR-RT not implemented yet."
+#endif
+
+#if defined(GIZMO_MFV_SPH) || defined(RT_GEAR)
+#include "./MFV/fvpm_geometry.h"
+#elif defined(GIZMO_MFM_SPH)
+#include "./MFM/fvpm_geometry.h"
+#endif
+
+/**
+ * @brief Check if the gradient matrix for this particle is well behaved.
+ *
+ * @param p Particle.
+ * @return 1 if the gradient matrix is well behaved, 0 otherwise.
+ */
+__attribute__((always_inline)) INLINE static int
+fvpm_part_geometry_well_behaved(const struct part* restrict p) {
+
+  return p->geometry.wcorr > const_gizmo_min_wcorr;
+}
+
+/**
+ * @brief Collect the data needed for the matrix construction.
+ */
+__attribute__((always_inline)) INLINE static void
+fvpm_accumulate_geometry_and_matrix(struct part* restrict pi, const float wi,
+                                    const float dx[3]) {
+  /* these are eqns. (1) and (2) in the Gizmo theory summary */
+  pi->geometry.volume += wi;
+  for (int k = 0; k < 3; k++)
+    for (int l = 0; l < 3; l++)
+      pi->geometry.matrix_E[k][l] += dx[k] * dx[l] * wi;
+}
+
+__attribute__((always_inline)) INLINE static void fvpm_geometry_init(
+    struct part* restrict p) {
+
+  p->geometry.volume = 0.0f;
+  p->geometry.matrix_E[0][0] = 0.0f;
+  p->geometry.matrix_E[0][1] = 0.0f;
+  p->geometry.matrix_E[0][2] = 0.0f;
+  p->geometry.matrix_E[1][0] = 0.0f;
+  p->geometry.matrix_E[1][1] = 0.0f;
+  p->geometry.matrix_E[1][2] = 0.0f;
+  p->geometry.matrix_E[2][0] = 0.0f;
+  p->geometry.matrix_E[2][1] = 0.0f;
+  p->geometry.matrix_E[2][2] = 0.0f;
+
+  /* reset the centroid variables used for the velocity correction in MFV */
+  fvpm_reset_centroids(p);
+}
+
+/**
+ * @brief Finish the computation of the matrix.
+ *
+ * @param p the particle to work on
+ * @param ihdim 1/h^{dim}
+ */
+__attribute__((always_inline)) INLINE static void
+fvpm_compute_volume_and_matrix(struct part* restrict p, const float ihdim) {
+
+  /* Final operation on the geometry. */
+  /* we multiply with the smoothing kernel normalization ih3 and calculate the
+   * volume */
+  const float volume_inv = ihdim * (p->geometry.volume + kernel_root);
+  const float volume = 1.0f / volume_inv;
+  p->geometry.volume = volume;
+
+  /* we multiply with the smoothing kernel normalization */
+  p->geometry.matrix_E[0][0] *= ihdim;
+  p->geometry.matrix_E[0][1] *= ihdim;
+  p->geometry.matrix_E[0][2] *= ihdim;
+  p->geometry.matrix_E[1][0] *= ihdim;
+  p->geometry.matrix_E[1][1] *= ihdim;
+  p->geometry.matrix_E[1][2] *= ihdim;
+  p->geometry.matrix_E[2][0] *= ihdim;
+  p->geometry.matrix_E[2][1] *= ihdim;
+  p->geometry.matrix_E[2][2] *= ihdim;
+
+  /* normalise the centroids for MFV */
+  fvpm_normalise_centroid(p, p->density.wcount);
+
+  /* Check the condition number to see if we have a stable geometry. */
+  const float condition_number_E =
+      p->geometry.matrix_E[0][0] * p->geometry.matrix_E[0][0] +
+      p->geometry.matrix_E[0][1] * p->geometry.matrix_E[0][1] +
+      p->geometry.matrix_E[0][2] * p->geometry.matrix_E[0][2] +
+      p->geometry.matrix_E[1][0] * p->geometry.matrix_E[1][0] +
+      p->geometry.matrix_E[1][1] * p->geometry.matrix_E[1][1] +
+      p->geometry.matrix_E[1][2] * p->geometry.matrix_E[1][2] +
+      p->geometry.matrix_E[2][0] * p->geometry.matrix_E[2][0] +
+      p->geometry.matrix_E[2][1] * p->geometry.matrix_E[2][1] +
+      p->geometry.matrix_E[2][2] * p->geometry.matrix_E[2][2];
+
+  float condition_number = 0.0f;
+  if (invert_dimension_by_dimension_matrix(p->geometry.matrix_E) != 0) {
+    /* something went wrong in the inversion; force bad condition number */
+    condition_number = const_gizmo_max_condition_number + 1.0f;
+  } else {
+    const float condition_number_Einv =
+        p->geometry.matrix_E[0][0] * p->geometry.matrix_E[0][0] +
+        p->geometry.matrix_E[0][1] * p->geometry.matrix_E[0][1] +
+        p->geometry.matrix_E[0][2] * p->geometry.matrix_E[0][2] +
+        p->geometry.matrix_E[1][0] * p->geometry.matrix_E[1][0] +
+        p->geometry.matrix_E[1][1] * p->geometry.matrix_E[1][1] +
+        p->geometry.matrix_E[1][2] * p->geometry.matrix_E[1][2] +
+        p->geometry.matrix_E[2][0] * p->geometry.matrix_E[2][0] +
+        p->geometry.matrix_E[2][1] * p->geometry.matrix_E[2][1] +
+        p->geometry.matrix_E[2][2] * p->geometry.matrix_E[2][2];
+
+    condition_number =
+        hydro_dimension_inv * sqrtf(condition_number_E * condition_number_Einv);
+  }
+
+  if (condition_number > const_gizmo_max_condition_number &&
+      p->geometry.wcorr > const_gizmo_min_wcorr) {
+#ifdef GIZMO_PATHOLOGICAL_ERROR
+    error("Condition number larger than %g (%g)!",
+          const_gizmo_max_condition_number, condition_number);
+#endif
+#ifdef GIZMO_PATHOLOGICAL_WARNING
+    message("Condition number too large: %g (> %g, p->id: %llu)!",
+            condition_number, const_gizmo_max_condition_number, p->id);
+#endif
+    /* add a correction to the number of neighbours for this particle */
+    p->geometry.wcorr = const_gizmo_w_correction_factor * p->geometry.wcorr;
+  }
+}
+
+#endif /* SWIFT_FVPM_GEOMETRY_GIZMO_H */
diff --git a/src/fvpm_geometry/Gizmo/fvpm_geometry_struct.h b/src/fvpm_geometry/Gizmo/fvpm_geometry_struct.h
new file mode 100644
index 0000000000000000000000000000000000000000..7f3e8cb2cabc8a355d2d8690eefe1f7b53147eee
--- /dev/null
+++ b/src/fvpm_geometry/Gizmo/fvpm_geometry_struct.h
@@ -0,0 +1,30 @@
+#ifndef SWIFT_FVPM_GEOMETRY_STRUCT_GIZMO_H
+#define SWIFT_FVPM_GEOMETRY_STRUCT_GIZMO_H
+
+/**
+ * @file Gizmo/fvpm_geometry_struct.h
+ * @brief Struct related to the Gizmo FVPM geometry particle data collection,
+ * in particular the collection of the data required for the matrix needed
+ * for gradients.
+ * This was moved here so we can cleanly couple GEAR-RT on top of SPH
+ * hydrodynamics while avoiding code replication.
+ */
+
+/* Geometrical quantities used for hydro. */
+struct fvpm_geometry_struct {
+
+  /* Volume of the particle. */
+  float volume;
+
+  /* Geometrical shear matrix used to calculate second order accurate
+     gradients */
+  float matrix_E[3][3];
+
+  /* Centroid of the "cell". */
+  float centroid[3];
+
+  /* Correction factor for wcount. */
+  float wcorr;
+};
+
+#endif /* SWIFT_FVPM_GEOMETRY_STRUCT_GIZMO_H */
diff --git a/src/fvpm_geometry/None/fvpm_geometry.h b/src/fvpm_geometry/None/fvpm_geometry.h
new file mode 100644
index 0000000000000000000000000000000000000000..eabc7edee719eb677cdaaa8eb77f16a5ebb9a545
--- /dev/null
+++ b/src/fvpm_geometry/None/fvpm_geometry.h
@@ -0,0 +1,83 @@
+#ifndef SWIFT_FVPM_GEOMETRY_NONE_H
+#define SWIFT_FVPM_GEOMETRY_NONE_H
+
+/**
+ * @file None/fvpm_geometry.h
+ * @brief Functions related to the Gizmo FVPM geometry struct collection,
+ * in particular the collection of the data required for the matrix needed
+ * for gradients. Empty definitions for when FVPM are unused.
+ */
+
+/**
+ * @brief Reset the variables used to store the centroid; used for the velocity
+ * correction.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_reset_centroids(
+    struct part* restrict p) {}
+
+/**
+ * @brief Normalise the centroids after the density loop.
+ *
+ * @param p Particle.
+ * @param wcount Wcount for the particle. This is an explicit argument, so that
+ * it is clear from the code that wcount needs to be normalised by the time it
+ * is used here.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_normalise_centroid(
+    struct part* restrict p, const float wcount) {}
+
+/**
+ * @brief Update the centroid with the given contribution, assuming the particle
+ * acts as the left particle in the neighbour interaction.
+ *
+ * @param p Particle (pi).
+ * @param dx Distance vector between the particle and its neighbour (dx = pi->x
+ * - pj->x).
+ * @param w Kernel value at position pj->x.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_update_centroid_left(
+    struct part* restrict p, const float* dx, const float w) {}
+
+/**
+ * @brief Update the centroid with the given contribution, assuming the particle
+ * acts as the right particle in the neighbour interaction.
+ *
+ * @param p Particle (pj).
+ * @param dx Distance vector between the particle and its neighbour (dx = pi->x
+ * - pj->x).
+ * @param w Kernel value at position pi->x.
+ */
+__attribute__((always_inline)) INLINE static void fvpm_update_centroid_right(
+    struct part* restrict p, const float* dx, const float w) {}
+
+/**
+ * @brief Check if the gradient matrix for this particle is well behaved.
+ *
+ * @param p Particle.
+ * @return 1 if the gradient matrix is well behaved, 0 otherwise.
+ */
+__attribute__((always_inline)) INLINE static int
+fvpm_part_geometry_well_behaved(const struct part* restrict p) {
+  return 0;
+}
+
+/**
+ * @brief Collect the data needed for the matrix construction.
+ */
+__attribute__((always_inline)) INLINE static void
+fvpm_accumulate_geometry_and_matrix(struct part* restrict pi, const float wi,
+                                    const float dx[3]) {}
+
+__attribute__((always_inline)) INLINE static void fvpm_geometry_init(
+    struct part* restrict p) {}
+
+/**
+ * @brief Finish the computation of the matrix.
+ *
+ * @param p the particle to work on
+ * @param ihdim 1/h^{dim}
+ */
+__attribute__((always_inline)) INLINE static void
+fvpm_compute_volume_and_matrix(struct part* restrict p, const float ihdim) {}
+
+#endif /* SWIFT_FVPM_GEOMETRY_NONE_H */
diff --git a/src/fvpm_geometry/None/fvpm_geometry_struct.h b/src/fvpm_geometry/None/fvpm_geometry_struct.h
new file mode 100644
index 0000000000000000000000000000000000000000..db00534b56b6517962198906359a06056c8cfd7a
--- /dev/null
+++ b/src/fvpm_geometry/None/fvpm_geometry_struct.h
@@ -0,0 +1,14 @@
+#ifndef SWIFT_FVPM_GEOMETRY_STRUCT_NONE_H
+#define SWIFT_FVPM_GEOMETRY_STRUCT_NONE_H
+
+/**
+ * @file fvpm_geometry_struct.h
+ * @brief Struct related to the Gizmo FVPM geometry particle data collection,
+ * in particular the collection of the data required for the matrix needed
+ * for gradients. Empty definitions for when FVPM are unused.
+ */
+
+/* Geometrical quantities used for hydro. */
+struct fvpm_geometry_struct {};
+
+#endif /* SWIFT_FVPM_GEOMETRY_STRUCT_NONE_H */
diff --git a/src/fvpm_geometry_struct.h b/src/fvpm_geometry_struct.h
new file mode 100644
index 0000000000000000000000000000000000000000..9cd32b7846423fa5e80abb20c3e9a825d8db183c
--- /dev/null
+++ b/src/fvpm_geometry_struct.h
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_FVPM_GEOMETRY_STRUCT_H
+#define SWIFT_FVPM_GEOMETRY_STRUCT_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Import the right geometry struct definition */
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(RT_GEAR)
+#include "./fvpm_geometry/Gizmo/fvpm_geometry_struct.h"
+#else
+#include "./fvpm_geometry/None/fvpm_geometry_struct.h"
+#endif
+
+#endif /* SWIFT_FVPM_GEOMETRY_STRUCT_H */
diff --git a/src/gravity/Default/gravity_io.h b/src/gravity/Default/gravity_io.h
index 9cd4f1e051f9e24698949b98292f6af467626edd..cb1887fe120ca99752e52430db8d9c8af24bf4e9 100644
--- a/src/gravity/Default/gravity_io.h
+++ b/src/gravity/Default/gravity_io.h
@@ -135,9 +135,10 @@ INLINE static void darkmatter_write_particles(const struct gpart* gparts,
   list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                                  gparts, mass, "Masses of the particles");
 
-  list[3] = io_make_output_field(
+  list[3] = io_make_physical_output_field(
       "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, gparts,
-      id_or_neg_offset, "Unique ID of the particles");
+      id_or_neg_offset, /*can convert to comoving=*/0,
+      "Unique ID of the particles");
 
   list[4] = io_make_output_field_convert_gpart(
       "Potentials", FLOAT, 1, UNIT_CONV_POTENTIAL, -1.f, gparts,
diff --git a/src/gravity/MultiSoftening/gravity.h b/src/gravity/MultiSoftening/gravity.h
index b25cf51bfee13ffd17520cde025a8bafad95fc52..c6392ccd2c9c548629abc4e08da8bc4b6b563a5d 100644
--- a/src/gravity/MultiSoftening/gravity.h
+++ b/src/gravity/MultiSoftening/gravity.h
@@ -35,7 +35,7 @@
  * @param gp The particle of interest
  */
 __attribute__((always_inline)) INLINE static float gravity_get_mass(
-    const struct gpart* restrict gp) {
+    const struct gpart* gp) {
 
   return gp->mass;
 }
@@ -47,7 +47,7 @@ __attribute__((always_inline)) INLINE static float gravity_get_mass(
  * @param grav_props The global gravity properties.
  */
 __attribute__((always_inline)) INLINE static float gravity_get_softening(
-    const struct gpart* gp, const struct gravity_props* restrict grav_props) {
+    const struct gpart* gp, const struct gravity_props* grav_props) {
   return gp->epsilon;
 }
 
@@ -58,7 +58,7 @@ __attribute__((always_inline)) INLINE static float gravity_get_softening(
  * @param pot The contribution to add.
  */
 __attribute__((always_inline)) INLINE static void
-gravity_add_comoving_potential(struct gpart* restrict gp, const float pot) {
+gravity_add_comoving_potential(struct gpart* gp, const float pot) {
 
 #ifndef SWIFT_GRAVITY_NO_POTENTIAL
   gp->potential += pot;
@@ -72,8 +72,7 @@ gravity_add_comoving_potential(struct gpart* restrict gp, const float pot) {
  * @param pot The contribution to add.
  */
 __attribute__((always_inline)) INLINE static void
-gravity_add_comoving_mesh_potential(struct gpart* restrict gp,
-                                    const float pot) {
+gravity_add_comoving_mesh_potential(struct gpart* gp, const float pot) {
 
 #ifndef SWIFT_GRAVITY_NO_POTENTIAL
   gp->potential_mesh += pot;
@@ -86,7 +85,7 @@ gravity_add_comoving_mesh_potential(struct gpart* restrict gp,
  * @param gp The particle of interest
  */
 __attribute__((always_inline)) INLINE static float
-gravity_get_comoving_potential(const struct gpart* restrict gp) {
+gravity_get_comoving_potential(const struct gpart* gp) {
 
 #ifndef SWIFT_GRAVITY_NO_POTENTIAL
   return gp->potential;
@@ -101,7 +100,7 @@ gravity_get_comoving_potential(const struct gpart* restrict gp) {
  * @param gp The particle of interest
  */
 __attribute__((always_inline)) INLINE static float
-gravity_get_comoving_mesh_potential(const struct gpart* restrict gp) {
+gravity_get_comoving_mesh_potential(const struct gpart* gp) {
 
 #ifndef SWIFT_GRAVITY_NO_POTENTIAL
   return gp->potential_mesh;
@@ -117,7 +116,7 @@ gravity_get_comoving_mesh_potential(const struct gpart* restrict gp) {
  * @param cosmo The cosmological model.
  */
 __attribute__((always_inline)) INLINE static float
-gravity_get_physical_potential(const struct gpart* restrict gp,
+gravity_get_physical_potential(const struct gpart* gp,
                                const struct cosmology* cosmo) {
 
 #ifndef SWIFT_GRAVITY_NO_POTENTIAL
@@ -140,7 +139,7 @@ gravity_get_physical_potential(const struct gpart* restrict gp,
 __attribute__((always_inline)) INLINE static float
 gravity_compute_timestep_self(const struct gpart* const gp,
                               const float a_hydro[3],
-                              const struct gravity_props* restrict grav_props,
+                              const struct gravity_props* grav_props,
                               const struct cosmology* cosmo) {
 
   /* Get physical acceleration (gravity contribution) */
@@ -305,7 +304,9 @@ __attribute__((always_inline)) INLINE static void gravity_predict_extra(
       gp->epsilon = grav_props->epsilon_baryon_cur;
       break;
     case swift_type_gas:
+#ifndef ADAPTIVE_SOFTENING
       gp->epsilon = grav_props->epsilon_baryon_cur;
+#endif
       break;
     case swift_type_black_hole:
       gp->epsilon = grav_props->epsilon_baryon_cur;
@@ -371,7 +372,9 @@ __attribute__((always_inline)) INLINE static void gravity_first_init_gpart(
       gp->epsilon = grav_props->epsilon_baryon_cur;
       break;
     case swift_type_gas:
+#ifndef ADAPTIVE_SOFTENING
       gp->epsilon = grav_props->epsilon_baryon_cur;
+#endif
       break;
     case swift_type_black_hole:
       gp->epsilon = grav_props->epsilon_baryon_cur;
diff --git a/src/gravity/MultiSoftening/gravity_io.h b/src/gravity/MultiSoftening/gravity_io.h
index 81db9762dbb207b0cfbafc83bbabbbc8dc8dbafe..459c15dd34c9bf26ac4f39a4b5d988e34b3a6b15 100644
--- a/src/gravity/MultiSoftening/gravity_io.h
+++ b/src/gravity/MultiSoftening/gravity_io.h
@@ -142,9 +142,10 @@ INLINE static void darkmatter_write_particles(const struct gpart* gparts,
   list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                                  gparts, mass, "Masses of the particles");
 
-  list[3] = io_make_output_field(
+  list[3] = io_make_physical_output_field(
       "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, gparts,
-      id_or_neg_offset, "Unique ID of the particles");
+      id_or_neg_offset, /*can convert to comoving=*/0,
+      "Unique ID of the particles");
 
   list[4] = io_make_output_field_convert_gpart(
       "Softenings", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, gparts, convert_gpart_soft,
diff --git a/src/gravity_cache.h b/src/gravity_cache.h
index d8b33de3c56d4656819ca4c94beb017212e840d1..2afb5e4b31f14eb61a4a9390d72b38a5488af012 100644
--- a/src/gravity_cache.h
+++ b/src/gravity_cache.h
@@ -209,10 +209,11 @@ INLINE static void gravity_cache_populate(
   if (gcount_padded < gcount) error("Invalid padded cache size. Too small.");
   if (gcount_padded % VEC_SIZE != 0)
     error("Padded gravity cache size invalid. Not a multiple of SIMD length.");
-  if (c->count < gcount_padded)
-    error("Size of the gravity cache is not large enough.");
 #endif
 
+  /* Do we need to grow the cache? */
+  if (c->count < gcount_padded) gravity_cache_init(c, gcount_padded + VEC_SIZE);
+
   /* Make the compiler understand we are in happy vectorization land */
   swift_declare_aligned_ptr(float, x, c->x, SWIFT_CACHE_ALIGNMENT);
   swift_declare_aligned_ptr(float, y, c->y, SWIFT_CACHE_ALIGNMENT);
@@ -317,10 +318,11 @@ INLINE static void gravity_cache_populate_no_mpole(
   if (gcount_padded < gcount) error("Invalid padded cache size. Too small.");
   if (gcount_padded % VEC_SIZE != 0)
     error("Padded gravity cache size invalid. Not a multiple of SIMD length.");
-  if (c->count < gcount_padded)
-    error("Size of the gravity cache is not large enough.");
 #endif
 
+  /* Do we need to grow the cache? */
+  if (c->count < gcount_padded) gravity_cache_init(c, gcount_padded + VEC_SIZE);
+
   /* Make the compiler understand we are in happy vectorization land */
   swift_declare_aligned_ptr(float, x, c->x, SWIFT_CACHE_ALIGNMENT);
   swift_declare_aligned_ptr(float, y, c->y, SWIFT_CACHE_ALIGNMENT);
@@ -406,10 +408,11 @@ INLINE static void gravity_cache_populate_all_mpole(
   if (gcount_padded < gcount) error("Invalid padded cache size. Too small.");
   if (gcount_padded % VEC_SIZE != 0)
     error("Padded gravity cache size invalid. Not a multiple of SIMD length.");
-  if (c->count < gcount_padded)
-    error("Size of the gravity cache is not large enough.");
 #endif
 
+  /* Do we need to grow the cache? */
+  if (c->count < gcount_padded) gravity_cache_init(c, gcount_padded + VEC_SIZE);
+
   /* Make the compiler understand we are in happy vectorization land */
   swift_declare_aligned_ptr(float, x, c->x, SWIFT_CACHE_ALIGNMENT);
   swift_declare_aligned_ptr(float, y, c->y, SWIFT_CACHE_ALIGNMENT);
diff --git a/src/gravity_properties.c b/src/gravity_properties.c
index fa190fd92b2544c1a4e7612058db48dd202dd698..2931ca5988ef85803aae8bb67f481af1a9c3d515 100644
--- a/src/gravity_properties.c
+++ b/src/gravity_properties.c
@@ -41,6 +41,8 @@
 #define gravity_props_default_rebuild_frequency 0.01f
 #define gravity_props_default_rebuild_active_fraction 1.01f  // > 1 means never
 #define gravity_props_default_distributed_mesh 0
+#define gravity_props_default_max_adaptive_softening FLT_MAX
+#define gravity_props_default_min_adaptive_softening 0.f
 
 void gravity_props_init(struct gravity_props *p, struct swift_params *params,
                         const struct phys_const *phys_const,
@@ -242,6 +244,18 @@ void gravity_props_init(struct gravity_props *p, struct swift_params *params,
     p->epsilon_baryon_comoving = p->epsilon_baryon_max_physical;
   }
 
+  /* Adaptive softening properties */
+  p->max_adaptive_softening = parser_get_opt_param_float(
+      params, "Gravity:max_adaptive_softening",
+      gravity_props_default_max_adaptive_softening /
+          kernel_gravity_softening_plummer_equivalent);
+  p->min_adaptive_softening =
+      parser_get_opt_param_float(params, "Gravity:min_adaptive_softening",
+                                 gravity_props_default_min_adaptive_softening);
+
+  p->max_adaptive_softening *= kernel_gravity_softening_plummer_equivalent;
+  p->min_adaptive_softening *= kernel_gravity_softening_plummer_equivalent;
+
   /* Copy over the gravitational constant */
   p->G_Newton = phys_const->const_newton_G;
 
diff --git a/src/gravity_properties.h b/src/gravity_properties.h
index 2a3be6c22497a62d816a34265e8e30951d4a87af..56bc9365a68e4ebe07dd7e29f1d6c72bdd3d8789 100644
--- a/src/gravity_properties.h
+++ b/src/gravity_properties.h
@@ -109,12 +109,18 @@ struct gravity_props {
    * co-moving softening length of the low-res. particles (DM + baryons) */
   float mean_inter_particle_fraction_high_res;
 
+  /*! Maximal comoving softening in the case of adaptive softening for gas */
+  float max_adaptive_softening;
+
+  /*! Minimal comoving softening in the case of adaptive softening for gas */
+  float min_adaptive_softening;
+
   /* ------------- Properties of the time integration  ----------------- */
 
   /*! Frequency of tree-rebuild in units of #gpart updates. */
   float rebuild_frequency;
 
-  /*! Fraction of active #gparts needed to trigger a tree-rebuild */
+  /*! Fraction of active #gpart needed to trigger a tree-rebuild */
   float rebuild_active_fraction;
 
   /*! Time integration dimensionless multiplier */
diff --git a/src/hashmap.h b/src/hashmap.h
index 853f3965315b0a079799389a93e3869725ee35b7..6381e26e036463fbb672dd0552656bf6fe7fa44a 100644
--- a/src/hashmap.h
+++ b/src/hashmap.h
@@ -50,6 +50,7 @@ typedef size_t hashmap_mask_t;
 #ifndef hashmap_value_t
 typedef struct _hashmap_struct {
   long long value_st;
+  long long value_ll;
   float value_flt;
   double value_dbl;
   double value_array_dbl[3];
diff --git a/src/hydro.h b/src/hydro.h
index 1f96d1f3031033c85d5915ecb3ca1d0959738d70..c82c402e81f2937f9beaf389b4a0a68506236dfc 100644
--- a/src/hydro.h
+++ b/src/hydro.h
@@ -62,15 +62,18 @@
 #elif defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
 #include "./hydro/Gizmo/hydro.h"
 #include "./hydro/Gizmo/hydro_iact.h"
-#elif defined(SHADOWFAX_SPH)
+#elif defined(SHADOWSWIFT)
 #include "./hydro/Shadowswift/hydro.h"
 #include "./hydro/Shadowswift/hydro_iact.h"
-#define SPH_IMPLEMENTATION \
-  "Shadowfax moving mesh (Vandenbroucke and De Rijcke 2016)"
+#define SPH_IMPLEMENTATION "ShadowSWIFT moving mesh"
 #elif defined(PLANETARY_SPH)
 #include "./hydro/Planetary/hydro.h"
 #include "./hydro/Planetary/hydro_iact.h"
 #define SPH_IMPLEMENTATION "Minimal version of SPH with multiple materials"
+#elif defined(REMIX_SPH)
+#include "./hydro/REMIX/hydro.h"
+#include "./hydro/REMIX/hydro_iact.h"
+#define SPH_IMPLEMENTATION "REMIX (Sandnes+ 2025)"
 #elif defined(SPHENIX_SPH)
 #include "./hydro/SPHENIX/hydro.h"
 #include "./hydro/SPHENIX/hydro_iact.h"
@@ -90,9 +93,9 @@
 
 /* Check whether this scheme implements the density checks */
 #ifdef SWIFT_HYDRO_DENSITY_CHECKS
-#if !defined(SPHENIX_SPH) && !defined(PLANETARY_SPH)
+#if !defined(SPHENIX_SPH) && !defined(PLANETARY_SPH) && !defined(REMIX_SPH)
 #error \
-    "Can only use the hydro brute-force density checks with the SPHENIX or PLANETARY hydro schemes."
+    "Can only use the hydro brute-force density checks with the SPHENIX or PLANETARY or REMIX hydro schemes."
 #endif
 #endif
 
diff --git a/src/hydro/AnarchyPU/hydro_iact.h b/src/hydro/AnarchyPU/hydro_iact.h
index 861851d9854a3b33b37e96b02759069678bf730d..01ae297251738bab776645c263250bff417374af 100644
--- a/src/hydro/AnarchyPU/hydro_iact.h
+++ b/src/hydro/AnarchyPU/hydro_iact.h
@@ -29,6 +29,7 @@
  * See AnarchyPU/hydro.h for references.
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
@@ -74,6 +75,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
       mj * pj->u * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
 
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
@@ -87,6 +89,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
       mi * pi->u * (hydro_dimension * wj + uj * wj_dx);
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
 
   /* Now we need to compute the div terms */
   const float r_inv = r ? 1.0f / r : 0.0f;
@@ -156,6 +159,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
       mj * pj->u * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
 
   const float r_inv = r ? 1.0f / r : 0.0f;
   const float faci = mj * wi_dx * r_inv;
@@ -392,8 +396,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
       ((f_ij / pi->pressure_bar) * wi_dr + (f_ji / pj->pressure_bar) * wj_dr) *
       r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -524,8 +532,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
       ((f_ij / pi->pressure_bar) * wi_dr + (f_ji / pj->pressure_bar) * wj_dr) *
       r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/AnarchyPU/hydro_io.h b/src/hydro/AnarchyPU/hydro_io.h
index 3f363780fbe42439b020eb3aa79c448bb76413bb..ded723c9f1fc27613f5f99cfe14cfd203c15ece3 100644
--- a/src/hydro/AnarchyPU/hydro_io.h
+++ b/src/hydro/AnarchyPU/hydro_io.h
@@ -209,9 +209,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/AnarchyPU/hydro_part.h b/src/hydro/AnarchyPU/hydro_part.h
index 7cfa3f1cce1a637bf020e0bcda1c36977e2e2304..5b70117f2fca8f31f6a07715662841e90f9fbb16 100644
--- a/src/hydro/AnarchyPU/hydro_part.h
+++ b/src/hydro/AnarchyPU/hydro_part.h
@@ -29,6 +29,7 @@
  * See AnarchyPU/hydro.h for references.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -208,6 +209,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -235,6 +239,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/Gadget2/hydro_iact.h b/src/hydro/Gadget2/hydro_iact.h
index 615d7c0c19fd5339c768def070ebe5539885e22f..13662e7745b6b9ed2be51c94ebb15e308fd25f49 100644
--- a/src/hydro/Gadget2/hydro_iact.h
+++ b/src/hydro/Gadget2/hydro_iact.h
@@ -32,6 +32,7 @@
  * Gadget-2 tree-code neighbours search.
  */
 
+#include "adaptive_softening_iact.h"
 #include "cache.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
@@ -86,6 +87,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
 
+  /* Compute contribution to the adpative softening correction */
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
+
   /* Compute the kernel function for pj */
   const float hj_inv = 1.f / hj;
   const float uj = r * hj_inv;
@@ -99,6 +103,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
 
+  /* Compute contribution to the adpative softening correction */
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
+
   const float faci = mj * wi_dx * r_inv;
   const float facj = mi * wj_dx * r_inv;
 
@@ -183,6 +190,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
 
+  /* Compute contribution to the adpative softening correction */
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
+
   const float fac = mj * wi_dx * r_inv;
 
   /* Compute dv dot r */
@@ -567,8 +577,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
   const float sph_term =
       (f_i * P_over_rho2_i * wi_dr + f_j * P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_i, f_j, r_inv);
+
   /* Eventually got the acceleration */
-  const float acc = visc_term + sph_term;
+  const float acc = visc_term + sph_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -692,8 +706,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
   const float sph_term =
       (f_i * P_over_rho2_i * wi_dr + f_j * P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_i, f_j, r_inv);
+
   /* Eventually got the acceleration */
-  const float acc = visc_term + sph_term;
+  const float acc = visc_term + sph_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/Gadget2/hydro_io.h b/src/hydro/Gadget2/hydro_io.h
index 79d86af264dd2172a09451f7795a5f56b1e1066e..2ae73ff7be3db9c0f324ebb1b7b501342a076bd4 100644
--- a/src/hydro/Gadget2/hydro_io.h
+++ b/src/hydro/Gadget2/hydro_io.h
@@ -189,9 +189,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       "Entropies", FLOAT, 1, UNIT_CONV_ENTROPY_PER_UNIT_MASS, 0.f, parts,
       entropy, "Co-moving entropies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/Gadget2/hydro_part.h b/src/hydro/Gadget2/hydro_part.h
index a2ad80a1da696a458467288586e59bcd59576ab9..252e35fbcd0abe40020004a9ad3c4fb2f8c99931 100644
--- a/src/hydro/Gadget2/hydro_part.h
+++ b/src/hydro/Gadget2/hydro_part.h
@@ -31,6 +31,7 @@
  * Gadget-2 tree-code neighbours search.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -165,6 +166,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -195,6 +199,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/Gasoline/hydro_iact.h b/src/hydro/Gasoline/hydro_iact.h
index 4e97f9445540475fac930be6cf26157c74bdf4b9..426a482e4092319d3e00d9600e034460ecf6e90f 100644
--- a/src/hydro/Gasoline/hydro_iact.h
+++ b/src/hydro/Gasoline/hydro_iact.h
@@ -26,6 +26,7 @@
  *        with added Gasoline physics (Wadsley+ 2017) (interaction routines)
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
@@ -67,6 +68,8 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
 
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
+
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
   const float uj = r * hj_inv;
@@ -78,6 +81,8 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
 
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
+
   /* Now we need to compute the div terms */
   const float r_inv = r ? 1.0f / r : 0.0f;
   const float faci = mj * wi_dx * r_inv;
@@ -147,6 +152,8 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
 
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
+
   const float r_inv = r ? 1.0f / r : 0.0f;
   const float faci = mj * wi_dx * r_inv;
 
@@ -407,8 +414,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
   const float sph_acc_term =
       (pressurei + pressurej) * r_inv * kernel_gradient / (pi->rho * pj->rho);
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term = adaptive_softening_get_acc_term(
+      pi, pj, wi_dr, wj_dr, pi->force.f, pj->force.f, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -527,8 +538,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
   const float sph_acc_term =
       (pressurei + pressurej) * r_inv * kernel_gradient / (pi->rho * pj->rho);
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term = adaptive_softening_get_acc_term(
+      pi, pj, wi_dr, wj_dr, pi->force.f, pj->force.f, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/Gasoline/hydro_io.h b/src/hydro/Gasoline/hydro_io.h
index b1c69f9af06388dc7dfae8b8f24cc2bcc6ce62de..f066ab5ddd212d9e97677fc44242529b50c1e835 100644
--- a/src/hydro/Gasoline/hydro_io.h
+++ b/src/hydro/Gasoline/hydro_io.h
@@ -204,9 +204,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/Gasoline/hydro_part.h b/src/hydro/Gasoline/hydro_part.h
index 39ebe556ee8eee33eb156bc39a30a050f1b4911e..9ee640c6b95b913100cbf8da1d7115fe9f5cae62 100644
--- a/src/hydro/Gasoline/hydro_part.h
+++ b/src/hydro/Gasoline/hydro_part.h
@@ -26,6 +26,7 @@
  *        with added Gasoline physics (Wadsley+ 2017) (particle definition)
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -213,6 +214,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -243,6 +247,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/Gizmo/MFM/hydro_part.h b/src/hydro/Gizmo/MFM/hydro_part.h
index f476b860eaaca8d54386fe3f2502db3b58c67df5..b0cfc0dc410e5342d323551c701d1b41ebeb7fe1 100644
--- a/src/hydro/Gizmo/MFM/hydro_part.h
+++ b/src/hydro/Gizmo/MFM/hydro_part.h
@@ -22,13 +22,13 @@
 /* Data of a single particle. */
 struct part {
 
-  /* Particle ID. */
+  /*! Particle ID. */
   long long id;
 
-  /* Associated gravitas. */
+  /*! Associated gravitas. */
   struct gpart *gpart;
 
-  /* Particle position. */
+  /*! Particle position. */
   double x[3];
 
   /* In MFM, the particle and fluid velocities are the same.
@@ -42,16 +42,16 @@ struct part {
     float fluid_v[3];
   };
 
-  /* Particle acceleration. */
+  /*! Particle acceleration. */
   float a_hydro[3];
 
-  /* Particle smoothing length. */
+  /*! Particle smoothing length. */
   float h;
 
-  /* Density. */
+  /*! Density. */
   float rho;
 
-  /* Pressure. */
+  /*! Pressure. */
   float P;
 
   union {
@@ -94,7 +94,7 @@ struct part {
     };
   };
 
-  /* Fluxes. */
+  /*! Fluxes. */
   struct {
     /* No mass flux, since it is always zero. */
 
@@ -109,7 +109,7 @@ struct part {
 
   } flux;
 
-  /* Gradients of the primitive variables. */
+  /*! Gradients of the primitive variables. */
   struct {
 
     /* Density gradients. */
@@ -123,7 +123,7 @@ struct part {
 
   } gradients;
 
-  /* The conserved hydrodynamical variables. */
+  /*! The conserved hydrodynamical variables. */
   struct {
 
     /* Fluid mass */
@@ -137,22 +137,10 @@ struct part {
 
   } conserved;
 
-  /* Geometrical quantities used for hydro. */
-  struct {
-
-    /* Volume of the particle. */
-    float volume;
-
-    /* Geometrical shear matrix used to calculate second order accurate
-       gradients */
-    float matrix_E[3][3];
+  /*! Geometrical quantities used for hydro. */
+  struct fvpm_geometry_struct geometry;
 
-    /* Correction factor for wcount. */
-    float wcorr;
-
-  } geometry;
-
-  /* Variables used for timestep calculation. */
+  /*! Variables used for timestep calculation. */
   struct {
 
     /* Maximum signal velocity among all the neighbours of the particle. The
@@ -189,6 +177,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/Gizmo/MFM/hydro_velocities.h b/src/hydro/Gizmo/MFM/hydro_velocities.h
index c3929cf2f3e4b864397cebed5490c15ba80c5332..84da17bf4b8e30524ba76f01013b8cbde1645f6a 100644
--- a/src/hydro/Gizmo/MFM/hydro_velocities.h
+++ b/src/hydro/Gizmo/MFM/hydro_velocities.h
@@ -101,51 +101,4 @@ __attribute__((always_inline)) INLINE static void hydro_velocities_set(
   }
 }
 
-/**
- * @brief Reset the variables used to store the centroid; used for the velocity
- * correction.
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_reset_centroids(struct part* restrict p) {}
-
-/**
- * @brief Normalise the centroids after the density loop.
- *
- * @param p Particle.
- * @param wcount Wcount for the particle. This is an explicit argument, so that
- * it is clear from the code that wcount needs to be normalised by the time it
- * is used here.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_normalise_centroid(struct part* restrict p,
-                                    const float wcount) {}
-
-/**
- * @brief Update the centroid with the given contribution, assuming the particle
- * acts as the left particle in the neighbour interaction.
- *
- * @param p Particle (pi).
- * @param dx Distance vector between the particle and its neighbour (dx = pi->x
- * - pj->x).
- * @param w Kernel value at position pj->x.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_update_centroid_left(struct part* restrict p, const float* dx,
-                                      const float w) {}
-
-/**
- * @brief Update the centroid with the given contribution, assuming the particle
- * acts as the right particle in the neighbour interaction.
- *
- * @param p Particle (pj).
- * @param dx Distance vector between the particle and its neighbour (dx = pi->x
- * - pj->x).
- * @param w Kernel value at position pi->x.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_update_centroid_right(struct part* restrict p, const float* dx,
-                                       const float w) {}
-
 #endif /* SWIFT_GIZMO_MFM_HYDRO_VELOCITIES_H */
diff --git a/src/hydro/Gizmo/MFV/hydro_part.h b/src/hydro/Gizmo/MFV/hydro_part.h
index e64b605f2d240a5dbda0266dcdc5c8277f994d55..70b4857a59145c4c447b3a3c4a06b89db0acb697 100644
--- a/src/hydro/Gizmo/MFV/hydro_part.h
+++ b/src/hydro/Gizmo/MFV/hydro_part.h
@@ -22,34 +22,34 @@
 /* Data of a single particle. */
 struct part {
 
-  /* Particle ID. */
+  /*! Particle ID. */
   long long id;
 
-  /* Associated gravitas. */
+  /*! Associated gravitas. */
   struct gpart *gpart;
 
-  /* Particle position. */
+  /*! Particle position. */
   double x[3];
 
-  /* Particle predicted velocity. */
+  /*! Particle predicted velocity. */
   float v[3];
 
-  /* Particle acceleration. */
+  /*! Particle acceleration. */
   float a_hydro[3];
 
-  /* Particle smoothing length. */
+  /*! Particle smoothing length. */
   float h;
 
-  /* Density. */
+  /*! Density. */
   float rho;
 
-  /* Fluid velocity. */
+  /*! Fluid velocity. */
   float fluid_v[3];
 
-  /* Pressure. */
+  /*! Pressure. */
   float P;
 
-  /* Gradients of the primitive variables. */
+  /*! Gradients of the primitive variables. */
   struct {
 
     /* Density gradients. */
@@ -63,7 +63,7 @@ struct part {
 
   } gradients;
 
-  /* Quantities needed by the slope limiter. */
+  /*! Quantities needed by the slope limiter. */
   struct {
 
     /* Extreme values of the density among the neighbours. */
@@ -80,7 +80,7 @@ struct part {
 
   } limiter;
 
-  /* The conserved hydrodynamical variables. */
+  /*! The conserved hydrodynamical variables. */
   struct {
 
     /* Fluid mass */
@@ -94,7 +94,7 @@ struct part {
 
   } conserved;
 
-  /* Fluxes. */
+  /*! Fluxes. */
   struct {
 
     /* Mass flux. */
@@ -111,25 +111,10 @@ struct part {
 
   } flux;
 
-  /* Geometrical quantities used for hydro. */
-  struct {
-
-    /* Volume of the particle. */
-    float volume;
-
-    /* Geometrical shear matrix used to calculate second order accurate
-       gradients */
-    float matrix_E[3][3];
-
-    /* Centroid of the "cell". */
-    float centroid[3];
+  /*! Geometrical quantities used for hydro. */
+  struct fvpm_geometry_struct geometry;
 
-    /* Correction factor for wcount. */
-    float wcorr;
-
-  } geometry;
-
-  /* Variables used for timestep calculation. */
+  /*! Variables used for timestep calculation. */
   struct {
 
     /* Maximum signal velocity among all the neighbours of the particle. The
@@ -140,7 +125,7 @@ struct part {
 
   } timestepvars;
 
-  /* Quantities used during the volume (=density) loop. */
+  /*! Quantities used during the volume (=density) loop. */
   struct {
 
     /* Derivative of particle number density. */
@@ -151,7 +136,7 @@ struct part {
 
   } density;
 
-  /* Quantities used during the force loop. */
+  /*! Quantities used during the force loop. */
   struct {
 
     /* Needed to drift the primitive variables. */
@@ -159,7 +144,7 @@ struct part {
 
   } force;
 
-  /* Specific stuff for the gravity-hydro coupling. */
+  /*! Specific stuff for the gravity-hydro coupling. */
   struct {
 
     /* Current value of the mass flux vector. */
@@ -191,6 +176,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/Gizmo/MFV/hydro_velocities.h b/src/hydro/Gizmo/MFV/hydro_velocities.h
index faba82652bc9cfbc32563fb5566508ed05fa101c..3120c780d81d79362cdc8c47efb1370d759a6c2d 100644
--- a/src/hydro/Gizmo/MFV/hydro_velocities.h
+++ b/src/hydro/Gizmo/MFV/hydro_velocities.h
@@ -152,70 +152,4 @@ __attribute__((always_inline)) INLINE static void hydro_velocities_set(
   }
 }
 
-/**
- * @brief Reset the variables used to store the centroid; used for the velocity
- * correction.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_reset_centroids(struct part* restrict p) {
-
-  p->geometry.centroid[0] = 0.0f;
-  p->geometry.centroid[1] = 0.0f;
-  p->geometry.centroid[2] = 0.0f;
-}
-
-/**
- * @brief Normalise the centroids after the density loop.
- *
- * @param p Particle.
- * @param wcount Wcount for the particle. This is an explicit argument, so that
- * it is clear from the code that wcount needs to be normalised by the time it
- * is used here.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_normalise_centroid(struct part* restrict p,
-                                    const float wcount) {
-
-  const float norm = kernel_norm / wcount;
-  p->geometry.centroid[0] *= norm;
-  p->geometry.centroid[1] *= norm;
-  p->geometry.centroid[2] *= norm;
-}
-
-/**
- * @brief Update the centroid with the given contribution, assuming the particle
- * acts as the left particle in the neighbour interaction.
- *
- * @param p Particle (pi).
- * @param dx Distance vector between the particle and its neighbour (dx = pi->x
- * - pj->x).
- * @param w Kernel value at position pj->x.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_update_centroid_left(struct part* restrict p, const float* dx,
-                                      const float w) {
-
-  p->geometry.centroid[0] -= dx[0] * w;
-  p->geometry.centroid[1] -= dx[1] * w;
-  p->geometry.centroid[2] -= dx[2] * w;
-}
-
-/**
- * @brief Update the centroid with the given contribution, assuming the particle
- * acts as the right particle in the neighbour interaction.
- *
- * @param p Particle (pj).
- * @param dx Distance vector between the particle and its neighbour (dx = pi->x
- * - pj->x).
- * @param w Kernel value at position pi->x.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_velocities_update_centroid_right(struct part* restrict p, const float* dx,
-                                       const float w) {
-
-  p->geometry.centroid[0] += dx[0] * w;
-  p->geometry.centroid[1] += dx[1] * w;
-  p->geometry.centroid[2] += dx[2] * w;
-}
-
 #endif /* SWIFT_GIZMO_MFV_HYDRO_VELOCITIES_H */
diff --git a/src/hydro/Gizmo/hydro.h b/src/hydro/Gizmo/hydro.h
index 85a16a4a64cccc4cb5745c766822a8f4ccfedfdc..5a843c7144a9c12f32b4815bbcab7bfb52f11669 100644
--- a/src/hydro/Gizmo/hydro.h
+++ b/src/hydro/Gizmo/hydro.h
@@ -29,6 +29,7 @@
 
 #include "approx_math.h"
 #include "entropy_floor.h"
+#include "fvpm_geometry.h"
 #include "hydro_flux.h"
 #include "hydro_getters.h"
 #include "hydro_gradients.h"
@@ -190,19 +191,7 @@ __attribute__((always_inline)) INLINE static void hydro_init_part(
   p->density.wcount = 0.0f;
   p->density.wcount_dh = 0.0f;
 
-  p->geometry.volume = 0.0f;
-  p->geometry.matrix_E[0][0] = 0.0f;
-  p->geometry.matrix_E[0][1] = 0.0f;
-  p->geometry.matrix_E[0][2] = 0.0f;
-  p->geometry.matrix_E[1][0] = 0.0f;
-  p->geometry.matrix_E[1][1] = 0.0f;
-  p->geometry.matrix_E[1][2] = 0.0f;
-  p->geometry.matrix_E[2][0] = 0.0f;
-  p->geometry.matrix_E[2][1] = 0.0f;
-  p->geometry.matrix_E[2][2] = 0.0f;
-
-  /* reset the centroid variables used for the velocity correction in MFV */
-  hydro_velocities_reset_centroids(p);
+  fvpm_geometry_init(p);
 }
 
 /**
@@ -241,72 +230,10 @@ __attribute__((always_inline)) INLINE static void hydro_end_density(
   p->density.wcount_dh -= hydro_dimension * kernel_root;
   p->density.wcount_dh *= ihdim_plus_one;
 
-  /* Final operation on the geometry. */
-  /* we multiply with the smoothing kernel normalization ih3 and calculate the
-   * volume */
-  const float volume_inv = ihdim * (p->geometry.volume + kernel_root);
-  const float volume = 1.0f / volume_inv;
-  p->geometry.volume = volume;
-
-  /* we multiply with the smoothing kernel normalization */
-  p->geometry.matrix_E[0][0] *= ihdim;
-  p->geometry.matrix_E[0][1] *= ihdim;
-  p->geometry.matrix_E[0][2] *= ihdim;
-  p->geometry.matrix_E[1][0] *= ihdim;
-  p->geometry.matrix_E[1][1] *= ihdim;
-  p->geometry.matrix_E[1][2] *= ihdim;
-  p->geometry.matrix_E[2][0] *= ihdim;
-  p->geometry.matrix_E[2][1] *= ihdim;
-  p->geometry.matrix_E[2][2] *= ihdim;
-
-  /* normalise the centroids for MFV */
-  hydro_velocities_normalise_centroid(p, p->density.wcount);
-
-  /* Check the condition number to see if we have a stable geometry. */
-  const float condition_number_E =
-      p->geometry.matrix_E[0][0] * p->geometry.matrix_E[0][0] +
-      p->geometry.matrix_E[0][1] * p->geometry.matrix_E[0][1] +
-      p->geometry.matrix_E[0][2] * p->geometry.matrix_E[0][2] +
-      p->geometry.matrix_E[1][0] * p->geometry.matrix_E[1][0] +
-      p->geometry.matrix_E[1][1] * p->geometry.matrix_E[1][1] +
-      p->geometry.matrix_E[1][2] * p->geometry.matrix_E[1][2] +
-      p->geometry.matrix_E[2][0] * p->geometry.matrix_E[2][0] +
-      p->geometry.matrix_E[2][1] * p->geometry.matrix_E[2][1] +
-      p->geometry.matrix_E[2][2] * p->geometry.matrix_E[2][2];
-
-  float condition_number = 0.0f;
-  if (invert_dimension_by_dimension_matrix(p->geometry.matrix_E) != 0) {
-    /* something went wrong in the inversion; force bad condition number */
-    condition_number = const_gizmo_max_condition_number + 1.0f;
-  } else {
-    const float condition_number_Einv =
-        p->geometry.matrix_E[0][0] * p->geometry.matrix_E[0][0] +
-        p->geometry.matrix_E[0][1] * p->geometry.matrix_E[0][1] +
-        p->geometry.matrix_E[0][2] * p->geometry.matrix_E[0][2] +
-        p->geometry.matrix_E[1][0] * p->geometry.matrix_E[1][0] +
-        p->geometry.matrix_E[1][1] * p->geometry.matrix_E[1][1] +
-        p->geometry.matrix_E[1][2] * p->geometry.matrix_E[1][2] +
-        p->geometry.matrix_E[2][0] * p->geometry.matrix_E[2][0] +
-        p->geometry.matrix_E[2][1] * p->geometry.matrix_E[2][1] +
-        p->geometry.matrix_E[2][2] * p->geometry.matrix_E[2][2];
-
-    condition_number =
-        hydro_dimension_inv * sqrtf(condition_number_E * condition_number_Einv);
-  }
-
-  if (condition_number > const_gizmo_max_condition_number &&
-      p->geometry.wcorr > const_gizmo_min_wcorr) {
-#ifdef GIZMO_PATHOLOGICAL_ERROR
-    error("Condition number larger than %g (%g)!",
-          const_gizmo_max_condition_number, condition_number);
-#endif
-#ifdef GIZMO_PATHOLOGICAL_WARNING
-    message("Condition number too large: %g (> %g, p->id: %llu)!",
-            condition_number, const_gizmo_max_condition_number, p->id);
-#endif
-    /* add a correction to the number of neighbours for this particle */
-    p->geometry.wcorr = const_gizmo_w_correction_factor * p->geometry.wcorr;
-  }
+  /* Finish operation on particle volume and matrix. */
+  fvpm_compute_volume_and_matrix(p, ihdim);
+  const float volume = p->geometry.volume;
+  const float volume_inv = 1.f / volume;
 
   /* compute primitive variables */
   /* eqns (3)-(5) */
@@ -399,7 +326,7 @@ __attribute__((always_inline)) INLINE static void hydro_part_has_no_neighbours(
   p->geometry.matrix_E[2][2] = 1.0f;
 
   /* reset the centroid to disable MFV velocity corrections for this particle */
-  hydro_velocities_reset_centroids(p);
+  fvpm_reset_centroids(p);
 }
 
 /**
diff --git a/src/hydro/Gizmo/hydro_getters.h b/src/hydro/Gizmo/hydro_getters.h
index 775e548e561e3735e39e0a630e5e424304468dde..2c06da3c35ea199fa8990a4de24f000fa49227de 100644
--- a/src/hydro/Gizmo/hydro_getters.h
+++ b/src/hydro/Gizmo/hydro_getters.h
@@ -383,16 +383,4 @@ hydro_get_physical_internal_energy_dt(const struct part* restrict p,
          cosmo->a_factor_internal_energy;
 }
 
-/**
- * @brief Check if the gradient matrix for this particle is well behaved.
- *
- * @param p Particle.
- * @return 1 if the gradient matrix is well behaved, 0 otherwise.
- */
-__attribute__((always_inline)) INLINE static int
-hydro_part_geometry_well_behaved(const struct part* restrict p) {
-
-  return p->geometry.wcorr > const_gizmo_min_wcorr;
-}
-
 #endif /* SWIFT_GIZMO_HYDRO_GETTERS_H */
diff --git a/src/hydro/Gizmo/hydro_gradients.h b/src/hydro/Gizmo/hydro_gradients.h
index df6399fa25dd6ed533a4e105b1be0ebc1464319d..9ec2193caf9168fce62507919ca38ff117560fd0 100644
--- a/src/hydro/Gizmo/hydro_gradients.h
+++ b/src/hydro/Gizmo/hydro_gradients.h
@@ -57,8 +57,8 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_init(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
-    float r2, const float* dx, float hi, float hj, struct part* restrict pi,
-    struct part* restrict pj) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part* restrict pi, struct part* restrict pj) {}
 
 /**
  * @brief Gradient calculations done during the neighbour loop: non-symmetric
@@ -72,7 +72,8 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void
-hydro_gradients_nonsym_collect(float r2, const float* dx, float hi, float hj,
+hydro_gradients_nonsym_collect(const float r2, const float dx[3],
+                               const float hi, const float hj,
                                struct part* restrict pi,
                                struct part* restrict pj) {}
 
@@ -105,8 +106,9 @@ __attribute__((always_inline)) INLINE static float hydro_gradients_extrapolate(
  * gradients_none does nothing, since all gradients are zero -- are they?).
  */
 __attribute__((always_inline)) INLINE static void hydro_gradients_predict(
-    struct part* restrict pi, struct part* restrict pj, float hi, float hj,
-    const float* dx, float r, const float* xij_i, float* Wi, float* Wj) {
+    struct part* restrict pi, struct part* restrict pj, const float hi,
+    const float hj, const float* dx, float r, const float* xij_i, float* Wi,
+    float* Wj) {
 
   /* perform gradient reconstruction in space and time */
   /* Compute interface position (relative to pj, since we don't need the actual
diff --git a/src/hydro/Gizmo/hydro_gradients_gizmo.h b/src/hydro/Gizmo/hydro_gradients_gizmo.h
index 7d55eea38dd562dab1cfb6b091f66bca646284d5..3f2aeb174d13a30495ec2b1a88447fb8369ca727 100644
--- a/src/hydro/Gizmo/hydro_gradients_gizmo.h
+++ b/src/hydro/Gizmo/hydro_gradients_gizmo.h
@@ -25,6 +25,7 @@
 #ifndef SWIFT_GIZMO_MFM_HYDRO_GRADIENTS_H
 #define SWIFT_GIZMO_MFM_HYDRO_GRADIENTS_H
 
+#include "fvpm_geometry.h"
 #include "hydro_getters.h"
 #include "hydro_setters.h"
 
@@ -47,8 +48,8 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_init(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj) {
 
   /* Get r and 1/r. */
   const float r = sqrtf(r2);
@@ -78,7 +79,7 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
                        Wi[3] - Wj[3], Wi[4] - Wj[4]};
 
   float wiBidx[3];
-  if (hydro_part_geometry_well_behaved(pi)) {
+  if (fvpm_part_geometry_well_behaved(pi)) {
     wiBidx[0] = wi * (Bi[0][0] * dx[0] + Bi[0][1] * dx[1] + Bi[0][2] * dx[2]);
     wiBidx[1] = wi * (Bi[1][0] * dx[0] + Bi[1][1] * dx[1] + Bi[1][2] * dx[2]);
     wiBidx[2] = wi * (Bi[2][0] * dx[0] + Bi[2][1] * dx[1] + Bi[2][2] * dx[2]);
@@ -123,7 +124,7 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
   kernel_deval(xj, &wj, &wj_dx);
 
   float wjBjdx[3];
-  if (hydro_part_geometry_well_behaved(pj)) {
+  if (fvpm_part_geometry_well_behaved(pj)) {
 
     wjBjdx[0] = wj * (Bj[0][0] * dx[0] + Bj[0][1] * dx[1] + Bj[0][2] * dx[2]);
     wjBjdx[1] = wj * (Bj[1][0] * dx[0] + Bj[1][1] * dx[1] + Bj[1][2] * dx[2]);
@@ -175,7 +176,8 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void
-hydro_gradients_nonsym_collect(float r2, const float *dx, float hi, float hj,
+hydro_gradients_nonsym_collect(const float r2, const float dx[3],
+                               const float hi, const float hj,
                                struct part *restrict pi,
                                struct part *restrict pj) {
 
@@ -205,7 +207,7 @@ hydro_gradients_nonsym_collect(float r2, const float *dx, float hi, float hj,
                        Wi[3] - Wj[3], Wi[4] - Wj[4]};
 
   float wiBidx[3];
-  if (hydro_part_geometry_well_behaved(pi)) {
+  if (fvpm_part_geometry_well_behaved(pi)) {
     wiBidx[0] = wi * (Bi[0][0] * dx[0] + Bi[0][1] * dx[1] + Bi[0][2] * dx[2]);
     wiBidx[1] = wi * (Bi[1][0] * dx[0] + Bi[1][1] * dx[1] + Bi[1][2] * dx[2]);
     wiBidx[2] = wi * (Bi[2][0] * dx[0] + Bi[2][1] * dx[1] + Bi[2][2] * dx[2]);
@@ -259,7 +261,7 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_finalize(
   const float ihdim = pow_dimension(h_inv);
 
   float norm;
-  if (hydro_part_geometry_well_behaved(p)) {
+  if (fvpm_part_geometry_well_behaved(p)) {
     norm = ihdim;
   } else {
     const float ihdimp1 = pow_dimension_plus_one(h_inv);
diff --git a/src/hydro/Gizmo/hydro_gradients_sph.h b/src/hydro/Gizmo/hydro_gradients_sph.h
index ec4db739f6d4baa91e57a9201300d763109d7496..bb9fa22e9a67d841af6f34cacf8452580020c236 100644
--- a/src/hydro/Gizmo/hydro_gradients_sph.h
+++ b/src/hydro/Gizmo/hydro_gradients_sph.h
@@ -47,8 +47,8 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_init(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj) {
 
   /* Get r and 1/r. */
   const float r = sqrtf(r2);
@@ -139,7 +139,8 @@ __attribute__((always_inline)) INLINE static void hydro_gradients_collect(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void
-hydro_gradients_nonsym_collect(float r2, const float *dx, float hi, float hj,
+hydro_gradients_nonsym_collect(const float r2, const float dx[3],
+                               const float hi, const float hj,
                                struct part *restrict pi,
                                struct part *restrict pj) {
 
diff --git a/src/hydro/Gizmo/hydro_iact.h b/src/hydro/Gizmo/hydro_iact.h
index 3d96473879f78b79e2ee56bef32f449cef915894..088fcb1414f14bfcbf59a4f2b4a2d8b56e942d33 100644
--- a/src/hydro/Gizmo/hydro_iact.h
+++ b/src/hydro/Gizmo/hydro_iact.h
@@ -19,6 +19,8 @@
 #ifndef SWIFT_GIZMO_HYDRO_IACT_H
 #define SWIFT_GIZMO_HYDRO_IACT_H
 
+#include "chemistry_additions.h"
+#include "fvpm_geometry.h"
 #include "hydro_flux.h"
 #include "hydro_getters.h"
 #include "hydro_gradients.h"
@@ -66,13 +68,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + xi * wi_dx);
 
-  /* these are eqns. (1) and (2) in the summary */
-  pi->geometry.volume += wi;
-  for (int k = 0; k < 3; k++)
-    for (int l = 0; l < 3; l++)
-      pi->geometry.matrix_E[k][l] += dx[k] * dx[l] * wi;
-
-  hydro_velocities_update_centroid_left(pi, dx, wi);
+  /* Collect data for matrix construction */
+  fvpm_accumulate_geometry_and_matrix(pi, wi, dx);
+  fvpm_update_centroid_left(pi, dx, wi);
 
   /* Compute density of pj. */
   const float hj_inv = 1.0f / hj;
@@ -82,13 +80,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + xj * wj_dx);
 
-  /* these are eqns. (1) and (2) in the summary */
-  pj->geometry.volume += wj;
-  for (int k = 0; k < 3; k++)
-    for (int l = 0; l < 3; l++)
-      pj->geometry.matrix_E[k][l] += dx[k] * dx[l] * wj;
-
-  hydro_velocities_update_centroid_right(pj, dx, wj);
+  /* Collect data for matrix construction */
+  fvpm_accumulate_geometry_and_matrix(pj, wj, dx);
+  fvpm_update_centroid_right(pj, dx, wj);
 }
 
 /**
@@ -129,13 +123,8 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + xi * wi_dx);
 
-  /* these are eqns. (1) and (2) in the summary */
-  pi->geometry.volume += wi;
-  for (int k = 0; k < 3; k++)
-    for (int l = 0; l < 3; l++)
-      pi->geometry.matrix_E[k][l] += dx[k] * dx[l] * wi;
-
-  hydro_velocities_update_centroid_left(pi, dx, wi);
+  fvpm_accumulate_geometry_and_matrix(pi, wi, dx);
+  fvpm_update_centroid_left(pi, dx, wi);
 }
 
 /**
@@ -305,8 +294,8 @@ __attribute__((always_inline)) INLINE static void runner_iact_fluxes_common(
   /* eqn. (7) */
   float Anorm2 = 0.0f;
   float A[3];
-  if (hydro_part_geometry_well_behaved(pi) &&
-      hydro_part_geometry_well_behaved(pj)) {
+  if (fvpm_part_geometry_well_behaved(pi) &&
+      fvpm_part_geometry_well_behaved(pj)) {
     /* in principle, we use Vi and Vj as weights for the left and right
        contributions to the generalized surface vector.
        However, if Vi and Vj are very different (because they have very
@@ -424,6 +413,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_fluxes_common(
   /* If we're working with RT, we need to pay additional attention to the
    * individual mass fractions of ionizing species. */
   rt_part_update_mass_fluxes(pi, pj, totflux[0], mode);
+
+  /* Advect metals if working with chemistry. */
+  runner_iact_chemistry_fluxes(pi, pj, totflux[0], mindt, mode);
 }
 
 /**
diff --git a/src/hydro/Gizmo/hydro_io.h b/src/hydro/Gizmo/hydro_io.h
index 171484205512e86718f4c5de60d826b0c77c7985..1c7a48fa0e79a65e377af3db7a03a0aa81b6b878 100644
--- a/src/hydro/Gizmo/hydro_io.h
+++ b/src/hydro/Gizmo/hydro_io.h
@@ -207,7 +207,7 @@ INLINE static void hydro_write_particles(const struct part* parts,
       "co-moving positions of the particles");
 
   list[2] =
-      io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 1.f, parts,
+      io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f, parts,
                            conserved.mass, "Co-moving masses of the particles");
 
   list[3] = io_make_output_field(
@@ -216,12 +216,12 @@ INLINE static void hydro_write_particles(const struct part* parts,
 
   list[4] = io_make_output_field_convert_part(
       "InternalEnergies", FLOAT, 1, UNIT_CONV_ENERGY_PER_UNIT_MASS,
-      3.f * hydro_gamma_minus_one, parts, xparts, convert_u,
+      -3.f * hydro_gamma_minus_one, parts, xparts, convert_u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/Gizmo/hydro_part.h b/src/hydro/Gizmo/hydro_part.h
index 731efd00ec50cda71331cbe372fbc146d6f853df..1d591ecdd79ec082478ea8c3ccd83cb5b0985539 100644
--- a/src/hydro/Gizmo/hydro_part.h
+++ b/src/hydro/Gizmo/hydro_part.h
@@ -23,6 +23,7 @@
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
 #include "feedback_struct.h"
+#include "fvpm_geometry_struct.h"
 #include "particle_splitting_struct.h"
 #include "rt_struct.h"
 #include "sink_struct.h"
diff --git a/src/hydro/Minimal/hydro_iact.h b/src/hydro/Minimal/hydro_iact.h
index 77c50dd428b8968e66f99104866683b5ebe27936..e04265a3a836ac21e146708f1f47ea91af60aede 100644
--- a/src/hydro/Minimal/hydro_iact.h
+++ b/src/hydro/Minimal/hydro_iact.h
@@ -32,6 +32,7 @@
  * Physics, 2012, Volume 231, Issue 3, pp. 759-794.
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
@@ -80,6 +81,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
 
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
@@ -90,6 +92,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.rho_dh -= mi * (hydro_dimension * wj + uj * wj_dx);
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
 
   /* Compute dv dot r */
   float dv[3], curlvr[3];
@@ -160,6 +163,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
 
   /* Compute dv dot r */
   float dv[3], curlvr[3];
@@ -319,8 +323,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -450,8 +458,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/Minimal/hydro_io.h b/src/hydro/Minimal/hydro_io.h
index 8c32aa7f020fd059fe556e12c147fa397b2d5163..e214a54eb055c9259c6556be8a727e25348c6b9b 100644
--- a/src/hydro/Minimal/hydro_io.h
+++ b/src/hydro/Minimal/hydro_io.h
@@ -204,9 +204,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/Minimal/hydro_part.h b/src/hydro/Minimal/hydro_part.h
index 6709bb5e236bfd1f3ab1cffb6ecdc925115a9375..22e5f6f8c5751266997517715cdd3f1de1a32bcf 100644
--- a/src/hydro/Minimal/hydro_part.h
+++ b/src/hydro/Minimal/hydro_part.h
@@ -32,6 +32,7 @@
  * Physics, 2012, Volume 231, Issue 3, pp. 759-794.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -186,6 +187,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -213,6 +217,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/None/hydro.h b/src/hydro/None/hydro.h
index e883b321e79852699049f8adffdf23d28c26e15b..dffce73d65f973ee7171e81f23bd4e5412594d85 100644
--- a/src/hydro/None/hydro.h
+++ b/src/hydro/None/hydro.h
@@ -213,7 +213,7 @@ hydro_get_physical_soundspeed(const struct part *restrict p,
 }
 
 /**
- * @brief Returns the comoving density of a particle
+ * @brief Returns the physical density of a particle
  *
  * @param p The particle of interest
  */
diff --git a/src/hydro/None/hydro_part.h b/src/hydro/None/hydro_part.h
index 589c874e9716bb17c97d14da6d6e202ccffa6439..354aec0c86f26e6da2299cae84bb0ade2b817f93 100644
--- a/src/hydro/None/hydro_part.h
+++ b/src/hydro/None/hydro_part.h
@@ -179,6 +179,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/Phantom/hydro_iact.h b/src/hydro/Phantom/hydro_iact.h
index 3ae9c98ca5eaf48a93246947ae1d2e811f1419ca..e5c5651093e66465e44c4cdfff2048465a5c8c2e 100644
--- a/src/hydro/Phantom/hydro_iact.h
+++ b/src/hydro/Phantom/hydro_iact.h
@@ -30,6 +30,7 @@
  *        similar to the one presented in Price 2018.
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
@@ -69,9 +70,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
 
   pi->rho += mj * wi;
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
-
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
 
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
@@ -82,6 +83,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.rho_dh -= mi * (hydro_dimension * wj + uj * wj_dx);
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
 
   /* Now we need to compute the div terms */
   const float r_inv = r ? 1.0f / r : 0.0f;
@@ -144,9 +146,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
 
   pi->rho += mj * wi;
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
-
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
 
   const float r_inv = r ? 1.0f / r : 0.0f;
   const float faci = mj * wi_dx * r_inv;
@@ -360,8 +362,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -492,8 +498,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/Phantom/hydro_io.h b/src/hydro/Phantom/hydro_io.h
index 7dbdef3424f02c5a4d3d09d2bcde679269b01455..79a72a835875b30d10850c253895958269413777 100644
--- a/src/hydro/Phantom/hydro_io.h
+++ b/src/hydro/Phantom/hydro_io.h
@@ -210,9 +210,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/Phantom/hydro_part.h b/src/hydro/Phantom/hydro_part.h
index 7c65800e3ccca7c069a993de95733163cb3bac45..032198a5cd6cfeb0b5bef34670fdbca97d345ae0 100644
--- a/src/hydro/Phantom/hydro_part.h
+++ b/src/hydro/Phantom/hydro_part.h
@@ -30,6 +30,7 @@
  *        similar to the one presented in Price 2018.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -206,6 +207,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -233,6 +237,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/Planetary/hydro_iact.h b/src/hydro/Planetary/hydro_iact.h
index 455a8def43c4266833356e26fb4db74b5f698a56..bb9357ad822e125d7555edb5db02b1b63c62f82b 100644
--- a/src/hydro/Planetary/hydro_iact.h
+++ b/src/hydro/Planetary/hydro_iact.h
@@ -33,6 +33,7 @@
  * Physics, 2012, Volume 231, Issue 3, pp. 759-794.
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
 #include "const.h"
 #include "hydro_parameters.h"
@@ -82,6 +83,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
 
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
@@ -92,6 +94,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.rho_dh -= mi * (hydro_dimension * wj + uj * wj_dx);
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
 
   /* Compute dv dot r */
   float dv[3], curlvr[3];
@@ -119,6 +122,13 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.rot_v[0] += facj * curlvr[0];
   pj->density.rot_v[1] += facj * curlvr[1];
   pj->density.rot_v[2] += facj * curlvr[2];
+
+#ifdef SWIFT_HYDRO_DENSITY_CHECKS
+  pi->n_density += wi;
+  pj->n_density += wj;
+  pi->N_density++;
+  pj->N_density++;
+#endif
 }
 
 /**
@@ -162,6 +172,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
 
   /* Compute dv dot r */
   float dv[3], curlvr[3];
@@ -183,6 +194,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
   pi->density.rot_v[0] += faci * curlvr[0];
   pi->density.rot_v[1] += faci * curlvr[1];
   pi->density.rot_v[2] += faci * curlvr[2];
+
+#ifdef SWIFT_HYDRO_DENSITY_CHECKS
+  pi->n_density += wi;
+  pi->N_density++;
+#endif
 }
 
 /**
@@ -318,8 +334,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -352,6 +372,13 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
   /* Update the signal velocity. */
   pi->force.v_sig = max(pi->force.v_sig, v_sig);
   pj->force.v_sig = max(pj->force.v_sig, v_sig);
+
+#ifdef SWIFT_HYDRO_DENSITY_CHECKS
+  pi->n_force += wi + wj;
+  pj->n_force += wi + wj;
+  pi->N_force++;
+  pj->N_force++;
+#endif
 }
 
 /**
@@ -446,8 +473,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -471,6 +502,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
 
   /* Update the signal velocity. */
   pi->force.v_sig = max(pi->force.v_sig, v_sig);
+
+#ifdef SWIFT_HYDRO_DENSITY_CHECKS
+  pi->n_force += wi + wj;
+  pi->N_force++;
+#endif
 }
 
 #endif /* SWIFT_PLANETARY_HYDRO_IACT_H */
diff --git a/src/hydro/Planetary/hydro_io.h b/src/hydro/Planetary/hydro_io.h
index f18e1180066513862470e46f99efd5308f0c4495..5b291114be59b083db2485822c216723f8c0e465 100644
--- a/src/hydro/Planetary/hydro_io.h
+++ b/src/hydro/Planetary/hydro_io.h
@@ -57,6 +57,13 @@ INLINE static void hydro_read_particles(struct part* parts,
   *num_fields = 9;
 #endif
 
+  /* Temporary warning to be printed for a few months after the change */
+  message(
+      "\n # Warning: some required field names for initial conditions were"
+      " tweaked in July 2024 to match the GADGET-2 format that SWIFT follows."
+      " Please update your scripts (e.g. download the latest WoMa package) to"
+      " match. Apologies for any inconvenience");
+
   /* List what we want to read */
   list[0] = io_make_input_field("Coordinates", DOUBLE, 3, COMPULSORY,
                                 UNIT_CONV_LENGTH, parts, x);
@@ -64,15 +71,15 @@ INLINE static void hydro_read_particles(struct part* parts,
                                 UNIT_CONV_SPEED, parts, v);
   list[2] = io_make_input_field("Masses", FLOAT, 1, COMPULSORY, UNIT_CONV_MASS,
                                 parts, mass);
-  list[3] = io_make_input_field("SmoothingLengths", FLOAT, 1, COMPULSORY,
+  list[3] = io_make_input_field("SmoothingLength", FLOAT, 1, COMPULSORY,
                                 UNIT_CONV_LENGTH, parts, h);
-  list[4] = io_make_input_field("InternalEnergies", FLOAT, 1, COMPULSORY,
+  list[4] = io_make_input_field("InternalEnergy", FLOAT, 1, COMPULSORY,
                                 UNIT_CONV_ENERGY_PER_UNIT_MASS, parts, u);
   list[5] = io_make_input_field("ParticleIDs", ULONGLONG, 1, COMPULSORY,
                                 UNIT_CONV_NO_UNITS, parts, id);
   list[6] = io_make_input_field("Accelerations", FLOAT, 3, OPTIONAL,
                                 UNIT_CONV_ACCELERATION, parts, a_hydro);
-  list[7] = io_make_input_field("Densities", FLOAT, 1, OPTIONAL,
+  list[7] = io_make_input_field("Density", FLOAT, 1, OPTIONAL,
                                 UNIT_CONV_DENSITY, parts, rho);
   list[8] = io_make_input_field("MaterialIDs", INT, 1, COMPULSORY,
                                 UNIT_CONV_NO_UNITS, parts, mat_id);
@@ -208,9 +215,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       "InternalEnergies", FLOAT, 1, UNIT_CONV_ENERGY_PER_UNIT_MASS,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Thermal energies per unit mass of the particles");
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho, "Densities of the particles");
   list[7] = io_make_output_field_convert_part(
diff --git a/src/hydro/Planetary/hydro_part.h b/src/hydro/Planetary/hydro_part.h
index ccc7ef3b9996ccd544a5b27af0eaca0f052b69c8..4732cccaf716b31a170c258054ef94bdde0cc89a 100644
--- a/src/hydro/Planetary/hydro_part.h
+++ b/src/hydro/Planetary/hydro_part.h
@@ -33,6 +33,7 @@
  * Physics, 2012, Volume 231, Issue 3, pp. 759-794.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -188,6 +189,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -215,6 +219,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
@@ -228,6 +235,54 @@ struct part {
 
 #endif
 
+#ifdef SWIFT_HYDRO_DENSITY_CHECKS
+
+  /* Integer number of neighbours in the density loop */
+  int N_density;
+
+  /* Exact integer number of neighbours in the density loop */
+  int N_density_exact;
+
+  /* Integer number of neighbours in the gradient loop */
+  int N_gradient;
+
+  /* Exact integer number of neighbours in the gradient loop */
+  int N_gradient_exact;
+
+  /* Integer number of neighbours in the force loop */
+  int N_force;
+
+  /* Exact integer number of neighbours in the force loop */
+  int N_force_exact;
+
+  /*! Exact value of the density field obtained via brute-force loop */
+  float rho_exact;
+
+  /*! Weighted numer of neighbours in the density loop */
+  float n_density;
+
+  /*! Exact value of the weighted numer of neighbours in the density loop */
+  float n_density_exact;
+
+  /*! Weighted numer of neighbours in the gradient loop */
+  float n_gradient;
+
+  /*! Exact value of the weighted numer of neighbours in the gradient loop */
+  float n_gradient_exact;
+
+  /*! Weighted numer of neighbours in the force loop */
+  float n_force;
+
+  /*! Exact value of the weighted numer of neighbours in the force loop */
+  float n_force_exact;
+
+  /*! Has this particle interacted with any unhibited neighbour? */
+  char inhibited_exact;
+
+  /*! Has this particle been woken up by the limiter? */
+  char limited_part;
+#endif
+
 #ifdef PLANETARY_FIXED_ENTROPY
   /* Fixed specific entropy */
   float s_fixed;
diff --git a/src/hydro/PressureEnergy/hydro_iact.h b/src/hydro/PressureEnergy/hydro_iact.h
index e2cae19b9fb0d9cd5f2d8f483901e116c296dc62..5715c2d03b8be5f435e90a467d4b48acdd14b0cc 100644
--- a/src/hydro/PressureEnergy/hydro_iact.h
+++ b/src/hydro/PressureEnergy/hydro_iact.h
@@ -32,6 +32,7 @@
  * See PressureEnergy/hydro.h for references.
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
@@ -77,6 +78,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
       mj * pj->u * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
 
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
@@ -91,6 +93,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
       mi * pi->u * (hydro_dimension * wj + uj * wj_dx);
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
 
   /* Now we need to compute the div terms */
   const float r_inv = r ? 1.0f / r : 0.0f;
@@ -160,6 +163,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
       mj * pj->u * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
 
   const float r_inv = r ? 1.0f / r : 0.0f;
   const float faci = mj * wi_dx * r_inv;
@@ -315,8 +319,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
                               (f_ji * pressure_inverse_j) * wj_dr) *
                              r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -450,8 +458,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
                               (f_ji * pressure_inverse_j) * wj_dr) *
                              r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/PressureEnergy/hydro_io.h b/src/hydro/PressureEnergy/hydro_io.h
index 00cc3e4f0c69536a27db5b68374f444ad11098bb..ca8205f70cf928f6e0aa7fb9f3ccb3e9e4272ac0 100644
--- a/src/hydro/PressureEnergy/hydro_io.h
+++ b/src/hydro/PressureEnergy/hydro_io.h
@@ -199,9 +199,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/PressureEnergy/hydro_part.h b/src/hydro/PressureEnergy/hydro_part.h
index 75d5df1835bde62933b590d0606ec7a60a51a130..eedf2fe66684ca7f06e848d2d7a744cac719fd26 100644
--- a/src/hydro/PressureEnergy/hydro_part.h
+++ b/src/hydro/PressureEnergy/hydro_part.h
@@ -31,6 +31,7 @@
  * See PressureEnergy/hydro.h for references.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -190,6 +191,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -220,6 +224,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_iact.h b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_iact.h
index 745702b15887e5ffcd38c982356525f8a04a2e69..695314bfd34a8236b175146cf65fcb45a60e1e12 100644
--- a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_iact.h
+++ b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_iact.h
@@ -33,6 +33,7 @@
  * See PressureEnergy/hydro.h for references.
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
@@ -77,6 +78,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
       mj * pj->u * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
 
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
@@ -90,6 +92,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
       mi * pi->u * (hydro_dimension * wj + uj * wj_dx);
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
 
   /* Now we need to compute the div terms */
   const float r_inv = r ? 1.0f / r : 0.0f;
@@ -159,6 +162,7 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
       mj * pj->u * (hydro_dimension * wi + ui * wi_dx);
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
 
   const float r_inv = r ? 1.0f / r : 0.0f;
   const float faci = mj * wi_dx * r_inv;
@@ -311,8 +315,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
       ((f_ij / pi->pressure_bar) * wi_dr + (f_ji / pj->pressure_bar) * wj_dr) *
       r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -441,8 +449,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
       ((f_ij / pi->pressure_bar) * wi_dr + (f_ji / pj->pressure_bar) * wj_dr) *
       r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_io.h b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_io.h
index 601a99b2d295506f5a7003776add7e2fe33d7d69..467843eeca4d27a5738b1a44bae0cda6e8ddaa27 100644
--- a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_io.h
+++ b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_io.h
@@ -200,9 +200,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h
index aae8ac721e462c916e003b3086ab127c76a928d7..c28d74c64cb05b42f65f1fa5c71308937f301982 100644
--- a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h
+++ b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h
@@ -32,6 +32,7 @@
  * See PressureEnergy/hydro.h for references.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -190,6 +191,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -217,6 +221,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/PressureEntropy/hydro_iact.h b/src/hydro/PressureEntropy/hydro_iact.h
index 7338e87da9b4ec09f7d97d5cb1de4ec867e36b76..7678f001b07bec667185a857820aa3bc95b456db 100644
--- a/src/hydro/PressureEntropy/hydro_iact.h
+++ b/src/hydro/PressureEntropy/hydro_iact.h
@@ -19,6 +19,7 @@
 #ifndef SWIFT_PRESSURE_ENTROPY_HYDRO_IACT_H
 #define SWIFT_PRESSURE_ENTROPY_HYDRO_IACT_H
 
+#include "adaptive_softening_iact.h"
 #include "hydro_parameters.h"
 #include "signal_velocity.h"
 
@@ -80,6 +81,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pi->density.pressure_dh -=
       mj * pj->entropy_one_over_gamma * (hydro_dimension * wi + ui * wi_dx);
 
+  /* Compute contribution to the adpative softening correction */
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
+
   /* Compute the kernel function for pj */
   const float hj_inv = 1.f / hj;
   const float uj = r * hj_inv;
@@ -98,6 +102,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.pressure_dh -=
       mi * pi->entropy_one_over_gamma * (hydro_dimension * wj + uj * wj_dx);
 
+  /* Compute contribution to the adpative softening correction */
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
+
   const float faci = mj * wi_dx * r_inv;
   const float facj = mi * wj_dx * r_inv;
 
@@ -169,6 +176,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
   pi->density.pressure_dh -=
       mj * pj->entropy_one_over_gamma * (hydro_dimension * wi + ui * wi_dx);
 
+  /* Compute contribution to the adpative softening correction */
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
+
   const float fac = mj * wi_dx * r_inv;
 
   /* Compute dv dot r */
@@ -314,8 +324,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
       (S_gamma_j / S_gamma_i - f_i / S_gamma_i) * P_over_rho2_i * wi_dr +
       (S_gamma_i / S_gamma_j - f_j / S_gamma_j) * P_over_rho2_j * wj_dr;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_i, f_j, r_inv);
+
   /* Eventually got the acceleration */
-  const float acc = (visc_term + sph_term) * r_inv;
+  const float acc = (visc_term + sph_term) * r_inv + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -424,8 +438,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
       (S_gamma_j / S_gamma_i - f_i / S_gamma_i) * P_over_rho2_i * wi_dr +
       (S_gamma_i / S_gamma_j - f_j / S_gamma_j) * P_over_rho2_j * wj_dr;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_i, f_j, r_inv);
+
   /* Eventually got the acceleration */
-  const float acc = (visc_term + sph_term) * r_inv;
+  const float acc = (visc_term + sph_term) * r_inv + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/PressureEntropy/hydro_io.h b/src/hydro/PressureEntropy/hydro_io.h
index 99defbc1f1ce6b86b28c9eadf265d771b20de724..59891d965b1cbc9298e922416c8bb97243515307 100644
--- a/src/hydro/PressureEntropy/hydro_io.h
+++ b/src/hydro/PressureEntropy/hydro_io.h
@@ -200,9 +200,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       "Entropies", FLOAT, 1, UNIT_CONV_ENTROPY_PER_UNIT_MASS, 0.f, parts,
       entropy, "Co-moving entropies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
diff --git a/src/hydro/PressureEntropy/hydro_part.h b/src/hydro/PressureEntropy/hydro_part.h
index 8665b70098f98df1dbb81a2a0f3dca34b2a21eeb..c0ade7783444bff93c47ac55d06761a929ee8e96 100644
--- a/src/hydro/PressureEntropy/hydro_part.h
+++ b/src/hydro/PressureEntropy/hydro_part.h
@@ -30,6 +30,7 @@
  * Volume 428, Issue 4, pp. 2840-2856 with a simple Balsara viscosity term.
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
@@ -166,6 +167,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -193,6 +197,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
diff --git a/src/hydro/REMIX/hydro.h b/src/hydro/REMIX/hydro.h
new file mode 100644
index 0000000000000000000000000000000000000000..bb5517e78a103a3fce777b20db115a4826a49219
--- /dev/null
+++ b/src/hydro/REMIX/hydro.h
@@ -0,0 +1,981 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_H
+#define SWIFT_REMIX_HYDRO_H
+
+/**
+ * @file REMIX/hydro.h
+ * @brief REMIX implementation of SPH (Sandnes et al. 2025)
+ */
+
+#include "adiabatic_index.h"
+#include "approx_math.h"
+#include "cosmology.h"
+#include "debug.h"
+#include "dimension.h"
+#include "entropy_floor.h"
+#include "equation_of_state.h"
+#include "hydro_kernels.h"
+#include "hydro_parameters.h"
+#include "hydro_properties.h"
+#include "hydro_space.h"
+#include "hydro_visc_difn.h"
+#include "kernel_hydro.h"
+#include "minmax.h"
+#include "pressure_floor.h"
+
+/**
+ * @brief Returns the comoving internal energy of a particle at the last
+ * time the particle was kicked.
+ *
+ * For implementations where the main thermodynamic variable
+ * is not internal energy, this function computes the internal
+ * energy from the thermodynamic variable.
+ *
+ * @param p The particle of interest
+ * @param xp The extended data of the particle of interest.
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_comoving_internal_energy(const struct part *restrict p,
+                                   const struct xpart *restrict xp) {
+
+  return xp->u_full;
+}
+
+/**
+ * @brief Returns the physical internal energy of a particle at the last
+ * time the particle was kicked.
+ *
+ * For implementations where the main thermodynamic variable
+ * is not internal energy, this function computes the internal
+ * energy from the thermodynamic variable and converts it to
+ * physical coordinates.
+ *
+ * @param p The particle of interest.
+ * @param xp The extended data of the particle of interest.
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_physical_internal_energy(const struct part *restrict p,
+                                   const struct xpart *restrict xp,
+                                   const struct cosmology *cosmo) {
+
+  return xp->u_full * cosmo->a_factor_internal_energy;
+}
+
+/**
+ * @brief Returns the comoving internal energy of a particle drifted to the
+ * current time.
+ *
+ * @param p The particle of interest
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_comoving_internal_energy(const struct part *restrict p) {
+
+  return p->u;
+}
+
+/**
+ * @brief Returns the physical internal energy of a particle drifted to the
+ * current time.
+ *
+ * @param p The particle of interest.
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_physical_internal_energy(const struct part *restrict p,
+                                           const struct cosmology *cosmo) {
+
+  return p->u * cosmo->a_factor_internal_energy;
+}
+
+/**
+ * @brief Returns the comoving pressure of a particle
+ *
+ * Computes the pressure based on the particle's properties.
+ *
+ * @param p The particle of interest
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_comoving_pressure(
+    const struct part *restrict p) {
+
+  return gas_pressure_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+}
+
+/**
+ * @brief Returns the physical pressure of a particle
+ *
+ * Computes the pressure based on the particle's properties and
+ * convert it to physical coordinates.
+ *
+ * @param p The particle of interest
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_physical_pressure(
+    const struct part *restrict p, const struct cosmology *cosmo) {
+
+  return cosmo->a_factor_pressure *
+         gas_pressure_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+}
+
+/**
+ * @brief Returns the comoving entropy of a particle
+ *
+ * For implementations where the main thermodynamic variable
+ * is not entropy, this function computes the entropy from
+ * the thermodynamic variable.
+ *
+ * @param p The particle of interest
+ * @param xp The extended data of the particle of interest.
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_comoving_entropy(
+    const struct part *restrict p, const struct xpart *restrict xp) {
+
+  return gas_entropy_from_internal_energy(p->rho_evol, xp->u_full, p->mat_id);
+}
+
+/**
+ * @brief Returns the physical entropy of a particle
+ *
+ * For implementations where the main thermodynamic variable
+ * is not entropy, this function computes the entropy from
+ * the thermodynamic variable and converts it to
+ * physical coordinates.
+ *
+ * @param p The particle of interest
+ * @param xp The extended data of the particle of interest.
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_physical_entropy(
+    const struct part *restrict p, const struct xpart *restrict xp,
+    const struct cosmology *cosmo) {
+
+  /* Note: no cosmological conversion required here with our choice of
+   * coordinates. */
+  return gas_entropy_from_internal_energy(p->rho_evol, xp->u_full, p->mat_id);
+}
+
+/**
+ * @brief Returns the comoving entropy of a particle drifted to the
+ * current time.
+ *
+ * @param p The particle of interest.
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_comoving_entropy(const struct part *restrict p) {
+
+  return gas_entropy_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+}
+
+/**
+ * @brief Returns the physical entropy of a particle drifted to the
+ * current time.
+ *
+ * @param p The particle of interest.
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_physical_entropy(const struct part *restrict p,
+                                   const struct cosmology *cosmo) {
+
+  /* Note: no cosmological conversion required here with our choice of
+   * coordinates. */
+  return gas_entropy_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+}
+
+/**
+ * @brief Returns the comoving sound speed of a particle
+ *
+ * @param p The particle of interest
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_comoving_soundspeed(const struct part *restrict p) {
+
+  return p->force.soundspeed;
+}
+
+/**
+ * @brief Returns the physical sound speed of a particle
+ *
+ * @param p The particle of interest
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_physical_soundspeed(const struct part *restrict p,
+                              const struct cosmology *cosmo) {
+
+  return cosmo->a_factor_sound_speed * p->force.soundspeed;
+}
+
+/**
+ * @brief Returns the comoving density of a particle
+ *
+ * @param p The particle of interest
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_comoving_density(
+    const struct part *restrict p) {
+
+  return p->rho;
+}
+
+/**
+ * @brief Returns the comoving density of a particle.
+ *
+ * @param p The particle of interest
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_physical_density(
+    const struct part *restrict p, const struct cosmology *cosmo) {
+
+  return cosmo->a3_inv * p->rho;
+}
+
+/**
+ * @brief Returns the mass of a particle
+ *
+ * @param p The particle of interest
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_mass(
+    const struct part *restrict p) {
+
+  return p->mass;
+}
+
+/**
+ * @brief Sets the mass of a particle
+ *
+ * @param p The particle of interest
+ * @param m The mass to set.
+ */
+__attribute__((always_inline)) INLINE static void hydro_set_mass(
+    struct part *restrict p, float m) {
+
+  p->mass = m;
+}
+
+/**
+ * @brief Returns the time derivative of internal energy of a particle
+ *
+ * We assume a constant density.
+ *
+ * @param p The particle of interest
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_comoving_internal_energy_dt(const struct part *restrict p) {
+
+  return p->u_dt;
+}
+
+/**
+ * @brief Returns the time derivative of internal energy of a particle
+ *
+ * We assume a constant density.
+ *
+ * @param p The particle of interest
+ * @param cosmo Cosmology data structure
+ */
+__attribute__((always_inline)) INLINE static float
+hydro_get_physical_internal_energy_dt(const struct part *restrict p,
+                                      const struct cosmology *cosmo) {
+
+  return p->u_dt * cosmo->a_factor_internal_energy;
+}
+
+/**
+ * @brief Returns the time derivative of internal energy of a particle
+ *
+ * We assume a constant density.
+ *
+ * @param p The particle of interest.
+ * @param du_dt The new time derivative of the internal energy.
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_set_comoving_internal_energy_dt(struct part *restrict p, float du_dt) {
+
+  p->u_dt = du_dt;
+}
+
+/**
+ * @brief Returns the time derivative of internal energy of a particle
+ *
+ * We assume a constant density.
+ *
+ * @param p The particle of interest.
+ * @param cosmo Cosmology data structure
+ * @param du_dt The new time derivative of the internal energy.
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_set_physical_internal_energy_dt(struct part *restrict p,
+                                      const struct cosmology *cosmo,
+                                      float du_dt) {
+
+  p->u_dt = du_dt / cosmo->a_factor_internal_energy;
+}
+
+/**
+ * @brief Sets the physical entropy of a particle
+ *
+ * @param p The particle of interest.
+ * @param xp The extended particle data.
+ * @param cosmo Cosmology data structure
+ * @param entropy The physical entropy
+ */
+__attribute__((always_inline)) INLINE static void hydro_set_physical_entropy(
+    struct part *p, struct xpart *xp, const struct cosmology *cosmo,
+    const float entropy) {
+
+  /* Note there is no conversion from physical to comoving entropy */
+  const float comoving_entropy = entropy;
+  xp->u_full = gas_internal_energy_from_entropy(p->rho_evol, comoving_entropy,
+                                                p->mat_id);
+}
+
+/**
+ * @brief Sets the physical internal energy of a particle
+ *
+ * @param p The particle of interest.
+ * @param xp The extended particle data.
+ * @param cosmo Cosmology data structure
+ * @param u The physical internal energy
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_set_physical_internal_energy(struct part *p, struct xpart *xp,
+                                   const struct cosmology *cosmo,
+                                   const float u) {
+
+  xp->u_full = u / cosmo->a_factor_internal_energy;
+}
+
+/**
+ * @brief Sets the drifted physical internal energy of a particle
+ *
+ * @param p The particle of interest.
+ * @param cosmo Cosmology data structure
+ * @param u The physical internal energy
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_set_drifted_physical_internal_energy(struct part *p,
+                                           const struct cosmology *cosmo,
+                                           const float u) {
+
+  p->u = u / cosmo->a_factor_internal_energy;
+
+  /* Now recompute the extra quantities */
+
+  /* Compute the sound speed */
+  const float pressure =
+      gas_pressure_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+  const float soundspeed = hydro_get_comoving_soundspeed(p);
+
+  /* Update variables. */
+  p->force.pressure = pressure;
+  p->force.soundspeed = soundspeed;
+
+  p->force.v_sig = max(p->force.v_sig, 2.f * soundspeed);
+}
+
+/**
+ * @brief Update the value of the viscosity alpha for the scheme.
+ *
+ * @param p the particle of interest
+ * @param alpha the new value for the viscosity coefficient.
+ */
+__attribute__((always_inline)) INLINE static void hydro_set_viscosity_alpha(
+    struct part *restrict p, float alpha) {
+  /* This scheme has fixed alpha */
+}
+
+/**
+ * @brief Update the value of the diffusive coefficients to the
+ *        feedback reset value for the scheme.
+ *
+ * @param p the particle of interest
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_diffusive_feedback_reset(struct part *restrict p) {
+  /* This scheme has fixed alpha */
+}
+
+/**
+ * @brief Computes the hydro time-step of a given particle
+ *
+ * This function returns the time-step of a particle given its hydro-dynamical
+ * state. A typical time-step calculation would be the use of the CFL condition.
+ *
+ * @param p Pointer to the particle data
+ * @param xp Pointer to the extended particle data
+ * @param hydro_properties The SPH parameters
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static float hydro_compute_timestep(
+    const struct part *restrict p, const struct xpart *restrict xp,
+    const struct hydro_props *restrict hydro_properties,
+    const struct cosmology *restrict cosmo) {
+
+  const float CFL_condition = hydro_properties->CFL_condition;
+
+  /* CFL condition */
+  const float dt_cfl = 2.f * kernel_gamma * CFL_condition * cosmo->a * p->h /
+                       (cosmo->a_factor_sound_speed * p->force.v_sig);
+
+  return dt_cfl;
+}
+
+/**
+ * @brief returns the signal velocity
+ *
+ * @brief p  the particle
+ */
+__attribute__((always_inline)) INLINE static float hydro_get_signal_velocity(
+    const struct part *restrict p) {
+
+  return p->force.v_sig;
+}
+
+/**
+ * @brief Does some extra hydro operations once the actual physical time step
+ * for the particle is known.
+ *
+ * @param p The particle to act upon.
+ * @param dt Physical time step of the particle during the next step.
+ */
+__attribute__((always_inline)) INLINE static void hydro_timestep_extra(
+    struct part *p, float dt) {}
+
+/**
+ * @brief Prepares a particle for the density calculation.
+ *
+ * Zeroes all the relevant arrays in preparation for the sums taking place in
+ * the various density loop over neighbours. Typically, all fields of the
+ * density sub-structure of a particle get zeroed in here.
+ *
+ * @param p The particle to act upon
+ * @param hs #hydro_space containing hydro specific space information.
+ */
+__attribute__((always_inline)) INLINE static void hydro_init_part(
+    struct part *restrict p, const struct hydro_space *hs) {
+
+  p->density.wcount = 0.f;
+  p->density.wcount_dh = 0.f;
+  p->rho = 0.f;
+  p->density.rho_dh = 0.f;
+
+  hydro_init_part_extra_kernel(p);
+}
+
+/**
+ * @brief Finishes the density calculation.
+ *
+ * Multiplies the density and number of neighbours by the appropiate constants
+ * and add the self-contribution term.
+ * Additional quantities such as velocity gradients will also get the final
+ * terms added to them here.
+ *
+ * Also adds/multiplies the cosmological terms if need be.
+ *
+ * @param p The particle to act upon
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void hydro_end_density(
+    struct part *restrict p, const struct cosmology *cosmo) {
+
+  /* Some smoothing length multiples. */
+  const float h = p->h;
+  const float h_inv = 1.0f / h;                       /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv);       /* 1/h^d */
+  const float h_inv_dim_plus_one = h_inv_dim * h_inv; /* 1/h^(d+1) */
+
+  /* Final operation on the density (add self-contribution). */
+  p->rho += p->mass * kernel_root;
+  p->density.rho_dh -= hydro_dimension * p->mass * kernel_root;
+  p->density.wcount += kernel_root;
+  p->density.wcount_dh -= hydro_dimension * kernel_root;
+
+  /* Finish the calculation by inserting the missing h-factors */
+  p->rho *= h_inv_dim;
+  p->density.rho_dh *= h_inv_dim_plus_one;
+  p->density.wcount *= h_inv_dim;
+  p->density.wcount_dh *= h_inv_dim_plus_one;
+
+  hydro_end_density_extra_kernel(p);
+}
+
+/**
+ * @brief Sets all particle fields to sensible values when the #part has 0 ngbs.
+ *
+ * In the desperate case where a particle has no neighbours (likely because
+ * of the h_max ceiling), set the particle fields to something sensible to avoid
+ * NaNs in the next calculations.
+ *
+ * @param p The particle to act upon
+ * @param xp The extended particle data to act upon
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void hydro_part_has_no_neighbours(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo) {
+
+  /* Some smoothing length multiples. */
+  const float h = p->h;
+  const float h_inv = 1.0f / h;                 /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
+
+  /* Re-set problematic values */
+  p->rho = p->mass * kernel_root * h_inv_dim;
+  p->density.wcount = kernel_root * h_inv_dim;
+  p->density.rho_dh = 0.f;
+  p->density.wcount_dh = 0.f;
+
+  p->is_h_max = 1;
+  p->m0 = p->mass * kernel_root * h_inv_dim / p->rho_evol;
+}
+
+/**
+ * @brief Prepare a particle for the gradient calculation.
+ *
+ * This function is called after the density loop and before the gradient loop.
+ *
+ * We use it to set the physical timestep for the particle and to copy the
+ * actual velocities, which we need to boost our interfaces during the flux
+ * calculation. We also initialize the variables used for the time step
+ * calculation.
+ *
+ * @param p The particle to act upon.
+ * @param xp The extended particle data to act upon.
+ * @param cosmo The cosmological model.
+ * @param hydro_props Hydrodynamic properties.
+ */
+__attribute__((always_inline)) INLINE static void hydro_prepare_gradient(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct pressure_floor_props *pressure_floor) {
+
+  /* Set p->is_h_max = 1 if smoothing length is h_max */
+  if (p->h > 0.999f * hydro_props->h_max) {
+    p->is_h_max = 1;
+  } else {
+    p->is_h_max = 0;
+  }
+
+  hydro_prepare_gradient_extra_kernel(p);
+  hydro_prepare_gradient_extra_visc_difn(p);
+}
+
+/**
+ * @brief Resets the variables that are required for a gradient calculation.
+ *
+ * This function is called after hydro_prepare_gradient.
+ *
+ * @param p The particle to act upon.
+ * @param xp The extended particle data to act upon.
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void hydro_reset_gradient(
+    struct part *restrict p) {}
+
+/**
+ * @brief Finishes the gradient calculation.
+ *
+ * Just a wrapper around hydro_gradients_finalize, which can be an empty method,
+ * in which case no gradients are used.
+ *
+ * This method also initializes the force loop variables.
+ *
+ * @param p The particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void hydro_end_gradient(
+    struct part *p) {
+
+  hydro_end_gradient_extra_kernel(p);
+  hydro_end_gradient_extra_visc_difn(p);
+
+  /* Set the density to be used in the force loop to be the evolved density */
+  p->rho = p->rho_evol;
+}
+
+/**
+ * @brief Prepare a particle for the force calculation.
+ *
+ * This function is called in the ghost task to convert some quantities coming
+ * from the density loop over neighbours into quantities ready to be used in the
+ * force loop over neighbours. Quantities are typically read from the density
+ * sub-structure and written to the force sub-structure.
+ * Examples of calculations done here include the calculation of viscosity term
+ * constants, thermal conduction terms, hydro conversions, etc.
+ *
+ * @param p The particle to act upon
+ * @param xp The extended particle data to act upon
+ * @param cosmo The current cosmological model.
+ * @param hydro_props Hydrodynamic properties.
+ * @param dt_alpha The time-step used to evolve non-cosmological quantities such
+ *                 as the artificial viscosity.
+ * @param dt_therm The time-step used to evolve hydrodynamical quantities.
+ */
+__attribute__((always_inline)) INLINE static void hydro_prepare_force(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct pressure_floor_props *pressure_floor, const float dt_alpha,
+    const float dt_therm) {
+
+  hydro_prepare_force_extra_kernel(p);
+
+#ifdef PLANETARY_FIXED_ENTROPY
+  /* Override the internal energy to satisfy the fixed entropy */
+  p->u = gas_internal_energy_from_entropy(p->rho_evol, p->s_fixed, p->mat_id);
+  xp->u_full = p->u;
+#endif
+
+  /* Compute the sound speed */
+  const float soundspeed =
+      gas_soundspeed_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+
+  const float div_v = p->dv_norm_kernel[0][0] + p->dv_norm_kernel[1][1] +
+                      p->dv_norm_kernel[2][2];
+  float curl_v[3];
+  curl_v[0] = p->dv_norm_kernel[1][2] - p->dv_norm_kernel[2][1];
+  curl_v[1] = p->dv_norm_kernel[2][0] - p->dv_norm_kernel[0][2];
+  curl_v[2] = p->dv_norm_kernel[0][1] - p->dv_norm_kernel[1][0];
+  const float mod_curl_v = sqrtf(curl_v[0] * curl_v[0] + curl_v[1] * curl_v[1] +
+                                 curl_v[2] * curl_v[2]);
+
+  /* Balsara switch using normalised kernel gradients (Sandnes+2025 Eqn. 34 with
+   * velocity gradients calculated by Eqn. 35) */
+  float balsara;
+  if (div_v == 0.f) {
+    balsara = 0.f;
+  } else {
+    balsara = fabsf(div_v) /
+              (fabsf(div_v) + mod_curl_v + 0.0001f * soundspeed / p->h);
+  }
+
+  /* Compute the pressure */
+  const float pressure =
+      gas_pressure_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+  p->force.pressure = pressure;
+  p->force.soundspeed = soundspeed;
+  p->force.balsara = balsara;
+  /* Set eta_crit for arificial viscosity and diffusion slope limiters */
+  p->force.eta_crit = 1.f / hydro_props->eta_neighbours;
+}
+
+/**
+ * @brief Reset acceleration fields of a particle
+ *
+ * Resets all hydro acceleration and time derivative fields in preparation
+ * for the sums taking  place in the various force tasks.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void hydro_reset_acceleration(
+    struct part *restrict p) {
+
+  /* Reset the acceleration. */
+  p->a_hydro[0] = 0.0f;
+  p->a_hydro[1] = 0.0f;
+  p->a_hydro[2] = 0.0f;
+
+  /* Reset the time derivatives. */
+  p->u_dt = 0.0f;
+  p->drho_dt = 0.0f;
+  p->force.h_dt = 0.0f;
+  p->force.v_sig = p->force.soundspeed;
+}
+
+/**
+ * @brief Sets the values to be predicted in the drifts to their values at a
+ * kick time
+ *
+ * @param p The particle.
+ * @param xp The extended data of this particle.
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void hydro_reset_predicted_values(
+    struct part *restrict p, const struct xpart *restrict xp,
+    const struct cosmology *cosmo,
+    const struct pressure_floor_props *pressure_floor) {
+
+  /* Re-set the predicted velocities */
+  p->v[0] = xp->v_full[0];
+  p->v[1] = xp->v_full[1];
+  p->v[2] = xp->v_full[2];
+
+  /* Re-set the internal energy */
+  p->u = xp->u_full;
+
+  /* Re-set the density */
+  p->rho = xp->rho_evol_full;
+  p->rho_evol = xp->rho_evol_full;
+
+  /* Compute the pressure */
+  const float pressure =
+      gas_pressure_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+
+  /* Compute the sound speed */
+  const float soundspeed =
+      gas_soundspeed_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+
+  p->force.pressure = pressure;
+  p->force.soundspeed = soundspeed;
+
+  p->force.v_sig = max(p->force.v_sig, 2.f * soundspeed);
+}
+
+/**
+ * @brief Predict additional particle fields forward in time when drifting
+ *
+ * Additional hydrodynamic quantites are drifted forward in time here. These
+ * include thermal quantities (thermal energy or total energy or entropy, ...).
+ *
+ * Note the different time-step sizes used for the different quantities as they
+ * include cosmological factors.
+ *
+ * @param p The particle.
+ * @param xp The extended data of the particle.
+ * @param dt_drift The drift time-step for positions.
+ * @param dt_therm The drift time-step for thermal quantities.
+ * @param dt_kick_grav The time-step for gravity quantities.
+ * @param cosmo The cosmological model.
+ * @param hydro_props The constants used in the scheme
+ * @param floor_props The properties of the entropy floor.
+ */
+__attribute__((always_inline)) INLINE static void hydro_predict_extra(
+    struct part *restrict p, const struct xpart *restrict xp, float dt_drift,
+    float dt_therm, float dt_kick_grav, const struct cosmology *cosmo,
+    const struct hydro_props *hydro_props,
+    const struct entropy_floor_properties *floor_props,
+    const struct pressure_floor_props *pressure_floor) {
+
+  /* Predict the internal energy and density */
+  p->u += p->u_dt * dt_therm;
+  p->rho_evol += p->drho_dt * dt_therm;
+
+  /* compute minimum SPH quantities */
+  const float h = p->h;
+  const float h_inv = 1.0f / h;                 /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
+  const float floor_rho = p->mass * kernel_root * h_inv_dim;
+  p->rho_evol = max(p->rho_evol, floor_rho);
+  p->rho = p->rho_evol;
+
+  /* Check against absolute minimum */
+  const float min_u =
+      hydro_props->minimal_internal_energy / cosmo->a_factor_internal_energy;
+
+  p->u = max(p->u, min_u);
+
+  /* Predict smoothing length */
+  const float w1 = p->force.h_dt * h_inv * dt_drift;
+  if (fabsf(w1) < 0.2f)
+    p->h *= approx_expf(w1); /* 4th order expansion of exp(w) */
+  else
+    p->h *= expf(w1);
+
+  const float floor_u = FLT_MIN;
+  p->u = max(p->u, floor_u);
+
+  /* Compute the new pressure */
+  const float pressure =
+      gas_pressure_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+
+  /* Compute the new sound speed */
+  const float soundspeed =
+      gas_soundspeed_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+
+  p->force.pressure = pressure;
+  p->force.soundspeed = soundspeed;
+
+  p->force.v_sig = max(p->force.v_sig, 2.f * soundspeed);
+}
+
+/**
+ * @brief Finishes the force calculation.
+ *
+ * Multiplies the force and accelerations by the appropiate constants
+ * and add the self-contribution term. In most cases, there is little
+ * to do here.
+ *
+ * Cosmological terms are also added/multiplied here.
+ *
+ * @param p The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void hydro_end_force(
+    struct part *restrict p, const struct cosmology *cosmo) {
+
+  p->force.h_dt *= p->h * hydro_dimension_inv;
+}
+
+/**
+ * @brief Kick the additional variables
+ *
+ * Additional hydrodynamic quantites are kicked forward in time here. These
+ * include thermal quantities (thermal energy or total energy or entropy, ...).
+ *
+ * @param p The particle to act upon.
+ * @param xp The particle extended data to act upon.
+ * @param dt_therm The time-step for this kick (for thermodynamic quantities).
+ * @param dt_grav The time-step for this kick (for gravity quantities).
+ * @param dt_grav_mesh The time-step for this kick (mesh gravity).
+ * @param dt_hydro The time-step for this kick (for hydro quantities).
+ * @param dt_kick_corr The time-step for this kick (for gravity corrections).
+ * @param cosmo The cosmological model.
+ * @param hydro_props The constants used in the scheme
+ * @param floor_props The properties of the entropy floor.
+ */
+__attribute__((always_inline)) INLINE static void hydro_kick_extra(
+    struct part *restrict p, struct xpart *restrict xp, float dt_therm,
+    float dt_grav, float dt_grav_mesh, float dt_hydro, float dt_kick_corr,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct entropy_floor_properties *floor_props) {
+
+  /* Integrate the internal energy forward in time */
+  const float delta_u = p->u_dt * dt_therm;
+
+  /* Do not decrease the energy by more than a factor of 2*/
+  xp->u_full = max(xp->u_full + delta_u, 0.5f * xp->u_full);
+
+  /* Check against absolute minimum */
+  const float min_u =
+      hydro_props->minimal_internal_energy / cosmo->a_factor_internal_energy;
+  const float floor_u = FLT_MIN;
+
+  /* Take highest of both limits */
+  const float energy_min = max(min_u, floor_u);
+
+  if (xp->u_full < energy_min) {
+    xp->u_full = energy_min;
+    p->u_dt = 0.f;
+  }
+
+  /* Integrate the density forward in time */
+  const float delta_rho = p->drho_dt * dt_therm;
+
+  /* Do not decrease the density by more than a factor of 2*/
+  xp->rho_evol_full =
+      max(xp->rho_evol_full + delta_rho, 0.5f * xp->rho_evol_full);
+
+  /* Minimum SPH density */
+  const float h = p->h;
+  const float h_inv = 1.0f / h;                 /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
+  const float floor_rho = p->mass * kernel_root * h_inv_dim;
+  if (xp->rho_evol_full < floor_rho) {
+    xp->rho_evol_full = floor_rho;
+    p->drho_dt = 0.f;
+  }
+}
+
+/**
+ * @brief Converts hydro quantity of a particle at the start of a run
+ *
+ * This function is called once at the end of the engine_init_particle()
+ * routine (at the start of a calculation) after the densities of
+ * particles have been computed.
+ * This can be used to convert internal energy into entropy for instance.
+ *
+ * @param p The particle to act upon
+ * @param xp The extended particle to act upon
+ * @param cosmo The cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void hydro_convert_quantities(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct pressure_floor_props *pressure_floor) {
+
+  /* Compute the pressure */
+  const float pressure =
+      gas_pressure_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+
+  /* Compute the sound speed */
+  const float soundspeed =
+      gas_soundspeed_from_internal_energy(p->rho_evol, p->u, p->mat_id);
+
+  p->force.pressure = pressure;
+  p->force.soundspeed = soundspeed;
+}
+
+/**
+ * @brief Initialises the particles for the first time
+ *
+ * This function is called only once just after the ICs have been
+ * read in to do some conversions or assignments between the particle
+ * and extended particle fields.
+ *
+ * @param p The particle to act upon
+ * @param xp The extended particle data to act upon
+ */
+__attribute__((always_inline)) INLINE static void hydro_first_init_part(
+    struct part *restrict p, struct xpart *restrict xp) {
+
+  p->time_bin = 0;
+  xp->v_full[0] = p->v[0];
+  xp->v_full[1] = p->v[1];
+  xp->v_full[2] = p->v[2];
+  xp->u_full = p->u;
+
+  p->rho_evol = p->rho;
+  xp->rho_evol_full = p->rho_evol;
+
+  hydro_reset_acceleration(p);
+  hydro_init_part(p, NULL);
+}
+
+/**
+ * @brief Overwrite the initial internal energy of a particle.
+ *
+ * Note that in the cases where the thermodynamic variable is not
+ * internal energy but gets converted later, we must overwrite that
+ * field. The conversion to the actual variable happens later after
+ * the initial fake time-step.
+ *
+ * @param p The #part to write to.
+ * @param u_init The new initial internal energy.
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_set_init_internal_energy(struct part *p, float u_init) {
+
+  p->u = u_init;
+}
+
+/**
+ * @brief Operations performed when a particle gets removed from the
+ * simulation volume.
+ *
+ * @param p The particle.
+ * @param xp The extended particle data.
+ * @param time The simulation time.
+ */
+__attribute__((always_inline)) INLINE static void hydro_remove_part(
+    const struct part *p, const struct xpart *xp, const double time) {
+
+  /* Print the particle info as csv to facilitate later analysis, e.g. with
+   * grep '## Removed' -A 1 --no-group-separator output.txt > removed.txt
+   */
+  printf(
+      "## Removed particle: "
+      "id, x, y, z, vx, vy, vz, m, u, P, rho, h, mat_id, time \n"
+      "%lld, %.7g, %.7g, %.7g, %.7g, %.7g, %.7g, "
+      "%.7g, %.7g, %.7g, %.7g, %.7g, %d, %.7g \n",
+      p->id, p->x[0], p->x[1], p->x[2], p->v[0], p->v[1], p->v[2], p->mass,
+      p->u, p->force.pressure, p->rho, p->h, p->mat_id, time);
+}
+
+#endif /* SWIFT_REMIX_HYDRO_H */
diff --git a/src/hydro/REMIX/hydro_debug.h b/src/hydro/REMIX/hydro_debug.h
new file mode 100644
index 0000000000000000000000000000000000000000..5fbd6c6efd985a55c5c43f2df079ae8654f90347
--- /dev/null
+++ b/src/hydro/REMIX/hydro_debug.h
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_DEBUG_H
+#define SWIFT_REMIX_HYDRO_DEBUG_H
+
+/**
+ * @file REMIX/hydro_debug.h
+ * @brief Debugging routines for REMIX SPH.
+ */
+
+__attribute__((always_inline)) INLINE static void hydro_debug_particle(
+    const struct part* p, const struct xpart* xp) {
+  warning("[PID%lld] part:", p->id);
+  warning(
+      "[PID%lld] "
+      "x=[%.6g, %.6g, %.6g], v=[%.3g, %.3g, %.3g], "
+      "a=[%.3g, %.3g, %.3g], "
+      "m=%.3g, u=%.3g, du/dt=%.3g, P=%.3g, c_s=%.3g, "
+      "v_sig=%.3g, h=%.3g, dh/dt=%.3g, wcount=%.3g, rho=%.3g, "
+      "dh_drho=%.3g, rho_evol=%.3g, time_bin=%d, wakeup=%d, mat_id=%d",
+      p->id, p->x[0], p->x[1], p->x[2], p->v[0], p->v[1], p->v[2],
+      p->a_hydro[0], p->a_hydro[1], p->a_hydro[2], p->mass, p->u, p->u_dt,
+      hydro_get_comoving_pressure(p), p->force.soundspeed, p->force.v_sig, p->h,
+      p->force.h_dt, p->density.wcount, p->rho, p->density.rho_dh, p->rho_evol,
+      p->time_bin, p->limiter_data.wakeup, p->mat_id);
+  if (xp != NULL) {
+    warning("[PID%lld] xpart:", p->id);
+    warning(
+        "[PID%lld] "
+        "v_full=[%.3g, %.3g, %.3g]",
+        p->id, xp->v_full[0], xp->v_full[1], xp->v_full[2]);
+  }
+}
+
+#endif /* SWIFT_REMIX_HYDRO_DEBUG_H */
diff --git a/src/hydro/REMIX/hydro_iact.h b/src/hydro/REMIX/hydro_iact.h
new file mode 100644
index 0000000000000000000000000000000000000000..a1b27788ef59f410b15b6e7b99fc5bb42736156b
--- /dev/null
+++ b/src/hydro/REMIX/hydro_iact.h
@@ -0,0 +1,539 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_IACT_H
+#define SWIFT_REMIX_HYDRO_IACT_H
+
+/**
+ * @file REMIX/hydro_iact.h
+ * @brief REMIX implementation of SPH (Sandnes et al. 2025)
+ */
+
+#include "adiabatic_index.h"
+#include "const.h"
+#include "hydro_kernels.h"
+#include "hydro_parameters.h"
+#include "hydro_visc_difn.h"
+#include "math.h"
+#include "minmax.h"
+
+/**
+ * @brief Density interaction between two particles.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_density(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
+
+  float wi, wj, wi_dx, wj_dx;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (pi->time_bin >= time_bin_inhibited)
+    error("Inhibited pi in interaction function!");
+  if (pj->time_bin >= time_bin_inhibited)
+    error("Inhibited pj in interaction function!");
+#endif
+
+  /* Get r */
+  const float r = sqrtf(r2);
+
+  /* Get the masses. */
+  const float mi = pi->mass;
+  const float mj = pj->mass;
+
+  /* Compute density of pi. */
+  const float hi_inv = 1.f / hi;
+  const float ui = r * hi_inv;
+  kernel_deval(ui, &wi, &wi_dx);
+
+  pi->rho += mj * wi;
+  pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
+  pi->density.wcount += wi;
+  pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+
+  /* Compute density of pj. */
+  const float hj_inv = 1.f / hj;
+  const float uj = r * hj_inv;
+  kernel_deval(uj, &wj, &wj_dx);
+
+  pj->rho += mi * wj;
+  pj->density.rho_dh -= mi * (hydro_dimension * wj + uj * wj_dx);
+  pj->density.wcount += wj;
+  pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+
+  hydro_runner_iact_density_extra_kernel(pi, pj, dx, wi, wj, wi_dx, wj_dx);
+}
+
+/**
+ * @brief Density interaction between two particles (non-symmetric).
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle (not updated).
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, const struct part *restrict pj, const float a,
+    const float H) {
+
+  float wi, wi_dx;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (pi->time_bin >= time_bin_inhibited)
+    error("Inhibited pi in interaction function!");
+  if (pj->time_bin >= time_bin_inhibited)
+    error("Inhibited pj in interaction function!");
+#endif
+
+  /* Get the masses. */
+  const float mj = pj->mass;
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  const float h_inv = 1.f / hi;
+  const float ui = r * h_inv;
+  kernel_deval(ui, &wi, &wi_dx);
+
+  pi->rho += mj * wi;
+  pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
+  pi->density.wcount += wi;
+  pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+
+  hydro_runner_iact_nonsym_density_extra_kernel(pi, pj, dx, wi, wi_dx);
+}
+
+/**
+ * @brief Calculate the gradient interaction between particle i and particle j
+ *
+ * This method wraps around hydro_gradients_collect, which can be an empty
+ * method, in which case no gradients are used.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_gradient(
+    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
+    struct part *restrict pj, float a, float H) {
+
+  float wi, wj, wi_dx, wj_dx;
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  /* Compute kernel of pi. */
+  const float hi_inv = 1.f / hi;
+  const float ui = r * hi_inv;
+  kernel_deval(ui, &wi, &wi_dx);
+
+  /* Compute kernel of pj. */
+  const float hj_inv = 1.f / hj;
+  const float uj = r * hj_inv;
+  kernel_deval(uj, &wj, &wj_dx);
+
+  hydro_runner_iact_gradient_extra_kernel(pi, pj, dx, wi, wj, wi_dx, wj_dx);
+  hydro_runner_iact_gradient_extra_visc_difn(pi, pj, dx, wi, wj, wi_dx, wj_dx);
+}
+
+/**
+ * @brief Calculate the gradient interaction between particle i and particle j:
+ * non-symmetric version
+ *
+ * This method wraps around hydro_gradients_nonsym_collect, which can be an
+ * empty method, in which case no gradients are used.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle (not updated).
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_nonsym_gradient(
+    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
+    struct part *restrict pj, float a, float H) {
+
+  float wi, wj, wi_dx, wj_dx;
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  /* Compute kernel of pi. */
+  const float h_inv = 1.f / hi;
+  const float ui = r * h_inv;
+  kernel_deval(ui, &wi, &wi_dx);
+
+  /* Compute kernel of pj. */
+  const float hj_inv = 1.f / hj;
+  const float uj = r * hj_inv;
+  kernel_deval(uj, &wj, &wj_dx);
+
+  hydro_runner_iact_nonsym_gradient_extra_kernel(pi, pj, dx, wi, wj, wi_dx,
+                                                 wj_dx);
+  hydro_runner_iact_nonsym_gradient_extra_visc_difn(pi, pj, dx, wi, wi_dx);
+}
+
+/**
+ * @brief Force interaction between two particles.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_force(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (pi->time_bin >= time_bin_inhibited)
+    error("Inhibited pi in interaction function!");
+  if (pj->time_bin >= time_bin_inhibited)
+    error("Inhibited pj in interaction function!");
+#endif
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  /* Recover some data */
+  const float mi = pi->mass;
+  const float mj = pj->mass;
+  const float rhoi = pi->rho;
+  const float rhoj = pj->rho;
+  const float rhoi_inv = 1.f / rhoi;
+  const float rhoj_inv = 1.f / rhoj;
+  const float pressurei = pi->force.pressure;
+  const float pressurej = pj->force.pressure;
+
+  /* Get the kernel for hi. */
+  const float hi_inv = 1.0f / hi;
+  const float xi = r * hi_inv;
+  float wi, wi_dx;
+  kernel_deval(xi, &wi, &wi_dx);
+
+  /* Get the kernel for hj. */
+  const float hj_inv = 1.0f / hj;
+  const float xj = r * hj_inv;
+  float wj, wj_dx;
+  kernel_deval(xj, &wj, &wj_dx);
+
+  /* Linear-order reproducing kernel gradient term (Sandnes+2025 Eqn. 28) */
+  float Gj[3], Gi[3], G_mean[3];
+  hydro_set_Gi_Gj_forceloop(Gi, Gj, pi, pj, dx, wi, wj, wi_dx, wj_dx);
+
+  /* Antisymmetric kernel grad term for conservation of momentum and energy */
+  G_mean[0] = 0.5f * (Gi[0] - Gj[0]);
+  G_mean[1] = 0.5f * (Gi[1] - Gj[1]);
+  G_mean[2] = 0.5f * (Gi[2] - Gj[2]);
+
+  /* Viscous pressures (Sandnes+2025 Eqn. 41) */
+  float Qi, Qj;
+  float visc_signal_velocity, difn_signal_velocity;
+  hydro_set_Qi_Qj(&Qi, &Qj, &visc_signal_velocity, &difn_signal_velocity, pi,
+                  pj, dx);
+
+  /* Pressure terms to be used in evolution equations */
+  const float P_i_term = pressurei * rhoi_inv * rhoj_inv;
+  const float P_j_term = pressurej * rhoi_inv * rhoj_inv;
+  const float Q_i_term = Qi * rhoi_inv * rhoj_inv;
+  const float Q_j_term = Qj * rhoi_inv * rhoj_inv;
+
+  /* Use the force Luke! */
+  pi->a_hydro[0] -=
+      mj * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[0];
+  pi->a_hydro[1] -=
+      mj * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[1];
+  pi->a_hydro[2] -=
+      mj * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[2];
+
+  pj->a_hydro[0] +=
+      mi * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[0];
+  pj->a_hydro[1] +=
+      mi * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[1];
+  pj->a_hydro[2] +=
+      mi * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[2];
+
+  /* v_ij dot kernel gradient term */
+  const float dvdotG = (pi->v[0] - pj->v[0]) * G_mean[0] +
+                       (pi->v[1] - pj->v[1]) * G_mean[1] +
+                       (pi->v[2] - pj->v[2]) * G_mean[2];
+
+  /* Internal energy time derivative */
+  pi->u_dt += mj * (P_i_term + Q_i_term) * dvdotG;
+  pj->u_dt += mi * (P_j_term + Q_j_term) * dvdotG;
+
+  /* Density time derivative */
+  pi->drho_dt += mj * (rhoi * rhoj_inv) * dvdotG;
+  pj->drho_dt += mi * (rhoj * rhoi_inv) * dvdotG;
+
+  /* Get the time derivative for h. */
+  pi->force.h_dt -= mj * dvdotG * rhoj_inv;
+  pj->force.h_dt -= mi * dvdotG * rhoi_inv;
+
+  const float v_sig = visc_signal_velocity;
+
+  /* Update the signal velocity. */
+  pi->force.v_sig = max(pi->force.v_sig, v_sig);
+  pj->force.v_sig = max(pj->force.v_sig, v_sig);
+
+  if ((pi->is_h_max) || (pj->is_h_max)) {
+    /* Do not add diffusion or normalising terms if either particle has h=h_max
+     */
+    return;
+  }
+
+  /* Calculate some quantities */
+  const float mean_rho = 0.5f * (rhoi + rhoj);
+  const float mean_rho_inv = 1.f / mean_rho;
+  const float mean_balsara = 0.5f * (pi->force.balsara + pj->force.balsara);
+  const float mod_G = sqrtf(G_mean[0] * G_mean[0] + G_mean[1] * G_mean[1] +
+                            G_mean[2] * G_mean[2]);
+  const float v_sig_norm = sqrtf((pi->v[0] - pj->v[0]) * (pi->v[0] - pj->v[0]) +
+                                 (pi->v[1] - pj->v[1]) * (pi->v[1] - pj->v[1]) +
+                                 (pi->v[2] - pj->v[2]) * (pi->v[2] - pj->v[2]));
+
+  /* Add normalising term to density evolution (Sandnes+2025 Eqn. 51) */
+  const float alpha_norm = const_remix_norm_alpha;
+  float drho_dt_norm_and_difn_i = alpha_norm * mj * v_sig_norm *
+                                  pi->force.vac_switch *
+                                  (pi->m0 * rhoi - rhoi) * mod_G * mean_rho_inv;
+  float drho_dt_norm_and_difn_j = alpha_norm * mi * v_sig_norm *
+                                  pj->force.vac_switch *
+                                  (pj->m0 * rhoj - rhoj) * mod_G * mean_rho_inv;
+
+  /* Only include diffusion for same-material particle pair */
+  if (pi->mat_id == pj->mat_id) {
+    /* Diffusion parameters */
+    const float a_difn_rho = const_remix_difn_a_rho;
+    const float b_difn_rho = const_remix_difn_b_rho;
+    const float a_difn_u = const_remix_difn_a_u;
+    const float b_difn_u = const_remix_difn_b_u;
+
+    float utilde_i, utilde_j, rhotilde_i, rhotilde_j;
+    hydro_set_u_rho_difn(&utilde_i, &utilde_j, &rhotilde_i, &rhotilde_j, pi, pj,
+                         dx);
+    const float v_sig_difn = difn_signal_velocity;
+
+    /* Calculate artificial diffusion of internal energy (Sandnes+2025 Eqn. 42)
+     */
+    const float du_dt_difn_i = -(a_difn_u + b_difn_u * mean_balsara) * mj *
+                               v_sig_difn * (utilde_i - utilde_j) * mod_G *
+                               mean_rho_inv;
+    const float du_dt_difn_j = -(a_difn_u + b_difn_u * mean_balsara) * mi *
+                               v_sig_difn * (utilde_j - utilde_i) * mod_G *
+                               mean_rho_inv;
+
+    /* Add artificial diffusion to evolution of internal energy */
+    pi->u_dt += du_dt_difn_i;
+    pj->u_dt += du_dt_difn_j;
+
+    /* Calculate artificial diffusion of density (Sandnes+2025 Eqn. 43) */
+    drho_dt_norm_and_difn_i += -(a_difn_rho + b_difn_rho * mean_balsara) * mj *
+                               (rhoi * rhoj_inv) * v_sig_difn *
+                               (rhotilde_i - rhotilde_j) * mod_G * mean_rho_inv;
+    drho_dt_norm_and_difn_j += -(a_difn_rho + b_difn_rho * mean_balsara) * mi *
+                               (rhoj * rhoi_inv) * v_sig_difn *
+                               (rhotilde_j - rhotilde_i) * mod_G * mean_rho_inv;
+  }
+
+  /* Add normalising term and artificial diffusion to evolution of density */
+  pi->drho_dt += drho_dt_norm_and_difn_i;
+  pj->drho_dt += drho_dt_norm_and_difn_j;
+}
+
+/**
+ * @brief Force interaction between two particles (non-symmetric).
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle (not updated).
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, const struct part *restrict pj, const float a,
+    const float H) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (pi->time_bin >= time_bin_inhibited)
+    error("Inhibited pi in interaction function!");
+  if (pj->time_bin >= time_bin_inhibited)
+    error("Inhibited pj in interaction function!");
+#endif
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  /* Recover some data */
+  const float mj = pj->mass;
+  const float rhoi = pi->rho;
+  const float rhoj = pj->rho;
+  const float rhoi_inv = 1.f / rhoi;
+  const float rhoj_inv = 1.f / rhoj;
+  const float pressurei = pi->force.pressure;
+  const float pressurej = pj->force.pressure;
+
+  /* Get the kernel for hi. */
+  const float hi_inv = 1.0f / hi;
+  const float xi = r * hi_inv;
+  float wi, wi_dx;
+  kernel_deval(xi, &wi, &wi_dx);
+
+  /* Get the kernel for hj. */
+  const float hj_inv = 1.0f / hj;
+  const float xj = r * hj_inv;
+  float wj, wj_dx;
+  kernel_deval(xj, &wj, &wj_dx);
+
+  /* Linear-order reproducing kernel gradient term (Sandnes+2025 Eqn. 28) */
+  float Gj[3], Gi[3], G_mean[3];
+  hydro_set_Gi_Gj_forceloop(Gi, Gj, pi, pj, dx, wi, wj, wi_dx, wj_dx);
+
+  /* Antisymmetric kernel grad term for conservation of momentum and energy */
+  G_mean[0] = 0.5f * (Gi[0] - Gj[0]);
+  G_mean[1] = 0.5f * (Gi[1] - Gj[1]);
+  G_mean[2] = 0.5f * (Gi[2] - Gj[2]);
+
+  /* Viscous pressures (Sandnes+2025 Eqn. 41) */
+  float Qi, Qj;
+  float visc_signal_velocity, difn_signal_velocity;
+  hydro_set_Qi_Qj(&Qi, &Qj, &visc_signal_velocity, &difn_signal_velocity, pi,
+                  pj, dx);
+
+  /* Pressure terms to be used in evolution equations */
+  const float P_i_term = pressurei * rhoi_inv * rhoj_inv;
+  const float P_j_term = pressurej * rhoi_inv * rhoj_inv;
+  const float Q_i_term = Qi * rhoi_inv * rhoj_inv;
+  const float Q_j_term = Qj * rhoi_inv * rhoj_inv;
+
+  /* Use the force Luke! */
+  pi->a_hydro[0] -=
+      mj * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[0];
+  pi->a_hydro[1] -=
+      mj * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[1];
+  pi->a_hydro[2] -=
+      mj * (P_i_term + P_j_term + Q_i_term + Q_j_term) * G_mean[2];
+
+  /* v_ij dot kernel gradient term */
+  const float dvdotG = (pi->v[0] - pj->v[0]) * G_mean[0] +
+                       (pi->v[1] - pj->v[1]) * G_mean[1] +
+                       (pi->v[2] - pj->v[2]) * G_mean[2];
+
+  /* Internal energy time derivative */
+  pi->u_dt += mj * (P_i_term + Q_i_term) * dvdotG;
+
+  /* Density time derivative */
+  pi->drho_dt += mj * (rhoi * rhoj_inv) * dvdotG;
+
+  /* Get the time derivative for h. */
+  pi->force.h_dt -= mj * dvdotG * rhoj_inv;
+
+  const float v_sig = visc_signal_velocity;
+
+  /* Update the signal velocity. */
+  pi->force.v_sig = max(pi->force.v_sig, v_sig);
+
+  if ((pi->is_h_max) || (pj->is_h_max)) {
+    /* Do not add diffusion or normalising terms if either particle has h=h_max
+     */
+    return;
+  }
+
+  /* Calculate some quantities */
+  const float mean_rho = 0.5f * (rhoi + rhoj);
+  const float mean_rho_inv = 1.f / mean_rho;
+  const float mean_balsara = 0.5f * (pi->force.balsara + pj->force.balsara);
+  const float mod_G = sqrtf(G_mean[0] * G_mean[0] + G_mean[1] * G_mean[1] +
+                            G_mean[2] * G_mean[2]);
+
+  const float v_sig_norm = sqrtf((pi->v[0] - pj->v[0]) * (pi->v[0] - pj->v[0]) +
+                                 (pi->v[1] - pj->v[1]) * (pi->v[1] - pj->v[1]) +
+                                 (pi->v[2] - pj->v[2]) * (pi->v[2] - pj->v[2]));
+
+  /* Add normalising term to density evolution (Sandnes+2025 Eqn. 51) */
+  const float alpha_norm = const_remix_norm_alpha;
+  float drho_dt_norm_and_difn_i = alpha_norm * mj * v_sig_norm *
+                                  pi->force.vac_switch *
+                                  (pi->m0 * rhoi - rhoi) * mod_G * mean_rho_inv;
+
+  /* Only include diffusion for same-material particle pair */
+  if (pi->mat_id == pj->mat_id) {
+    /* Diffusion parameters */
+    const float a_difn_rho = const_remix_difn_a_rho;
+    const float b_difn_rho = const_remix_difn_b_rho;
+    const float a_difn_u = const_remix_difn_a_u;
+    const float b_difn_u = const_remix_difn_b_u;
+
+    float utilde_i, utilde_j, rhotilde_i, rhotilde_j;
+    hydro_set_u_rho_difn(&utilde_i, &utilde_j, &rhotilde_i, &rhotilde_j, pi, pj,
+                         dx);
+    const float v_sig_difn = difn_signal_velocity;
+
+    /* Calculate artificial diffusion of internal energy (Sandnes+2025 Eqn. 42)
+     */
+    const float du_dt_difn_i = -(a_difn_u + b_difn_u * mean_balsara) * mj *
+                               v_sig_difn * (utilde_i - utilde_j) * mod_G *
+                               mean_rho_inv;
+
+    /* Add artificial diffusion to evolution of internal energy */
+    pi->u_dt += du_dt_difn_i;
+
+    /* Calculate artificial diffusion of density (Sandnes+2025 Eqn. 43) */
+    drho_dt_norm_and_difn_i += -(a_difn_rho + b_difn_rho * mean_balsara) * mj *
+                               (rhoi * rhoj_inv) * v_sig_difn *
+                               (rhotilde_i - rhotilde_j) * mod_G * mean_rho_inv;
+  }
+
+  /* Add normalising term and artificial diffusion to evolution of density */
+  pi->drho_dt += drho_dt_norm_and_difn_i;
+}
+
+#endif /* SWIFT_REMIX_HYDRO_IACT_H */
diff --git a/src/hydro/REMIX/hydro_io.h b/src/hydro/REMIX/hydro_io.h
new file mode 100644
index 0000000000000000000000000000000000000000..1972ef029deb545ef18e486eb4eaf31c90476e4e
--- /dev/null
+++ b/src/hydro/REMIX/hydro_io.h
@@ -0,0 +1,251 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_IO_H
+#define SWIFT_REMIX_HYDRO_IO_H
+
+/**
+ * @file REMIX/hydro_io.h
+ * @brief REMIX implementation of SPH (Sandnes et al. 2025) i/o routines
+ */
+
+#include "adiabatic_index.h"
+#include "hydro.h"
+#include "hydro_parameters.h"
+#include "io_properties.h"
+#include "kernel_hydro.h"
+
+/**
+ * @brief Specifies which particle fields to read from a dataset
+ *
+ * @param parts The particle array.
+ * @param list The list of i/o properties to read.
+ * @param num_fields The number of i/o fields to read.
+ */
+INLINE static void hydro_read_particles(struct part* parts,
+                                        struct io_props* list,
+                                        int* num_fields) {
+
+#ifdef PLANETARY_FIXED_ENTROPY
+  *num_fields = 10;
+#else
+  *num_fields = 9;
+#endif
+
+  /* List what we want to read */
+  list[0] = io_make_input_field("Coordinates", DOUBLE, 3, COMPULSORY,
+                                UNIT_CONV_LENGTH, parts, x);
+  list[1] = io_make_input_field("Velocities", FLOAT, 3, COMPULSORY,
+                                UNIT_CONV_SPEED, parts, v);
+  list[2] = io_make_input_field("Masses", FLOAT, 1, COMPULSORY, UNIT_CONV_MASS,
+                                parts, mass);
+  list[3] = io_make_input_field("SmoothingLength", FLOAT, 1, COMPULSORY,
+                                UNIT_CONV_LENGTH, parts, h);
+  list[4] = io_make_input_field("InternalEnergy", FLOAT, 1, COMPULSORY,
+                                UNIT_CONV_ENERGY_PER_UNIT_MASS, parts, u);
+  list[5] = io_make_input_field("ParticleIDs", ULONGLONG, 1, COMPULSORY,
+                                UNIT_CONV_NO_UNITS, parts, id);
+  list[6] = io_make_input_field("Accelerations", FLOAT, 3, OPTIONAL,
+                                UNIT_CONV_ACCELERATION, parts, a_hydro);
+  list[7] = io_make_input_field("Density", FLOAT, 1, COMPULSORY,
+                                UNIT_CONV_DENSITY, parts, rho);
+  list[8] = io_make_input_field("MaterialIDs", INT, 1, COMPULSORY,
+                                UNIT_CONV_NO_UNITS, parts, mat_id);
+#ifdef PLANETARY_FIXED_ENTROPY
+  list[9] = io_make_input_field("Entropies", FLOAT, 1, COMPULSORY,
+                                UNIT_CONV_PHYSICAL_ENTROPY_PER_UNIT_MASS, parts,
+                                s_fixed);
+#endif
+}
+
+INLINE static void convert_S(const struct engine* e, const struct part* p,
+                             const struct xpart* xp, float* ret) {
+
+  ret[0] = hydro_get_comoving_entropy(p, xp);
+}
+
+INLINE static void convert_P(const struct engine* e, const struct part* p,
+                             const struct xpart* xp, float* ret) {
+
+  ret[0] = hydro_get_comoving_pressure(p);
+}
+
+INLINE static void convert_part_pos(const struct engine* e,
+                                    const struct part* p,
+                                    const struct xpart* xp, double* ret) {
+  const struct space* s = e->s;
+  if (s->periodic) {
+    ret[0] = box_wrap(p->x[0], 0.0, s->dim[0]);
+    ret[1] = box_wrap(p->x[1], 0.0, s->dim[1]);
+    ret[2] = box_wrap(p->x[2], 0.0, s->dim[2]);
+  } else {
+    ret[0] = p->x[0];
+    ret[1] = p->x[1];
+    ret[2] = p->x[2];
+  }
+  if (e->snapshot_use_delta_from_edge) {
+    ret[0] = min(ret[0], s->dim[0] - e->snapshot_delta_from_edge);
+    ret[1] = min(ret[1], s->dim[1] - e->snapshot_delta_from_edge);
+    ret[2] = min(ret[2], s->dim[2] - e->snapshot_delta_from_edge);
+  }
+}
+
+INLINE static void convert_part_vel(const struct engine* e,
+                                    const struct part* p,
+                                    const struct xpart* xp, float* ret) {
+
+  const int with_cosmology = (e->policy & engine_policy_cosmology);
+  const struct cosmology* cosmo = e->cosmology;
+  const integertime_t ti_current = e->ti_current;
+  const double time_base = e->time_base;
+  const float dt_kick_grav_mesh = e->dt_kick_grav_mesh_for_io;
+
+  const integertime_t ti_beg = get_integer_time_begin(ti_current, p->time_bin);
+  const integertime_t ti_end = get_integer_time_end(ti_current, p->time_bin);
+
+  /* Get time-step since the last kick */
+  float dt_kick_grav, dt_kick_hydro;
+  if (with_cosmology) {
+    dt_kick_grav = cosmology_get_grav_kick_factor(cosmo, ti_beg, ti_current);
+    dt_kick_grav -=
+        cosmology_get_grav_kick_factor(cosmo, ti_beg, (ti_beg + ti_end) / 2);
+    dt_kick_hydro = cosmology_get_hydro_kick_factor(cosmo, ti_beg, ti_current);
+    dt_kick_hydro -=
+        cosmology_get_hydro_kick_factor(cosmo, ti_beg, (ti_beg + ti_end) / 2);
+  } else {
+    dt_kick_grav = (ti_current - ((ti_beg + ti_end) / 2)) * time_base;
+    dt_kick_hydro = (ti_current - ((ti_beg + ti_end) / 2)) * time_base;
+  }
+
+  /* Extrapolate the velocites to the current time (hydro term)*/
+  ret[0] = xp->v_full[0] + p->a_hydro[0] * dt_kick_hydro;
+  ret[1] = xp->v_full[1] + p->a_hydro[1] * dt_kick_hydro;
+  ret[2] = xp->v_full[2] + p->a_hydro[2] * dt_kick_hydro;
+
+  /* Add the gravity term */
+  if (p->gpart != NULL) {
+    ret[0] += p->gpart->a_grav[0] * dt_kick_grav;
+    ret[1] += p->gpart->a_grav[1] * dt_kick_grav;
+    ret[2] += p->gpart->a_grav[2] * dt_kick_grav;
+  }
+
+  /* And the mesh gravity term */
+  if (p->gpart != NULL) {
+    ret[0] += p->gpart->a_grav_mesh[0] * dt_kick_grav_mesh;
+    ret[1] += p->gpart->a_grav_mesh[1] * dt_kick_grav_mesh;
+    ret[2] += p->gpart->a_grav_mesh[2] * dt_kick_grav_mesh;
+  }
+
+  /* Conversion from internal units to peculiar velocities */
+  ret[0] *= cosmo->a_inv;
+  ret[1] *= cosmo->a_inv;
+  ret[2] *= cosmo->a_inv;
+}
+
+INLINE static void convert_part_potential(const struct engine* e,
+                                          const struct part* p,
+                                          const struct xpart* xp, float* ret) {
+
+  if (p->gpart != NULL)
+    ret[0] = gravity_get_comoving_potential(p->gpart);
+  else
+    ret[0] = 0.f;
+}
+
+/**
+ * @brief Specifies which particle fields to write to a dataset
+ *
+ * @param parts The particle array.
+ * @param xparts The extended particle array.
+ * @param list The list of i/o properties to write.
+ * @param num_fields The number of i/o fields to write.
+ */
+INLINE static void hydro_write_particles(const struct part* parts,
+                                         const struct xpart* xparts,
+                                         struct io_props* list,
+                                         int* num_fields) {
+
+  *num_fields = 11;
+
+  /* List what we want to write */
+  list[0] = io_make_output_field_convert_part(
+      "Coordinates", DOUBLE, 3, UNIT_CONV_LENGTH, 1.f, parts, xparts,
+      convert_part_pos, "Positions of the particles");
+  list[1] = io_make_output_field_convert_part(
+      "Velocities", FLOAT, 3, UNIT_CONV_SPEED, 0.f, parts, xparts,
+      convert_part_vel, "Velocities of the particles");
+  list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f, parts,
+                                 mass, "Masses of the particles");
+  list[3] = io_make_output_field(
+      "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, parts, h,
+      "Smoothing lengths (FWHM of the kernel) of the particles");
+  list[4] = io_make_output_field(
+      "InternalEnergies", FLOAT, 1, UNIT_CONV_ENERGY_PER_UNIT_MASS,
+      -3.f * hydro_gamma_minus_one, parts, u,
+      "Thermal energies per unit mass of the particles");
+  list[5] =
+      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
+                           parts, id, "Unique IDs of the particles");
+  list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
+                                 parts, rho, "Densities of the particles");
+  list[7] = io_make_output_field_convert_part(
+      "Entropies", FLOAT, 1, UNIT_CONV_ENTROPY_PER_UNIT_MASS, 0.f, parts,
+      xparts, convert_S, "Entropies per unit mass of the particles");
+  list[8] =
+      io_make_output_field("MaterialIDs", INT, 1, UNIT_CONV_NO_UNITS, 0.f,
+                           parts, mat_id, "Material IDs of the particles");
+  list[9] = io_make_output_field_convert_part(
+      "Pressures", FLOAT, 1, UNIT_CONV_PRESSURE, -3.f * hydro_gamma, parts,
+      xparts, convert_P, "Pressures of the particles");
+  list[10] = io_make_output_field_convert_part(
+      "Potentials", FLOAT, 1, UNIT_CONV_POTENTIAL, 0.f, parts, xparts,
+      convert_part_potential, "Gravitational potentials of the particles");
+}
+
+/**
+ * @brief Writes the current model of SPH to the file
+ * @param h_grpsph The HDF5 group in which to write
+ */
+INLINE static void hydro_write_flavour(hid_t h_grpsph) {
+
+  io_write_attribute_s(h_grpsph, "Density estimate", "Evolved in time");
+  io_write_attribute_s(h_grpsph, "EoM free functions", "Evolved densities");
+  io_write_attribute_s(h_grpsph, "Kernel gradients",
+                       "Linear-order reproducing kernels with grad-h terms");
+  io_write_attribute_s(h_grpsph, "Vacuum boundary treatment",
+                       "As presented in Sandnes et al. (2025)");
+  io_write_attribute_s(
+      h_grpsph, "Artificial viscosity",
+      "With linear reconstruction, van Leer slope limiter, Balsara switch");
+  io_write_attribute_s(
+      h_grpsph, "Artificial diffusion",
+      "With linear reconstruction, van Leer slope limiter, Balsara switch");
+  io_write_attribute_s(h_grpsph, "Normalising term",
+                       "As presented in Sandnes et al. (2025)");
+}
+
+/**
+ * @brief Are we writing entropy in the internal energy field ?
+ *
+ * @return 1 if entropy is in 'internal energy', 0 otherwise.
+ */
+INLINE static int writeEntropyFlag(void) { return 0; }
+
+#endif /* SWIFT_REMIX_HYDRO_IO_H */
diff --git a/src/hydro/REMIX/hydro_kernels.h b/src/hydro/REMIX/hydro_kernels.h
new file mode 100644
index 0000000000000000000000000000000000000000..8d4edcb69fc71f894fec89a6869921369a8283b0
--- /dev/null
+++ b/src/hydro/REMIX/hydro_kernels.h
@@ -0,0 +1,644 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_KERNELS_H
+#define SWIFT_REMIX_HYDRO_KERNELS_H
+
+/**
+ * @file REMIX/hydro_kernels.h
+ * @brief Utilities for REMIX hydro kernels.
+ */
+
+#include "const.h"
+#include "hydro_parameters.h"
+#include "math.h"
+
+/**
+ * @brief Prepares extra kernel parameters for a particle for the density
+ * calculation.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void hydro_init_part_extra_kernel(
+    struct part *restrict p) {
+
+  p->m0 = 0.f;
+  p->grad_m0[0] = 0.f;
+  p->grad_m0[1] = 0.f;
+  p->grad_m0[2] = 0.f;
+}
+
+/**
+ * @brief Extra kernel density interaction between two particles
+ *
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param wi The value of the unmodified kernel function W(r, hi) * hi^d.
+ * @param wj The value of the unmodified kernel function W(r, hj) * hj^d.
+ * @param wi_dx The norm of the gradient of wi: dW(r, hi)/dr * hi^(d+1).
+ * @param wj_dx The norm of the gradient of wj: dW(r, hj)/dr * hj^(d+1).
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_runner_iact_density_extra_kernel(struct part *restrict pi,
+                                       struct part *restrict pj,
+                                       const float dx[3], const float wi,
+                                       const float wj, const float wi_dx,
+                                       const float wj_dx) {
+
+  /* Get r and 1/r. */
+  const float r = sqrtf(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  const float r_inv = r ? 1.0f / r : 0.0f;
+  const float volume_i = pi->mass / pi->rho_evol;
+  const float volume_j = pj->mass / pj->rho_evol;
+
+  /* Geometric moments and gradients that use an unmodified kernel (Sandnes+2025
+   * Eqn. 50 and its gradient). Used in the normalising term (Eqn. 51) and in
+   * gradient estimates (using Eqn. 30) that are used for the calculation of
+   * grad-h terms (Eqn. 31) and in the artificial viscosity (Eqn. 35) and
+   * diffusion (Eqns. 46 and 47) schemes */
+  pi->m0 += wi * volume_j;
+  pj->m0 += wj * volume_i;
+
+  pi->grad_m0[0] += dx[0] * wi_dx * r_inv * volume_j;
+  pi->grad_m0[1] += dx[1] * wi_dx * r_inv * volume_j;
+  pi->grad_m0[2] += dx[2] * wi_dx * r_inv * volume_j;
+
+  pj->grad_m0[0] += -dx[0] * wj_dx * r_inv * volume_i;
+  pj->grad_m0[1] += -dx[1] * wj_dx * r_inv * volume_i;
+  pj->grad_m0[2] += -dx[2] * wj_dx * r_inv * volume_i;
+}
+
+/**
+ * @brief Extra kernel density interaction between two particles (non-symmetric)
+ *
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param wi The value of the unmodified kernel function W(r, hi) * hi^d.
+ * @param wi_dx The norm of the gradient of wi: dW(r, hi)/dr * hi^(d+1).
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_runner_iact_nonsym_density_extra_kernel(struct part *restrict pi,
+                                              const struct part *restrict pj,
+                                              const float dx[3], const float wi,
+                                              const float wi_dx) {
+
+  /* Get r and 1/r. */
+  const float r = sqrtf(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  const float r_inv = r ? 1.0f / r : 0.0f;
+  const float volume_j = pj->mass / pj->rho_evol;
+
+  /* Geometric moments and gradients that use an unmodified kernel (Sandnes+2025
+   * Eqn. 50 and its gradient). Used in the normalising term (Eqn. 51) and in
+   * gradient estimates (using Eqn. 30) that are used for the calculation of
+   * grad-h terms (Eqn. 31) and in the artificial viscosity (Eqn. 35) and
+   * diffusion (Eqns. 46 and 47) schemes */
+  pi->m0 += wi * volume_j;
+
+  pi->grad_m0[0] += dx[0] * wi_dx * r_inv * volume_j;
+  pi->grad_m0[1] += dx[1] * wi_dx * r_inv * volume_j;
+  pi->grad_m0[2] += dx[2] * wi_dx * r_inv * volume_j;
+}
+
+/**
+ * @brief Finishes extra kernel parts of the density calculation.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_end_density_extra_kernel(struct part *restrict p) {
+
+  const float h = p->h;
+  const float h_inv = 1.0f / h;                       /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv);       /* 1/h^d */
+  const float h_inv_dim_plus_one = h_inv_dim * h_inv; /* 1/h^(d+1) */
+
+  /* Geometric moments and gradients that use an unmodified kernel (Sandnes+2025
+   * Eqn. 50 and its gradient). Used in the normalising term (Eqn. 51) and in
+   * gradient estimates (using Eqn. 30) that are used for the calculation of
+   * grad-h terms (Eqn. 31) and in the artificial viscosity (Eqn. 35) and
+   * diffusion (Eqns. 46 and 47) schemes */
+  p->m0 += p->mass * kernel_root / p->rho_evol;
+  p->m0 *= h_inv_dim;
+  p->grad_m0[0] *= h_inv_dim_plus_one;
+  p->grad_m0[1] *= h_inv_dim_plus_one;
+  p->grad_m0[2] *= h_inv_dim_plus_one;
+}
+
+/**
+ * @brief Prepares extra kernel parameters for a particle for the gradient
+ * calculation.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_prepare_gradient_extra_kernel(struct part *restrict p) {
+
+  /* Initialise geometric moment matrices (ij-mean, for linear-order repr.
+   * kernel) */
+  zero_sym_matrix(&p->gradient.m2_bar);
+  zero_sym_matrix(&p->gradient.grad_m2_bar[0]);
+  zero_sym_matrix(&p->gradient.grad_m2_bar[1]);
+  zero_sym_matrix(&p->gradient.grad_m2_bar[2]);
+  zero_sym_matrix(&p->gradient.grad_m2_bar_gradhterm);
+
+  /* Geometric moments and gradients that us a kernel given by 0.5 * (W_{ij} +
+   * W_{ji}). These are used to construct the linear-order repr. kernel */
+  p->gradient.m0_bar = 0.f;
+  p->gradient.grad_m0_bar_gradhterm = 0.f;
+  memset(p->gradient.m1_bar, 0.f, 3 * sizeof(float));
+  memset(p->gradient.grad_m0_bar, 0.f, 3 * sizeof(float));
+  memset(p->gradient.grad_m1_bar_gradhterm, 0.f, 3 * sizeof(float));
+  memset(p->gradient.grad_m1_bar, 0.f, 3 * 3 * sizeof(float));
+}
+
+/**
+ * @brief Extra kernel gradient interaction between two particles
+ *
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param wi The value of the unmodified kernel function W(r, hi) * hi^d.
+ * @param wj The value of the unmodified kernel function W(r, hj) * hj^d.
+ * @param wi_dx The norm of the gradient of wi: dW(r, hi)/dr * hi^(d+1).
+ * @param wj_dx The norm of the gradient of wj: dW(r, hj)/dr * hj^(d+1).
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_runner_iact_gradient_extra_kernel(struct part *restrict pi,
+                                        struct part *restrict pj,
+                                        const float dx[3], const float wi,
+                                        const float wj, const float wi_dx,
+                                        const float wj_dx) {
+
+  /* Get r and 1/r. */
+  const float r = sqrtf(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  const float r_inv = r ? 1.0f / r : 0.0f;
+
+  const float hi = pi->h;
+  const float hi_inv = 1.0f / hi;                        /* 1/h */
+  const float hi_inv_dim = pow_dimension(hi_inv);        /* 1/h^d */
+  const float hi_inv_dim_plus_one = hi_inv_dim * hi_inv; /* 1/h^(d+1) */
+
+  const float hj = pj->h;
+  const float hj_inv = 1.0f / hj;                        /* 1/h */
+  const float hj_inv_dim = pow_dimension(hj_inv);        /* 1/h^d */
+  const float hj_inv_dim_plus_one = hj_inv_dim * hj_inv; /* 1/h^(d+1) */
+
+  /* Volume elements */
+  const float volume_i = pi->mass / pi->rho_evol;
+  const float volume_j = pj->mass / pj->rho_evol;
+
+  /* Mean ij kernels and gradients */
+  const float wi_term = 0.5f * (wi * hi_inv_dim + wj * hj_inv_dim);
+  const float wj_term = wi_term;
+  float wi_dx_term[3], wj_dx_term[3];
+  const float mean_dw_dr =
+      0.5f * (wi_dx * hi_inv_dim_plus_one + wj_dx * hj_inv_dim_plus_one);
+  wi_dx_term[0] = dx[0] * r_inv * mean_dw_dr;
+  wi_dx_term[1] = dx[1] * r_inv * mean_dw_dr;
+  wi_dx_term[2] = dx[2] * r_inv * mean_dw_dr;
+  wj_dx_term[0] = -wi_dx_term[0];
+  wj_dx_term[1] = -wi_dx_term[1];
+  wj_dx_term[2] = -wi_dx_term[2];
+
+  /* Grad-h term, dW/dh */
+  const float wi_dx_gradhterm = -0.5f *
+                                (hydro_dimension * wi + (r * hi_inv) * wi_dx) *
+                                hi_inv_dim_plus_one;
+  const float wj_dx_gradhterm = -0.5f *
+                                (hydro_dimension * wj + (r * hj_inv) * wj_dx) *
+                                hj_inv_dim_plus_one;
+
+  /* Geometric moments m_0, m_1, and m_2 (Sandnes+2025 Eqns. 24--26), their
+   * gradients (Sandnes+2025 Eqns. B.10--B.12, initially we only construct the
+   * first terms in Eqns. B.11 and B.12) and grad-h terms (from second term
+   * in Eqn. 29 when used in Eqns. B.10--B.12)*/
+  pi->gradient.m0_bar += wi_term * volume_j;
+  pj->gradient.m0_bar += wj_term * volume_i;
+
+  pi->gradient.grad_m0_bar_gradhterm += wi_dx_gradhterm * volume_j;
+  pj->gradient.grad_m0_bar_gradhterm += wj_dx_gradhterm * volume_i;
+  for (int i = 0; i < 3; i++) {
+    pi->gradient.m1_bar[i] += dx[i] * wi_term * volume_j;
+    pj->gradient.m1_bar[i] += -dx[i] * wj_term * volume_i;
+
+    pi->gradient.grad_m0_bar[i] += wi_dx_term[i] * volume_j;
+    pj->gradient.grad_m0_bar[i] += wj_dx_term[i] * volume_i;
+
+    pi->gradient.grad_m1_bar_gradhterm[i] += dx[i] * wi_dx_gradhterm * volume_j;
+    pj->gradient.grad_m1_bar_gradhterm[i] +=
+        -dx[i] * wj_dx_gradhterm * volume_i;
+
+    for (int j = 0; j < 3; j++) {
+      pi->gradient.grad_m1_bar[i][j] += dx[j] * wi_dx_term[i] * volume_j;
+      pj->gradient.grad_m1_bar[i][j] += -dx[j] * wj_dx_term[i] * volume_i;
+    }
+  }
+
+  for (int j = 0; j < 3; j++) {
+    for (int k = j; k < 3; k++) {
+      int i = (j == k) ? j : 2 + j + k;
+      pi->gradient.m2_bar.elements[i] += dx[j] * dx[k] * wi_term * volume_j;
+      pj->gradient.m2_bar.elements[i] += dx[j] * dx[k] * wj_term * volume_i;
+
+      pi->gradient.grad_m2_bar_gradhterm.elements[i] +=
+          dx[j] * dx[k] * wi_dx_gradhterm * volume_j;
+      pj->gradient.grad_m2_bar_gradhterm.elements[i] +=
+          dx[j] * dx[k] * wj_dx_gradhterm * volume_i;
+
+      for (int l = 0; l < 3; l++) {
+        pi->gradient.grad_m2_bar[l].elements[i] +=
+            dx[j] * dx[k] * wi_dx_term[l] * volume_j;
+        pj->gradient.grad_m2_bar[l].elements[i] +=
+            dx[j] * dx[k] * wj_dx_term[l] * volume_i;
+      }
+    }
+  }
+}
+
+/**
+ * @brief Extra kernel gradient interaction between two particles
+ * (non-symmetric)
+ *
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param wi The value of the unmodified kernel function W(r, hi) * hi^d.
+ * @param wj The value of the unmodified kernel function W(r, hj) * hj^d.
+ * @param wi_dx The norm of the gradient of wi: dW(r, hi)/dr * hi^(d+1).
+ * @param wj_dx The norm of the gradient of wj: dW(r, hj)/dr * hj^(d+1).
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_runner_iact_nonsym_gradient_extra_kernel(
+    struct part *restrict pi, struct part *restrict pj, const float dx[3],
+    const float wi, const float wj, const float wi_dx, const float wj_dx) {
+
+  /* Get r and 1/r. */
+  const float r = sqrtf(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  const float r_inv = r ? 1.0f / r : 0.0f;
+
+  const float hi = pi->h;
+  const float hi_inv = 1.0f / hi;                        /* 1/h */
+  const float hi_inv_dim = pow_dimension(hi_inv);        /* 1/h^d */
+  const float hi_inv_dim_plus_one = hi_inv_dim * hi_inv; /* 1/h^(d+1) */
+
+  const float hj = pj->h;
+  const float hj_inv = 1.0f / hj;                        /* 1/h */
+  const float hj_inv_dim = pow_dimension(hj_inv);        /* 1/h^d */
+  const float hj_inv_dim_plus_one = hj_inv_dim * hj_inv; /* 1/h^(d+1) */
+
+  /* Volume elements */
+  const float volume_j = pj->mass / pj->rho_evol;
+
+  /* Mean ij kernel and gradients */
+  const float wi_term = 0.5f * (wi * hi_inv_dim + wj * hj_inv_dim);
+  float wi_dx_term[3];
+  const float mean_dw_dr =
+      0.5f * (wi_dx * hi_inv_dim_plus_one + wj_dx * hj_inv_dim_plus_one);
+  wi_dx_term[0] = dx[0] * r_inv * mean_dw_dr;
+  wi_dx_term[1] = dx[1] * r_inv * mean_dw_dr;
+  wi_dx_term[2] = dx[2] * r_inv * mean_dw_dr;
+
+  /* Grad-h term, dW/dh */
+  const float wi_dx_gradhterm = -0.5f *
+                                (hydro_dimension * wi + (r * hi_inv) * wi_dx) *
+                                hi_inv_dim_plus_one;
+
+  /* Geometric moments m_0, m_1, and m_2 (Sandnes+2025 Eqns. 24--26), their
+   * gradients (Sandnes+2025 Eqns. B.10--B.12, initially we only construct the
+   * first terms in Eqns. B.11 and B.12) and grad-h terms (from second term
+   * in Eqn. 29 when used in Eqns. B.10--B.12)*/
+  pi->gradient.m0_bar += wi_term * volume_j;
+
+  pi->gradient.grad_m0_bar_gradhterm += wi_dx_gradhterm * volume_j;
+  for (int i = 0; i < 3; i++) {
+    pi->gradient.m1_bar[i] += dx[i] * wi_term * volume_j;
+
+    pi->gradient.grad_m0_bar[i] += wi_dx_term[i] * volume_j;
+
+    pi->gradient.grad_m1_bar_gradhterm[i] += dx[i] * wi_dx_gradhterm * volume_j;
+
+    for (int j = 0; j < 3; j++) {
+      pi->gradient.grad_m1_bar[i][j] += dx[j] * wi_dx_term[i] * volume_j;
+    }
+  }
+
+  for (int j = 0; j < 3; j++) {
+    for (int k = j; k < 3; k++) {
+      int i = (j == k) ? j : 2 + j + k;
+      pi->gradient.m2_bar.elements[i] += dx[j] * dx[k] * wi_term * volume_j;
+
+      pi->gradient.grad_m2_bar_gradhterm.elements[i] +=
+          dx[j] * dx[k] * wi_dx_gradhterm * volume_j;
+
+      for (int l = 0; l < 3; l++) {
+        pi->gradient.grad_m2_bar[l].elements[i] +=
+            dx[j] * dx[k] * wi_dx_term[l] * volume_j;
+      }
+    }
+  }
+}
+
+/**
+ * @brief Finishes extra kernel parts of the gradient calculation.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_end_gradient_extra_kernel(struct part *restrict p) {
+
+  const float h = p->h;
+  const float h_inv = 1.0f / h;                       /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv);       /* 1/h^d */
+  const float h_inv_dim_plus_one = h_inv_dim * h_inv; /* 1/h^(d+1) */
+
+  /* Volume elements */
+  const float volume = p->mass / p->rho_evol;
+
+  /* Self contribution to geometric moments and gradients */
+  p->gradient.m0_bar += volume * kernel_root * h_inv_dim;
+  p->gradient.grad_m0_bar_gradhterm -=
+      0.5f * volume * hydro_dimension * kernel_root * h_inv_dim_plus_one;
+
+  /* Multiply dh/dr (Sandnes+2025 Eqn. 31) into grad-h terms (See second term
+   * in Sandnes+2025 Eqn. 29) */
+  for (int i = 0; i < 3; i++) {
+    p->gradient.grad_m0_bar[i] +=
+        p->gradient.grad_m0_bar_gradhterm * p->dh_norm_kernel[i];
+    for (int j = 0; j < 3; j++) {
+      p->gradient.grad_m1_bar[i][j] +=
+          p->gradient.grad_m1_bar_gradhterm[j] * p->dh_norm_kernel[i];
+    }
+    for (int k = 0; k < 6; k++) {
+      p->gradient.grad_m2_bar[i].elements[k] +=
+          p->gradient.grad_m2_bar_gradhterm.elements[k] * p->dh_norm_kernel[i];
+    }
+  }
+}
+
+/**
+ * @brief Prepare a particle for the force calculation.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_prepare_force_extra_kernel(struct part *restrict p) {
+
+  if (p->is_h_max) {
+    /* Use standard kernal gradients if h=h_max */
+    /* Linear-order reproducing kernel parameters are set to "defaults" */
+    p->force.A = 1.f;
+    p->force.vac_switch = 1.f;
+    memset(p->force.B, 0.f, 3 * sizeof(float));
+    memset(p->force.grad_A, 0.f, 3 * sizeof(float));
+    memset(p->force.grad_B, 0.f, 3 * 3 * sizeof(float));
+
+    return;
+  }
+
+  /* Add second terms in Sandnes+2025 Eqns. B.11 and B.12 to complete the
+   * geometric moment gradients */
+  for (int i = 0; i < 3; i++) {
+    p->gradient.grad_m1_bar[i][i] += p->gradient.m0_bar;
+  }
+
+  p->gradient.grad_m2_bar[0].xx += 2.f * p->gradient.m1_bar[0];
+  p->gradient.grad_m2_bar[0].xy += p->gradient.m1_bar[1];
+  p->gradient.grad_m2_bar[0].xz += p->gradient.m1_bar[2];
+
+  p->gradient.grad_m2_bar[1].yy += 2.f * p->gradient.m1_bar[1];
+  p->gradient.grad_m2_bar[1].xy += p->gradient.m1_bar[0];
+  p->gradient.grad_m2_bar[1].yz += p->gradient.m1_bar[2];
+
+  p->gradient.grad_m2_bar[2].zz += 2.f * p->gradient.m1_bar[2];
+  p->gradient.grad_m2_bar[2].xz += p->gradient.m1_bar[0];
+  p->gradient.grad_m2_bar[2].yz += p->gradient.m1_bar[1];
+
+  /* Inverse of symmetric geometric moment m_2 (bar) */
+  struct sym_matrix m2_bar_inv;
+  /* Make m2_bar dimensionless for calculation of inverse */
+  struct sym_matrix m2_bar_over_h2;
+  for (int i = 0; i < 6; i++) {
+    m2_bar_over_h2.elements[i] = p->gradient.m2_bar.elements[i] / (p->h * p->h);
+  }
+  sym_matrix_invert(&m2_bar_inv, &m2_bar_over_h2);
+  for (int i = 0; i < 6; i++) {
+    m2_bar_inv.elements[i] /= (p->h * p->h);
+  }
+
+  /* Components for constructing linear-order kernel's A and B (Sandnes+2025
+   * Eqns. 22 and 23), and gradients (Eqns. B.8 and B.9) that are calculated
+   * with sym_matrix functions from combinations of geometric moments and
+   * their gradients */
+  float m2_bar_inv_mult_m1_bar[3];
+  float m2_bar_inv_mult_grad_m1_bar[3][3];
+  struct sym_matrix m2_bar_inv_mult_grad_m2_bar_mult_m2_bar_inv[3];
+  float ABA_mult_m1_bar[3][3];
+
+  sym_matrix_multiply_by_vector(m2_bar_inv_mult_m1_bar, &m2_bar_inv,
+                                p->gradient.m1_bar);
+  for (int i = 0; i < 3; i++) {
+    sym_matrix_multiply_by_vector(m2_bar_inv_mult_grad_m1_bar[i], &m2_bar_inv,
+                                  p->gradient.grad_m1_bar[i]);
+    sym_matrix_multiplication_ABA(
+        &m2_bar_inv_mult_grad_m2_bar_mult_m2_bar_inv[i], &m2_bar_inv,
+        &p->gradient.grad_m2_bar[i]);
+    sym_matrix_multiply_by_vector(
+        ABA_mult_m1_bar[i], &m2_bar_inv_mult_grad_m2_bar_mult_m2_bar_inv[i],
+        p->gradient.m1_bar);
+  }
+
+  /* Linear-order reproducing kernel's A and B components (Sandnes+2025
+   * Eqns. 22 and 23) and gradients (Eqns. B.8 and B.9) */
+  float A, B[3], grad_A[3], grad_B[3][3];
+
+  /* Calculate A (Sandnes+2025 Eqn. 22) */
+  A = p->gradient.m0_bar;
+  for (int i = 0; i < 3; i++) {
+    A -= m2_bar_inv_mult_m1_bar[i] * p->gradient.m1_bar[i];
+  }
+  A = 1.f / A;
+
+  /* Calculate B (Sandnes+2025 Eqn. 23) */
+  for (int i = 0; i < 3; i++) {
+    B[i] = -m2_bar_inv_mult_m1_bar[i];
+  }
+
+  /* Calculate grad A (Sandnes+2025 Eqn. B.8) */
+  for (int i = 0; i < 3; i++) {
+    grad_A[i] = p->gradient.grad_m0_bar[i];
+
+    for (int j = 0; j < 3; j++) {
+      grad_A[i] +=
+          -2 * m2_bar_inv_mult_m1_bar[j] * p->gradient.grad_m1_bar[i][j] +
+          ABA_mult_m1_bar[i][j] * p->gradient.m1_bar[j];
+    }
+
+    grad_A[i] *= -A * A;
+  }
+
+  /* Calculate grad B (Sandnes+2025 Eqn. B.9) */
+  for (int i = 0; i < 3; i++) {
+    for (int j = 0; j < 3; j++) {
+      grad_B[j][i] = -m2_bar_inv_mult_grad_m1_bar[j][i] + ABA_mult_m1_bar[j][i];
+    }
+  }
+
+  /* Store final values */
+  p->force.A = A;
+  memcpy(p->force.B, B, 3 * sizeof(float));
+  memcpy(p->force.grad_A, grad_A, 3 * sizeof(float));
+  memcpy(p->force.grad_B, grad_B, 3 * 3 * sizeof(float));
+
+  /* Vacuum-boundary proximity switch (Sandnes+2025 Eqn. 33) */
+  p->force.vac_switch = 1.f;
+  const float hB = p->h * sqrtf(B[0] * B[0] + B[1] * B[1] + B[2] * B[2]);
+  const float offset = 0.8f;
+
+  if (hB > offset) {
+    const float sigma = 0.2f;
+    p->force.vac_switch =
+        expf(-(hB - offset) * (hB - offset) / (2.f * sigma * sigma));
+  }
+}
+
+/**
+ * @brief Set gradient terms for linear-order reproducing kernel.
+ *
+ * Note `G` here corresponds to `d/dr tilde{mathcal{W}}` in Sandnes et
+ * al.(2025). These are used in the REMIX equations of motion (Sandnes+2025
+ * Eqns. 14--16).
+ *
+ * @param Gi (return) Gradient of linear-order reproducing kernel for first
+ * particle.
+ * @param Gj (return) Gradient of linear-order reproducing kernel for second
+ * particle.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param wi The value of the unmodified kernel function W(r, hi) * hi^d.
+ * @param wj The value of the unmodified kernel function W(r, hj) * hj^d.
+ * @param wi_dx The norm of the gradient of wi: dW(r, hi)/dr * hi^(d+1).
+ * @param wj_dx The norm of the gradient of wj: dW(r, hj)/dr * hj^(d+1).
+ */
+__attribute__((always_inline)) INLINE static void hydro_set_Gi_Gj_forceloop(
+    float Gi[3], float Gj[3], const struct part *restrict pi,
+    const struct part *restrict pj, const float dx[3], const float wi,
+    const float wj, const float wi_dx, const float wj_dx) {
+
+  /* Get r and 1/r. */
+  const float r = sqrtf(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  const float r_inv = r ? 1.0f / r : 0.0f;
+
+  const float hi = pi->h;
+  const float hi_inv = 1.0f / hi;                        /* 1/h */
+  const float hi_inv_dim = pow_dimension(hi_inv);        /* 1/h^d */
+  const float hi_inv_dim_plus_one = hi_inv_dim * hi_inv; /* 1/h^(d+1) */
+
+  const float hj = pj->h;
+  const float hj_inv = 1.0f / hj;                        /* 1/h */
+  const float hj_inv_dim = pow_dimension(hj_inv);        /* 1/h^d */
+  const float hj_inv_dim_plus_one = hj_inv_dim * hj_inv; /* 1/h^(d+1) */
+
+  const float wi_dr = hi_inv_dim_plus_one * wi_dx;
+  const float wj_dr = hj_inv_dim_plus_one * wj_dx;
+
+  if ((pi->is_h_max) || (pj->is_h_max)) {
+    /* If one or both particles have h=h_max, revert to standard kernel grads,
+     * without grad-h terms */
+    Gi[0] = wi_dr * dx[0] * r_inv;
+    Gi[1] = wi_dr * dx[1] * r_inv;
+    Gi[2] = wi_dr * dx[2] * r_inv;
+
+    Gj[0] = -wj_dr * dx[0] * r_inv;
+    Gj[1] = -wj_dr * dx[1] * r_inv;
+    Gj[2] = -wj_dr * dx[2] * r_inv;
+
+    return;
+  }
+
+  /* Mean ij kernels and gradients with grad-h terms */
+  const float wi_term = 0.5f * (wi * hi_inv_dim + wj * hj_inv_dim);
+  const float wj_term = wi_term;
+  float wi_dx_term[3], wj_dx_term[3];
+
+  /* Get linear-order reproducing kernel's A and B components (Sandnes+2025
+   * Eqns. 22 and 23) and gradients (Eqns. B.8 and B.9) */
+  const float Ai = pi->force.A;
+  const float Aj = pj->force.A;
+  float grad_Ai[3], grad_Aj[3], Bi[3], Bj[3], grad_Bi[3][3], grad_Bj[3][3];
+  memcpy(grad_Ai, pi->force.grad_A, 3 * sizeof(float));
+  memcpy(grad_Aj, pj->force.grad_A, 3 * sizeof(float));
+  memcpy(Bi, pi->force.B, 3 * sizeof(float));
+  memcpy(Bj, pj->force.B, 3 * sizeof(float));
+  memcpy(grad_Bi, pi->force.grad_B, 3 * 3 * sizeof(float));
+  memcpy(grad_Bj, pj->force.grad_B, 3 * 3 * sizeof(float));
+
+  for (int i = 0; i < 3; i++) {
+
+    /* Assemble Sandnes+2025 Eqn. 29 */
+    const float mean_dw_dr =
+        0.5f * (wi_dx * hi_inv_dim_plus_one + wj_dx * hj_inv_dim_plus_one);
+    wi_dx_term[i] = dx[i] * r_inv * mean_dw_dr;
+    wj_dx_term[i] = -wi_dx_term[i];
+
+    wi_dx_term[i] += -0.5f * (hydro_dimension * wi + (r * hi_inv) * wi_dx) *
+                     hi_inv_dim_plus_one * pi->dh_norm_kernel[i];
+    wj_dx_term[i] += -0.5f * (hydro_dimension * wj + (r * hj_inv) * wj_dx) *
+                     hj_inv_dim_plus_one * pj->dh_norm_kernel[i];
+
+    /* Assemble Sandnes+2025 Eqn. 28 */
+    Gi[i] = Ai * wi_dx_term[i] + grad_Ai[i] * wi_term + Ai * Bi[i] * wi_term;
+    Gj[i] = Aj * wj_dx_term[i] + grad_Aj[i] * wj_term + Aj * Bj[i] * wj_term;
+
+    for (int j = 0; j < 3; j++) {
+      Gi[i] += Ai * Bi[j] * dx[j] * wi_dx_term[i] +
+               grad_Ai[i] * Bi[j] * dx[j] * wi_term +
+               Ai * grad_Bi[i][j] * dx[j] * wi_term;
+
+      Gj[i] += -(Aj * Bj[j] * dx[j] * wj_dx_term[i] +
+                 grad_Aj[i] * Bj[j] * dx[j] * wj_term +
+                 Aj * grad_Bj[i][j] * dx[j] * wj_term);
+    }
+  }
+
+  /* Standard-kernel gradients, to be used for vacuum boundary switch (For
+   * second term in Sandnes+2025 Eqn. 32)*/
+  float wi_dx_term_vac[3], wj_dx_term_vac[3];
+  for (int i = 0; i < 3; i++) {
+    wi_dx_term_vac[i] = dx[i] * r_inv * wi_dx * hi_inv_dim_plus_one;
+    wj_dx_term_vac[i] = -dx[i] * r_inv * wj_dx * hj_inv_dim_plus_one;
+
+    wi_dx_term_vac[i] += -(hydro_dimension * wi + (r * hi_inv) * wi_dx) *
+                         hi_inv_dim_plus_one * pi->dh_norm_kernel[i];
+    wj_dx_term_vac[i] += -(hydro_dimension * wj + (r * hj_inv) * wj_dx) *
+                         hj_inv_dim_plus_one * pj->dh_norm_kernel[i];
+  }
+
+  /* Gradients, including vacuum boundary switch (Sandnes+2025 Eqn. 32) */
+  for (int i = 0; i < 3; i++) {
+    Gi[i] = pi->force.vac_switch * Gi[i] +
+            (1.f - pi->force.vac_switch) * wi_dx_term_vac[i];
+    Gj[i] = pj->force.vac_switch * Gj[i] +
+            (1.f - pj->force.vac_switch) * wj_dx_term_vac[i];
+  }
+}
+
+#endif /* SWIFT_REMIX_HYDRO_KERNELS_H */
diff --git a/src/hydro/REMIX/hydro_parameters.h b/src/hydro/REMIX/hydro_parameters.h
new file mode 100644
index 0000000000000000000000000000000000000000..e33568a31b92c2543e9d60860b9d565746ba720d
--- /dev/null
+++ b/src/hydro/REMIX/hydro_parameters.h
@@ -0,0 +1,186 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *               2019 Josh Borrow (joshua.borrow@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_PARAMETERS_H
+#define SWIFT_REMIX_HYDRO_PARAMETERS_H
+
+/* Configuration file */
+#include "config.h"
+
+/* Global headers */
+#if defined(HAVE_HDF5)
+#include <hdf5.h>
+#endif
+
+/* Local headers */
+#include "common_io.h"
+#include "error.h"
+#include "inline.h"
+
+/**
+ * @file REMIX/hydro_parameters.h
+ * @brief REMIX implementation of SPH (Sandnes et al. 2025) (default parameters)
+ *
+ * This file defines a number of things that are used in
+ * hydro_properties.c as defaults for run-time parameters
+ * as well as a number of compile-time parameters.
+ */
+
+/* Viscosity paramaters -- Defaults; can be changed at run-time */
+
+/*! Default REMIX artificial viscosity parameters */
+#define const_remix_visc_alpha 1.5f
+#define const_remix_visc_beta 3.f
+#define const_remix_visc_epsilon 0.1f
+#define const_remix_visc_a 2.0f / 3.0f
+#define const_remix_visc_b 1.0f / 3.0f
+#define const_remix_difn_a_u 0.05f
+#define const_remix_difn_b_u 0.95f
+#define const_remix_difn_a_rho 0.05f
+#define const_remix_difn_b_rho 0.95f
+#define const_remix_norm_alpha 1.0f
+#define const_remix_slope_limiter_exp_denom 0.04f
+
+/* The viscosity that the particles are reset to after being hit by a
+ * feedback event. This should be set to the same value as the
+ * const_viscosity_alpha in fixed schemes, and likely
+ * to const_viscosity_alpha_max in variable schemes. */
+#define hydro_props_default_viscosity_alpha_feedback_reset 1.5f
+
+/* Structs that store the relevant variables */
+
+/*! Artificial viscosity parameters */
+struct viscosity_global_data {};
+
+/*! Artificial diffusion parameters */
+struct diffusion_global_data {};
+
+/* Functions for reading from parameter file */
+
+/* Forward declartions */
+struct swift_params;
+struct phys_const;
+struct unit_system;
+
+/* Viscosity */
+
+/**
+ * @brief Initialises the viscosity parameters in the struct from
+ *        the parameter file, or sets them to defaults.
+ *
+ * @param params: the pointer to the swift_params file
+ * @param unit_system: pointer to the unit system
+ * @param phys_const: pointer to the physical constants system
+ * @param viscosity: pointer to the viscosity_global_data struct to be filled.
+ **/
+static INLINE void viscosity_init(struct swift_params* params,
+                                  const struct unit_system* us,
+                                  const struct phys_const* phys_const,
+                                  struct viscosity_global_data* viscosity) {}
+
+/**
+ * @brief Initialises a viscosity struct to sensible numbers for mocking
+ *        purposes.
+ *
+ * @param viscosity: pointer to the viscosity_global_data struct to be filled.
+ **/
+static INLINE void viscosity_init_no_hydro(
+    struct viscosity_global_data* viscosity) {}
+
+/**
+ * @brief Prints out the viscosity parameters at the start of a run.
+ *
+ * @param viscosity: pointer to the viscosity_global_data struct found in
+ *                   hydro_properties
+ **/
+static INLINE void viscosity_print(
+    const struct viscosity_global_data* viscosity) {}
+
+#if defined(HAVE_HDF5)
+/**
+ * @brief Prints the viscosity information to the snapshot when writing.
+ *
+ * @param h_grpsph: the SPH group in the ICs to write attributes to.
+ * @param viscosity: pointer to the viscosity_global_data struct.
+ **/
+static INLINE void viscosity_print_snapshot(
+    hid_t h_grpsph, const struct viscosity_global_data* viscosity) {
+
+  io_write_attribute_f(h_grpsph, "Viscosity alpha", const_remix_visc_alpha);
+  io_write_attribute_f(h_grpsph, "Viscosity beta", const_remix_visc_beta);
+  io_write_attribute_f(h_grpsph, "Viscosity epsilon", const_remix_visc_epsilon);
+  io_write_attribute_f(h_grpsph, "Viscosity a_visc", const_remix_visc_a);
+  io_write_attribute_f(h_grpsph, "Viscosity b_visc", const_remix_visc_b);
+}
+#endif
+
+/* Diffusion */
+
+/**
+ * @brief Initialises the diffusion parameters in the struct from
+ *        the parameter file, or sets them to defaults.
+ *
+ * @param params: the pointer to the swift_params file
+ * @param unit_system: pointer to the unit system
+ * @param phys_const: pointer to the physical constants system
+ * @param diffusion_global_data: pointer to the diffusion struct to be filled.
+ **/
+static INLINE void diffusion_init(struct swift_params* params,
+                                  const struct unit_system* us,
+                                  const struct phys_const* phys_const,
+                                  struct diffusion_global_data* diffusion) {}
+
+/**
+ * @brief Initialises a diffusion struct to sensible numbers for mocking
+ *        purposes.
+ *
+ * @param diffusion: pointer to the diffusion_global_data struct to be filled.
+ **/
+static INLINE void diffusion_init_no_hydro(
+    struct diffusion_global_data* diffusion) {}
+
+/**
+ * @brief Prints out the diffusion parameters at the start of a run.
+ *
+ * @param diffusion: pointer to the diffusion_global_data struct found in
+ *                   hydro_properties
+ **/
+static INLINE void diffusion_print(
+    const struct diffusion_global_data* diffusion) {}
+
+#ifdef HAVE_HDF5
+/**
+ * @brief Prints the diffusion information to the snapshot when writing.
+ *
+ * @param h_grpsph: the SPH group in the ICs to write attributes to.
+ * @param diffusion: pointer to the diffusion_global_data struct.
+ **/
+static INLINE void diffusion_print_snapshot(
+    hid_t h_grpsph, const struct diffusion_global_data* diffusion) {
+  io_write_attribute_f(h_grpsph, "Diffusion a_difn_u", const_remix_difn_a_u);
+  io_write_attribute_f(h_grpsph, "Diffusion b_difn_u", const_remix_difn_b_u);
+  io_write_attribute_f(h_grpsph, "Diffusion a_difn_rho",
+                       const_remix_difn_a_rho);
+  io_write_attribute_f(h_grpsph, "Diffusion b_difn_rho",
+                       const_remix_difn_b_rho);
+}
+#endif
+
+#endif /* SWIFT_REMIX_HYDRO_PARAMETERS_H */
diff --git a/src/hydro/REMIX/hydro_part.h b/src/hydro/REMIX/hydro_part.h
new file mode 100644
index 0000000000000000000000000000000000000000..e0a642ad0dbd6bdab7670c60bb85f1f143d53d10
--- /dev/null
+++ b/src/hydro/REMIX/hydro_part.h
@@ -0,0 +1,313 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *               2016 Matthieu Schaller (matthieu.schaller@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_PART_H
+#define SWIFT_REMIX_HYDRO_PART_H
+
+/**
+ * @file REMIX/hydro_part.h
+ * @brief REMIX implementation of SPH (Sandnes et al. 2025)
+ */
+
+#include "black_holes_struct.h"
+#include "chemistry_struct.h"
+#include "cooling_struct.h"
+#include "equation_of_state.h" /* For enum material_id */
+#include "feedback_struct.h"
+#include "mhd_struct.h"
+#include "particle_splitting_struct.h"
+#include "rt_struct.h"
+#include "sink_struct.h"
+#include "star_formation_struct.h"
+#include "symmetric_matrix.h"
+#include "timestep_limiter_struct.h"
+#include "tracers_struct.h"
+
+/**
+ * @brief Particle fields not needed during the SPH loops over neighbours.
+ *
+ * This structure contains the particle fields that are not used in the
+ * density or force loops. Quantities should be used in the kick, drift and
+ * potentially ghost tasks only.
+ */
+struct xpart {
+
+  /*! Offset between current position and position at last tree rebuild. */
+  float x_diff[3];
+
+  /*! Offset between the current position and position at the last sort. */
+  float x_diff_sort[3];
+
+  /*! Velocity at the last full step. */
+  float v_full[3];
+
+  /*! Gravitational acceleration at the end of the last step */
+  float a_grav[3];
+
+  /*! Internal energy at the last full step. */
+  float u_full;
+
+  /*! Evolved density at the last full step. */
+  float rho_evol_full;
+
+  /*! Additional data used to record particle splits */
+  struct particle_splitting_data split_data;
+
+  /*! Additional data used to record cooling information */
+  struct cooling_xpart_data cooling_data;
+
+  /*! Additional data used by the tracers */
+  struct tracers_xpart_data tracers_data;
+
+  /*! Additional data used by the star formation */
+  struct star_formation_xpart_data sf_data;
+
+  /*! Additional data used by the feedback */
+  struct feedback_part_data feedback_data;
+
+  /*! Additional data used by the MHD scheme */
+  struct mhd_xpart_data mhd_data;
+
+} SWIFT_STRUCT_ALIGN;
+
+/**
+ * @brief Particle fields for the SPH particles
+ *
+ * The density and force substructures are used to contain variables only used
+ * within the density and force loops over neighbours. All more permanent
+ * variables should be declared in the main part of the part structure,
+ */
+struct part {
+
+  /*! Particle unique ID. */
+  long long id;
+
+  /*! Pointer to corresponding gravity part. */
+  struct gpart* gpart;
+
+  /*! Particle position. */
+  double x[3];
+
+  /*! Particle predicted velocity. */
+  float v[3];
+
+  /*! Particle acceleration. */
+  float a_hydro[3];
+
+  /*! Particle mass. */
+  float mass;
+
+  /*! Particle smoothing length. */
+  float h;
+
+  /*! Particle internal energy. */
+  float u;
+
+  /*! Time derivative of the internal energy. */
+  float u_dt;
+
+  /*! Particle density (standard SPH estimate). */
+  float rho;
+
+  /*! Particle evolved density (primary density for REMIX equations). */
+  float rho_evol;
+
+  /*! Time derivative of the evolved density. */
+  float drho_dt;
+
+  /*! Gradient of velocity, calculated using normalised kernel. */
+  float dv_norm_kernel[3][3];
+
+  /*! Gradient of internal energy, calculated using normalised kernel. */
+  float du_norm_kernel[3];
+
+  /*! Gradient of density, calculated using normalised kernel. */
+  float drho_norm_kernel[3];
+
+  /*! Gradient of smoothing length, calculated using normalised kernel. */
+  float dh_norm_kernel[3];
+
+  /*! Geometric moment m_0. */
+  float m0;
+
+  /*! Gradient of geometric moment m_0. */
+  float grad_m0[3];
+
+  /* Store density/force specific stuff. */
+  union {
+
+    /**
+     * @brief Structure for the variables only used in the density loop over
+     * neighbours.
+     *
+     * Quantities in this sub-structure should only be accessed in the density
+     * loop over neighbours and the ghost task.
+     */
+    struct {
+
+      /*! Neighbour number count. */
+      float wcount;
+
+      /*! Derivative of the neighbour number with respect to h. */
+      float wcount_dh;
+
+      /*! Derivative of density with respect to h */
+      float rho_dh;
+
+    } density;
+
+    /**
+     * @brief Structure for the variables only used in the gradient loop over
+     * neighbours.
+     *
+     * Quantities should only be accessed in the gradient loop over neighbours
+     * and the extra ghost task.
+     */
+    struct {
+
+      /*! Symmetrised kernel geometric moment m0. */
+      float m0_bar;
+
+      /*! Gradient of symmetrised kernel geometric moment m0. */
+      float grad_m0_bar[3];
+
+      /*! Contributing term for grad-h component of grad_m0_bar. */
+      float grad_m0_bar_gradhterm;
+
+      /*! Symmetrised kernel geometric moment m1. */
+      float m1_bar[3];
+
+      /*! Gradient of symmetrised kernel geometric moment m1. */
+      float grad_m1_bar[3][3];
+
+      /*! Contributing term for grad-h component of grad_m1_bar. */
+      float grad_m1_bar_gradhterm[3];
+
+      /*! Symmetrised kernel geometric moment m2. */
+      struct sym_matrix m2_bar;
+
+      /*! Gradient of symmetrised kernel geometric moment m2 (x, y, z
+       * components). */
+      struct sym_matrix grad_m2_bar[3];
+
+      /*! Contributing term for grad-h component of grad_m2_bar. */
+      struct sym_matrix grad_m2_bar_gradhterm;
+
+    } gradient;
+
+    /**
+     * @brief Structure for the variables only used in the force loop over
+     * neighbours.
+     *
+     * Quantities in this sub-structure should only be accessed in the force
+     * loop over neighbours and the ghost, drift and kick tasks.
+     */
+    struct {
+
+      /*! Particle pressure. */
+      float pressure;
+
+      /*! Particle soundspeed. */
+      float soundspeed;
+
+      /*! Particle signal velocity */
+      float v_sig;
+
+      /*! Time derivative of smoothing length  */
+      float h_dt;
+
+      /*! Balsara switch */
+      float balsara;
+
+      /*! Linear-order reproducing kernel coefficient */
+      float A;
+
+      /*! Linear-order reproducing kernel coefficient */
+      float B[3];
+
+      /*! Gradient of linear-order reproducing kernel coefficient */
+      float grad_A[3];
+
+      /*! Gradient of linear-order reproducing kernel coefficient */
+      float grad_B[3][3];
+
+      /*! Variable switch to identify proximity to vacuum boundaries. */
+      float vac_switch;
+
+      /*! Constant needed for slope limiter: 1 / eta_neighbours. */
+      float eta_crit;
+
+    } force;
+  };
+
+  /*! Additional data used by the MHD scheme */
+  struct mhd_part_data mhd_data;
+
+  /*! Chemistry information */
+  struct chemistry_part_data chemistry_data;
+
+  /*! Cooling information */
+  struct cooling_part_data cooling_data;
+
+  /*! Black holes information (e.g. swallowing ID) */
+  struct black_holes_part_data black_holes_data;
+
+  /*! Sink information (e.g. swallowing ID) */
+  struct sink_part_data sink_data;
+
+  /*! Material identifier flag */
+  enum eos_planetary_material_id mat_id;
+
+  /*! Additional Radiative Transfer Data */
+  struct rt_part_data rt_data;
+
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
+  /*! Time-step length */
+  timebin_t time_bin;
+
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
+  /* Whether or not the particle has h=h_max ('1' or '0') */
+  char is_h_max;
+
+  /*! Time-step limiter information */
+  struct timestep_limiter_data limiter_data;
+
+#ifdef SWIFT_DEBUG_CHECKS
+
+  /* Time of the last drift */
+  integertime_t ti_drift;
+
+  /* Time of the last kick */
+  integertime_t ti_kick;
+
+#endif
+
+#ifdef PLANETARY_FIXED_ENTROPY
+  /* Fixed specific entropy */
+  float s_fixed;
+#endif
+
+} SWIFT_STRUCT_ALIGN;
+
+#endif /* SWIFT_REMIX_HYDRO_PART_H */
diff --git a/src/hydro/REMIX/hydro_visc_difn.h b/src/hydro/REMIX/hydro_visc_difn.h
new file mode 100644
index 0000000000000000000000000000000000000000..8f387c3c4d768e4c30e0f440179a97f02c190efe
--- /dev/null
+++ b/src/hydro/REMIX/hydro_visc_difn.h
@@ -0,0 +1,452 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Thomas Sandnes (thomas.d.sandnes@durham.ac.uk)
+ *               2024 Jacob Kegerreis (jacob.kegerreis@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_REMIX_HYDRO_VISC_DIFN_H
+#define SWIFT_REMIX_HYDRO_VISC_DIFN_H
+
+/**
+ * @file REMIX/hydro_visc_difn.h
+ * @brief Utilities for REMIX artificial viscosity and diffusion calculations.
+ */
+
+#include "const.h"
+#include "hydro_parameters.h"
+#include "math.h"
+
+/**
+ * @brief Prepares extra artificial viscosity and artificial diffusion
+ * parameters for a particle for the gradient calculation.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_prepare_gradient_extra_visc_difn(struct part *restrict p) {
+
+  memset(p->du_norm_kernel, 0.f, 3 * sizeof(float));
+  memset(p->drho_norm_kernel, 0.f, 3 * sizeof(float));
+  memset(p->dh_norm_kernel, 0.f, 3 * sizeof(float));
+  memset(p->dv_norm_kernel, 0.f, 3 * 3 * sizeof(float));
+}
+
+/**
+ * @brief Extra artificial viscosity and artificial diffusion gradient
+ * interaction between two particles
+ *
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param wi The value of the unmodified kernel function W(r, hi) * hi^d.
+ * @param wj The value of the unmodified kernel function W(r, hj) * hj^d.
+ * @param wi_dx The norm of the gradient of wi: dW(r, hi)/dr * hi^(d+1).
+ * @param wj_dx The norm of the gradient of wj: dW(r, hj)/dr * hj^(d+1).
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_runner_iact_gradient_extra_visc_difn(struct part *restrict pi,
+                                           struct part *restrict pj,
+                                           const float dx[3], const float wi,
+                                           const float wj, const float wi_dx,
+                                           const float wj_dx) {
+
+  /* Get r and 1/r. */
+  const float r = sqrtf(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  const float r_inv = r ? 1.0f / r : 0.0f;
+
+  const float hi = pi->h;
+  const float hi_inv = 1.0f / hi;                        /* 1/h */
+  const float hi_inv_dim = pow_dimension(hi_inv);        /* 1/h^d */
+  const float hi_inv_dim_plus_one = hi_inv_dim * hi_inv; /* 1/h^(d+1) */
+
+  const float hj = pj->h;
+  const float hj_inv = 1.0f / hj;                        /* 1/h */
+  const float hj_inv_dim = pow_dimension(hj_inv);        /* 1/h^d */
+  const float hj_inv_dim_plus_one = hj_inv_dim * hj_inv; /* 1/h^(d+1) */
+
+  const float volume_i = pi->mass / pi->rho_evol;
+  const float volume_j = pj->mass / pj->rho_evol;
+
+  /* Gradients of normalised kernel (Sandnes+2025 Eqn. 30) */
+  float wi_dx_term[3], wj_dx_term[3];
+  for (int i = 0; i < 3; i++) {
+    wi_dx_term[i] = dx[i] * r_inv * wi_dx * hi_inv_dim_plus_one;
+    wj_dx_term[i] = -dx[i] * r_inv * wj_dx * hj_inv_dim_plus_one;
+
+    wi_dx_term[i] *= (1.f / pi->m0);
+    wj_dx_term[i] *= (1.f / pj->m0);
+
+    wi_dx_term[i] += -wi * hi_inv_dim * pi->grad_m0[i] / (pi->m0 * pi->m0);
+    wj_dx_term[i] += -wj * hj_inv_dim * pj->grad_m0[i] / (pj->m0 * pj->m0);
+  }
+
+  /* Gradient estimates of h (Sandnes+2025 Eqn. 31), v (Eqn. 35), u (Eqn. 46),
+   * rho (Eqn. 47) using normalised kernel */
+  for (int i = 0; i < 3; i++) {
+    pi->dh_norm_kernel[i] += (pj->h - pi->h) * wi_dx_term[i] * volume_j;
+    pj->dh_norm_kernel[i] += (pi->h - pj->h) * wj_dx_term[i] * volume_i;
+
+    /* Contributions only from same-material particles for u and rho diffusion
+     */
+    if (pi->mat_id == pj->mat_id) {
+      pi->du_norm_kernel[i] += (pj->u - pi->u) * wi_dx_term[i] * volume_j;
+      pj->du_norm_kernel[i] += (pi->u - pj->u) * wj_dx_term[i] * volume_i;
+
+      pi->drho_norm_kernel[i] +=
+          (pj->rho_evol - pi->rho_evol) * wi_dx_term[i] * volume_j;
+      pj->drho_norm_kernel[i] +=
+          (pi->rho_evol - pj->rho_evol) * wj_dx_term[i] * volume_i;
+    }
+
+    for (int j = 0; j < 3; j++) {
+      pi->dv_norm_kernel[i][j] +=
+          (pj->v[j] - pi->v[j]) * wi_dx_term[i] * volume_j;
+      pj->dv_norm_kernel[i][j] +=
+          (pi->v[j] - pj->v[j]) * wj_dx_term[i] * volume_i;
+    }
+  }
+}
+
+/**
+ * @brief Extra artificial viscosity and artificial diffusion gradient
+ * interaction between two particles (non-symmetric)
+ *
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param wi The value of the unmodified kernel function W(r, hi) * hi^d.
+ * @param wi_dx The norm of the gradient of wi: dW(r, hi)/dr * hi^(d+1).
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_runner_iact_nonsym_gradient_extra_visc_difn(
+    struct part *restrict pi, const struct part *restrict pj, const float dx[3],
+    const float wi, const float wi_dx) {
+
+  /* Get r and 1/r. */
+  const float r = sqrtf(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  const float r_inv = r ? 1.0f / r : 0.0f;
+
+  const float hi = pi->h;
+  const float hi_inv = 1.0f / hi;                        /* 1/h */
+  const float hi_inv_dim = pow_dimension(hi_inv);        /* 1/h^d */
+  const float hi_inv_dim_plus_one = hi_inv_dim * hi_inv; /* 1/h^(d+1) */
+
+  const float volume_j = pj->mass / pj->rho_evol;
+
+  /* Gradients of normalised kernel (Sandnes+2025 Eqn. 30) */
+  float wi_dx_term[3];
+  for (int i = 0; i < 3; i++) {
+    wi_dx_term[i] = dx[i] * r_inv * wi_dx * hi_inv_dim_plus_one;
+    wi_dx_term[i] *= (1.f / pi->m0);
+    wi_dx_term[i] += -wi * hi_inv_dim * pi->grad_m0[i] / (pi->m0 * pi->m0);
+  }
+
+  /* Gradient estimates of h (Sandnes+2025 Eqn. 31), v (Eqn. 35), u (Eqn. 46),
+   * rho (Eqn. 47) using normalised kernel */
+  for (int i = 0; i < 3; i++) {
+    pi->dh_norm_kernel[i] += (pj->h - pi->h) * wi_dx_term[i] * volume_j;
+
+    /* Contributions only from same-material particles for u and rho diffusion
+     */
+    if (pi->mat_id == pj->mat_id) {
+      pi->du_norm_kernel[i] += (pj->u - pi->u) * wi_dx_term[i] * volume_j;
+      pi->drho_norm_kernel[i] +=
+          (pj->rho_evol - pi->rho_evol) * wi_dx_term[i] * volume_j;
+    }
+
+    for (int j = 0; j < 3; j++) {
+      pi->dv_norm_kernel[i][j] +=
+          (pj->v[j] - pi->v[j]) * wi_dx_term[i] * volume_j;
+    }
+  }
+}
+
+/**
+ * @brief Finishes extra artificial viscosity and artificial diffusion parts of
+ * the gradient calculation.
+ *
+ * @param p The particle to act upon
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_end_gradient_extra_visc_difn(struct part *restrict p) {}
+
+/**
+ * @brief Returns particle viscous pressures
+ *
+ * @param Qi (return) Viscous pressure for first particle.
+ * @param Qj (return) Viscous pressure for second particle.
+ * @param visc_signal_velocity (return) Signal velocity of artificial viscosity.
+ * @param difn_signal_velocity (return) Signal velocity of artificial diffusion.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ */
+__attribute__((always_inline)) INLINE static void hydro_set_Qi_Qj(
+    float *Qi, float *Qj, float *visc_signal_velocity,
+    float *difn_signal_velocity, const struct part *restrict pi,
+    const struct part *restrict pj, const float dx[3]) {
+
+  const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+  const float r = sqrtf(r2);
+
+  const float hi_inv = 1.0f / pi->h;
+  const float hj_inv = 1.0f / pj->h;
+
+  /* Reconstructed velocities at the halfway point between particles
+   * (Sandnes+2025 Eqn. 36) */
+  float vtilde_i[3], vtilde_j[3];
+
+  /* Viscosity parameters. These are set in hydro_parameters.h  */
+  const float alpha = const_remix_visc_alpha;
+  const float beta = const_remix_visc_beta;
+  const float epsilon = const_remix_visc_epsilon;
+  const float a_visc = const_remix_visc_a;
+  const float b_visc = const_remix_visc_b;
+  const float eta_crit = 0.5f * (pi->force.eta_crit + pj->force.eta_crit);
+  const float slope_limiter_exp_denom = const_remix_slope_limiter_exp_denom;
+
+  if ((pi->is_h_max) || (pj->is_h_max)) {
+    /* Don't reconstruct velocity if either particle has h=h_max */
+    vtilde_i[0] = 0.f;
+    vtilde_i[1] = 0.f;
+    vtilde_i[2] = 0.f;
+
+    vtilde_j[0] = 0.f;
+    vtilde_j[1] = 0.f;
+    vtilde_j[2] = 0.f;
+
+  } else {
+    /* A numerators and denominators (Sandnes+2025 Eqn. 38) */
+    float A_i_v = 0.f;
+    float A_j_v = 0.f;
+
+    /* 1/2 * (r_j - r_i) * dv/dr in second term of Sandnes+2025 Eqn. 38 */
+    float v_reconst_i[3] = {0};
+    float v_reconst_j[3] = {0};
+
+    for (int i = 0; i < 3; i++) {
+      for (int j = 0; j < 3; j++) {
+        /* Get the A numerators and denominators (Sandnes+2025 Eqn. 38).
+         * dv_norm_kernel is from Eqn. 35 */
+        A_i_v += pi->dv_norm_kernel[i][j] * dx[i] * dx[j];
+        A_j_v += pj->dv_norm_kernel[i][j] * dx[i] * dx[j];
+
+        v_reconst_i[j] -= 0.5 * pi->dv_norm_kernel[i][j] * dx[i];
+        v_reconst_j[j] += 0.5 * pj->dv_norm_kernel[i][j] * dx[i];
+      }
+    }
+
+    /* Slope limiter (Sandnes+2025 Eqn. 37) special cases */
+    float phi_i_v, phi_j_v;
+    if ((A_i_v == 0.f) && (A_j_v == 0.f)) {
+      phi_i_v = 1.f;
+      phi_j_v = 1.f;
+
+    } else if ((A_i_v == 0.f && A_j_v != 0.f) ||
+               (A_j_v == 0.f && A_i_v != 0.f) || (A_i_v == -A_j_v)) {
+      phi_i_v = 0.f;
+      phi_j_v = 0.f;
+    } else {
+      /* Slope limiter (Sandnes+2025 Eqn. 37) */
+      phi_i_v = min(1.f, 4.f * A_i_v / A_j_v / (1.f + A_i_v / A_j_v) /
+                             (1.f + A_i_v / A_j_v));
+      phi_i_v = max(0.f, phi_i_v);
+
+      phi_j_v = min(1.f, 4.f * A_j_v / A_i_v / (1.f + A_j_v / A_i_v) /
+                             (1.f + A_j_v / A_i_v));
+      phi_j_v = max(0.f, phi_j_v);
+    }
+
+    /* exp in slope limiter (middle case in Sandnes+2025 Eqn. 37) */
+    const float eta_ab = min(r * hi_inv, r * hj_inv);
+    if (eta_ab < eta_crit) {
+      phi_i_v *= expf(-(eta_ab - eta_crit) * (eta_ab - eta_crit) /
+                      slope_limiter_exp_denom);
+      phi_j_v *= expf(-(eta_ab - eta_crit) * (eta_ab - eta_crit) /
+                      slope_limiter_exp_denom);
+    }
+
+    for (int i = 0; i < 3; i++) {
+      /* Assemble the reconstructed velocity (Sandnes+2025 Eqn. 36) */
+      vtilde_i[i] =
+          pi->v[i] + (1.f - pi->force.balsara) * phi_i_v * v_reconst_i[i];
+      vtilde_j[i] =
+          pj->v[i] + (1.f - pj->force.balsara) * phi_j_v * v_reconst_j[i];
+    }
+  }
+
+  /* Assemble Sandnes+2025 Eqn. 40 */
+  const float mu_i =
+      min(0.f, ((vtilde_i[0] - vtilde_j[0]) * dx[0] +
+                (vtilde_i[1] - vtilde_j[1]) * dx[1] +
+                (vtilde_i[2] - vtilde_j[2]) * dx[2]) *
+                   hi_inv / (r2 * hi_inv * hi_inv + epsilon * epsilon));
+  const float mu_j =
+      min(0.f, ((vtilde_i[0] - vtilde_j[0]) * dx[0] +
+                (vtilde_i[1] - vtilde_j[1]) * dx[1] +
+                (vtilde_i[2] - vtilde_j[2]) * dx[2]) *
+                   hj_inv / (r2 * hj_inv * hj_inv + epsilon * epsilon));
+
+  const float ci = pi->force.soundspeed;
+  const float cj = pj->force.soundspeed;
+
+  /* Finally assemble viscous pressure terms (Sandnes+2025 41) */
+  *Qi = (a_visc + b_visc * pi->force.balsara) * 0.5f * pi->rho *
+        (-alpha * ci * mu_i + beta * mu_i * mu_i);
+  *Qj = (a_visc + b_visc * pj->force.balsara) * 0.5f * pj->rho *
+        (-alpha * cj * mu_j + beta * mu_j * mu_j);
+
+  /* Account for alpha being outside brackets in timestep code */
+  const float viscosity_parameter_factor = (alpha == 0.f) ? 0.f : beta / alpha;
+  *visc_signal_velocity =
+      ci + cj - 2.f * viscosity_parameter_factor * min(mu_i, mu_j);
+
+  /* Signal velocity used for the artificial diffusion (Sandnes+2025 Eqns. 42
+   * and 43) */
+  *difn_signal_velocity =
+      sqrtf((vtilde_i[0] - vtilde_j[0]) * (vtilde_i[0] - vtilde_j[0]) +
+            (vtilde_i[1] - vtilde_j[1]) * (vtilde_i[1] - vtilde_j[1]) +
+            (vtilde_i[2] - vtilde_j[2]) * (vtilde_i[2] - vtilde_j[2]));
+}
+
+/**
+ * @brief Returns midpoint reconstructions of internal energies and densities
+ *
+ * @param utilde_i (return) u reconstructed to midpoint from first particle.
+ * @param utilde_j (return) u reconstructed to midpoint from second particle.
+ * @param rhotilde_i (return) rho reconstructed to midpoint from first particle.
+ * @param rhotilde_j (return) rho reconstructed to midpoint from second
+ * particle.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ */
+__attribute__((always_inline)) INLINE static void hydro_set_u_rho_difn(
+    float *utilde_i, float *utilde_j, float *rhotilde_i, float *rhotilde_j,
+    const struct part *restrict pi, const struct part *restrict pj,
+    const float dx[3]) {
+
+  const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+  const float r = sqrtf(r2);
+
+  const float hi_inv = 1.0f / pi->h;
+  const float hj_inv = 1.0f / pj->h;
+
+  const float eta_crit = 0.5f * (pi->force.eta_crit + pj->force.eta_crit);
+  const float slope_limiter_exp_denom = const_remix_slope_limiter_exp_denom;
+
+  if ((pi->is_h_max) || (pj->is_h_max)) {
+    /* Don't reconstruct internal energy of density if either particle has
+     * h=h_max */
+    *utilde_i = 0.f;
+    *utilde_j = 0.f;
+    *rhotilde_i = 0.f;
+    *rhotilde_j = 0.f;
+
+    return;
+  }
+
+  /* A numerators and denominators (Sandnes+2025 Eqns. 48 and 49) */
+  float A_i_u = 0.f;
+  float A_j_u = 0.f;
+  float A_i_rho = 0.f;
+  float A_j_rho = 0.f;
+
+  /* 1/2 * (r_j - r_i) * du/dr in second term of Sandnes+2025 Eqn. 44 */
+  float u_reconst_i = 0.f;
+  float u_reconst_j = 0.f;
+
+  /* 1/2 * (r_j - r_i) * drho/dr in second term of Sandnes+2025 Eqn. 45 */
+  float rho_reconst_i = 0.f;
+  float rho_reconst_j = 0.f;
+
+  for (int i = 0; i < 3; i++) {
+    /* Get the A numerators and denominators (Sandnes+2025 Eqns. 48 and 49).
+     * du_norm_kernel is from Eqn. 46 and drho_norm_kernel is from Eqn. 47 */
+    A_i_u += pi->du_norm_kernel[i] * dx[i];
+    A_j_u += pj->du_norm_kernel[i] * dx[i];
+    A_i_rho += pi->drho_norm_kernel[i] * dx[i];
+    A_j_rho += pj->drho_norm_kernel[i] * dx[i];
+
+    u_reconst_i -= 0.5 * pi->du_norm_kernel[i] * dx[i];
+    u_reconst_j += 0.5 * pj->du_norm_kernel[i] * dx[i];
+    rho_reconst_i -= 0.5 * pi->drho_norm_kernel[i] * dx[i];
+    rho_reconst_j += 0.5 * pj->drho_norm_kernel[i] * dx[i];
+  }
+
+  float phi_i_u, phi_j_u, phi_i_rho, phi_j_rho;
+  /* Slope limiter (Sandnes+2025 Eqn. 37) special cases */
+  if ((A_i_u == 0.f) && (A_j_u == 0.f)) {
+    phi_i_u = 1.f;
+    phi_j_u = 1.f;
+
+  } else if ((A_i_u == 0.f && A_j_u != 0.f) || (A_j_u == 0.f && A_i_u != 0.f) ||
+             (A_i_u == -A_j_u)) {
+    phi_i_u = 0.f;
+    phi_j_u = 0.f;
+  } else {
+    /* Slope limiter (Sandnes+2025 Eqn. 37) */
+    phi_i_u = min(1.f, 4.f * A_i_u / A_j_u / (1.f + A_i_u / A_j_u) /
+                           (1.f + A_i_u / A_j_u));
+    phi_i_u = max(0.f, phi_i_u);
+
+    phi_j_u = min(1.f, 4.f * A_j_u / A_i_u / (1.f + A_j_u / A_i_u) /
+                           (1.f + A_j_u / A_i_u));
+    phi_j_u = max(0.f, phi_j_u);
+  }
+
+  /* Slope limiter (Sandnes+2025 Eqn. 37) special cases */
+  if ((A_i_rho == 0.f) && (A_j_rho == 0.f)) {
+    phi_i_rho = 1.f;
+    phi_j_rho = 1.f;
+
+  } else if ((A_i_rho == 0.f && A_j_rho != 0.f) ||
+             (A_j_rho == 0.f && A_i_rho != 0.f) || (A_i_rho == -A_j_rho)) {
+    phi_i_rho = 0.f;
+    phi_j_rho = 0.f;
+  } else {
+    /* Slope limiter (Sandnes+2025 Eqn. 37) */
+    phi_i_rho = min(1.f, 4.f * A_i_rho / A_j_rho / (1.f + A_i_rho / A_j_rho) /
+                             (1.f + A_i_rho / A_j_rho));
+    phi_i_rho = max(0.f, phi_i_rho);
+
+    phi_j_rho = min(1.f, 4.f * A_j_rho / A_i_rho / (1.f + A_j_rho / A_i_rho) /
+                             (1.f + A_j_rho / A_i_rho));
+    phi_j_rho = max(0.f, phi_j_rho);
+  }
+
+  /* exp in slope limiter (middle case in Sandnes+2025 Eqn. 37) */
+  const float eta_ab = min(r * hi_inv, r * hj_inv);
+  if (eta_ab < eta_crit) {
+    phi_i_u *= expf(-(eta_ab - eta_crit) * (eta_ab - eta_crit) /
+                    slope_limiter_exp_denom);
+    phi_j_u *= expf(-(eta_ab - eta_crit) * (eta_ab - eta_crit) /
+                    slope_limiter_exp_denom);
+    phi_i_rho *= expf(-(eta_ab - eta_crit) * (eta_ab - eta_crit) /
+                      slope_limiter_exp_denom);
+    phi_j_rho *= expf(-(eta_ab - eta_crit) * (eta_ab - eta_crit) /
+                      slope_limiter_exp_denom);
+  }
+
+  /* Assemble the reconstructed internal energy (Sandnes+2025 Eqn. 44) and
+   * density (Sandnes+2025 Eqn. 45) */
+  *utilde_i = pi->u + phi_i_u * u_reconst_i;
+  *utilde_j = pj->u + phi_j_u * u_reconst_j;
+  *rhotilde_i = pi->rho_evol + phi_i_rho * rho_reconst_i;
+  *rhotilde_j = pj->rho_evol + phi_j_rho * rho_reconst_j;
+}
+
+#endif /* SWIFT_REMIX_HYDRO_VISC_DIFN_H */
diff --git a/src/hydro/SPHENIX/hydro.h b/src/hydro/SPHENIX/hydro.h
index f04c95abb3665bc6f20b7fc9d70f96c3ba32ebdd..26c5bdeeff4087273ab96f1922a0de48e640bed9 100644
--- a/src/hydro/SPHENIX/hydro.h
+++ b/src/hydro/SPHENIX/hydro.h
@@ -33,6 +33,7 @@
 #include "dimension.h"
 #include "entropy_floor.h"
 #include "equation_of_state.h"
+#include "fvpm_geometry.h"
 #include "hydro_parameters.h"
 #include "hydro_properties.h"
 #include "hydro_space.h"
@@ -364,6 +365,16 @@ hydro_set_physical_internal_energy(struct part *p, struct xpart *xp,
   xp->u_full = u / cosmo->a_factor_internal_energy;
 }
 
+__attribute__((always_inline)) INLINE static void
+hydro_set_physical_internal_energy_TESTING_SPH_RT(struct part *p,
+                                                  const struct cosmology *cosmo,
+                                                  const float u) {
+
+  // TODO: This might be a problem. Hacky version to get code to compile.
+  // TODO: Cosmology might need attention
+  p->u = u / cosmo->a_factor_internal_energy;
+}
+
 /**
  * @brief Sets the drifted physical internal energy of a particle
  *
@@ -581,6 +592,9 @@ __attribute__((always_inline)) INLINE static void hydro_init_part(
   p->inhibited_exact = 0;
   p->limited_part = 0;
 #endif
+
+  /* Init geometry for FVPM Radiative Transfer */
+  fvpm_geometry_init(p);
 }
 
 /**
@@ -629,6 +643,9 @@ __attribute__((always_inline)) INLINE static void hydro_end_density(
   p->viscosity.div_v *= h_inv_dim_plus_one * rho_inv * a_inv2;
   p->viscosity.div_v += cosmo->H * hydro_dimension;
 
+  /* Finish matrix and volume computations for FVPM Radiative Transfer */
+  fvpm_compute_volume_and_matrix(p, h_inv_dim);
+
 #ifdef SWIFT_HYDRO_DENSITY_CHECKS
   p->n_density += kernel_root;
   p->n_density *= h_inv_dim;
diff --git a/src/hydro/SPHENIX/hydro_iact.h b/src/hydro/SPHENIX/hydro_iact.h
index d8fdc7115860055cf96c7c7a4ab1f292c5472cd1..478e77f22f0d3b56de9e1f68f626777a279496af 100644
--- a/src/hydro/SPHENIX/hydro_iact.h
+++ b/src/hydro/SPHENIX/hydro_iact.h
@@ -26,7 +26,9 @@
  *        with added SPHENIX physics (Borrow 2020) (interaction routines)
  */
 
+#include "adaptive_softening_iact.h"
 #include "adiabatic_index.h"
+#include "fvpm_geometry.h"
 #include "hydro_parameters.h"
 #include "minmax.h"
 #include "signal_velocity.h"
@@ -65,9 +67,13 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
 
   pi->rho += mj * wi;
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
-
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, hi_inv, mj);
+
+  /* Collect data for FVPM matrix construction */
+  fvpm_accumulate_geometry_and_matrix(pi, wi, dx);
+  fvpm_update_centroid_left(pi, dx, wi);
 
   /* Compute density of pj. */
   const float hj_inv = 1.f / hj;
@@ -78,6 +84,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_density(
   pj->density.rho_dh -= mi * (hydro_dimension * wj + uj * wj_dx);
   pj->density.wcount += wj;
   pj->density.wcount_dh -= (hydro_dimension * wj + uj * wj_dx);
+  adaptive_softening_add_correction_term(pj, uj, hj_inv, mi);
+
+  /* Collect data for FVPM matrix construction */
+  fvpm_accumulate_geometry_and_matrix(pj, wj, dx);
+  fvpm_update_centroid_right(pj, dx, wj);
 
   /* Now we need to compute the div terms */
   const float r_inv = r ? 1.0f / r : 0.0f;
@@ -147,9 +158,13 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
 
   pi->rho += mj * wi;
   pi->density.rho_dh -= mj * (hydro_dimension * wi + ui * wi_dx);
-
   pi->density.wcount += wi;
   pi->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+  adaptive_softening_add_correction_term(pi, ui, h_inv, mj);
+
+  /* Collect data for FVPM matrix construction */
+  fvpm_accumulate_geometry_and_matrix(pi, wi, dx);
+  fvpm_update_centroid_left(pi, dx, wi);
 
   const float r_inv = r ? 1.0f / r : 0.0f;
   const float faci = mj * wi_dx * r_inv;
@@ -419,8 +434,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
@@ -564,8 +583,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
   const float sph_acc_term =
       (P_over_rho2_i * wi_dr + P_over_rho2_j * wj_dr) * r_inv;
 
+  /* Adaptive softening acceleration term */
+  const float adapt_soft_acc_term =
+      adaptive_softening_get_acc_term(pi, pj, wi_dr, wj_dr, f_ij, f_ji, r_inv);
+
   /* Assemble the acceleration */
-  const float acc = sph_acc_term + visc_acc_term;
+  const float acc = sph_acc_term + visc_acc_term + adapt_soft_acc_term;
 
   /* Use the force Luke ! */
   pi->a_hydro[0] -= mj * acc * dx[0];
diff --git a/src/hydro/SPHENIX/hydro_io.h b/src/hydro/SPHENIX/hydro_io.h
index 37396f1dbe4ff3fa48580b976505cab0f93af956..d123f6d1f703bdb45020f04e34b3a6c85b55d417 100644
--- a/src/hydro/SPHENIX/hydro_io.h
+++ b/src/hydro/SPHENIX/hydro_io.h
@@ -158,6 +158,16 @@ INLINE static void convert_part_potential(const struct engine* e,
     ret[0] = 0.f;
 }
 
+INLINE static void convert_part_softening(const struct engine* e,
+                                          const struct part* p,
+                                          const struct xpart* xp, float* ret) {
+  if (p->gpart != NULL)
+    ret[0] = kernel_gravity_softening_plummer_equivalent_inv *
+             gravity_get_softening(p->gpart, e->gravity_properties);
+  else
+    ret[0] = 0.f;
+}
+
 INLINE static void convert_viscosity(const struct engine* e,
                                      const struct part* p,
                                      const struct xpart* xp, float* ret) {
@@ -182,7 +192,7 @@ INLINE static void hydro_write_particles(const struct part* parts,
                                          struct io_props* list,
                                          int* num_fields) {
 
-  *num_fields = 15;
+  *num_fields = 16;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_part(
@@ -207,9 +217,9 @@ INLINE static void hydro_write_particles(const struct part* parts,
       -3.f * hydro_gamma_minus_one, parts, u,
       "Co-moving thermal energies per unit mass of the particles");
 
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
+  list[5] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, parts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[6] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
                                  parts, rho,
@@ -259,6 +269,11 @@ INLINE static void hydro_write_particles(const struct part* parts,
       "Potentials", FLOAT, 1, UNIT_CONV_POTENTIAL, -1.f, parts, xparts,
       convert_part_potential,
       "Co-moving gravitational potential at position of the particles");
+
+  list[15] = io_make_output_field_convert_part(
+      "Softenings", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, parts, xparts,
+      convert_part_softening,
+      "Co-moving gravitational Plummer-equivalent softenings of the particles");
 }
 
 /**
diff --git a/src/hydro/SPHENIX/hydro_part.h b/src/hydro/SPHENIX/hydro_part.h
index bbc3bd717d205e5799bce97561fdf757e421a836..1365357cbd4c47cfa1deb136ec064552f085c50d 100644
--- a/src/hydro/SPHENIX/hydro_part.h
+++ b/src/hydro/SPHENIX/hydro_part.h
@@ -26,11 +26,13 @@
  *        with added SPHENIX physics (Borrow 2020) (particle definition)
  */
 
+#include "adaptive_softening_struct.h"
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
 #include "csds.h"
 #include "feedback_struct.h"
+#include "fvpm_geometry_struct.h"
 #include "mhd_struct.h"
 #include "particle_splitting_struct.h"
 #include "pressure_floor_struct.h"
@@ -160,8 +162,8 @@ struct part {
   } diffusion;
 
   /* Store density/force specific stuff. */
-  union {
 
+  union {
     /**
      * @brief Structure for the variables only used in the density loop over
      * neighbours.
@@ -215,6 +217,9 @@ struct part {
     } force;
   };
 
+  /*! Additional data used for adaptive softening */
+  struct adaptive_softening_part_data adaptive_softening_data;
+
   /*! Additional data used by the MHD scheme */
   struct mhd_part_data mhd_data;
 
@@ -242,6 +247,9 @@ struct part {
   /*! RT sub-cycling time stepping data */
   struct rt_timestepping_data rt_time_data;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step length */
   timebin_t time_bin;
 
@@ -306,6 +314,9 @@ struct part {
   char limited_part;
 #endif
 
+  /*! Geometrical quantities used for Finite Volume Particle Method RT. */
+  struct fvpm_geometry_struct geometry;
+
 } SWIFT_STRUCT_ALIGN;
 
 #endif /* SWIFT_SPHENIX_HYDRO_PART_H */
diff --git a/src/hydro/Shadowswift/hydro.h b/src/hydro/Shadowswift/hydro.h
index 2343d7706103b598a186aa1a092c423d1156ecc5..6c52adb15ecf8c3c5aa8489fca9e65bd5e509db6 100644
--- a/src/hydro/Shadowswift/hydro.h
+++ b/src/hydro/Shadowswift/hydro.h
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
+ * Copyright (c) 2020 Matthieu Schaller (schaller@strw.leideuniv.nl)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,602 +16,249 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
-#ifndef SWIFT_SHADOWSWIFT_HYDRO_H
-#define SWIFT_SHADOWSWIFT_HYDRO_H
+#ifndef SWIFT_NONE_HYDRO_H
+#define SWIFT_NONE_HYDRO_H
+
+/**
+ * @file Shadowswift/hydro.h
+ * @brief Empty implementation
+ */
 
 #include "adiabatic_index.h"
 #include "approx_math.h"
 #include "cosmology.h"
+#include "dimension.h"
 #include "entropy_floor.h"
 #include "equation_of_state.h"
-#include "hydro_gradients.h"
+#include "hydro_parameters.h"
 #include "hydro_properties.h"
 #include "hydro_space.h"
+#include "kernel_hydro.h"
+#include "minmax.h"
 #include "pressure_floor.h"
-#include "voronoi_algorithm.h"
 
 #include <float.h>
 
 /**
- * @brief Computes the hydro time-step of a given particle
- *
- * @param p Pointer to the particle data.
- * @param xp Pointer to the extended particle data.
- * @param hydro_properties Pointer to the hydro parameters.
- */
-__attribute__((always_inline)) INLINE static float hydro_compute_timestep(
-    const struct part* restrict p, const struct xpart* restrict xp,
-    const struct hydro_props* restrict hydro_properties,
-    const struct cosmology* restrict cosmo) {
-
-  const float CFL_condition = hydro_properties->CFL_condition;
-
-  float vrel[3];
-  vrel[0] = p->primitives.v[0] - xp->v_full[0];
-  vrel[1] = p->primitives.v[1] - xp->v_full[1];
-  vrel[2] = p->primitives.v[2] - xp->v_full[2];
-  float vmax =
-      sqrtf(vrel[0] * vrel[0] + vrel[1] * vrel[1] + vrel[2] * vrel[2]) +
-      sqrtf(hydro_gamma * p->primitives.P / p->primitives.rho);
-  vmax = max(vmax, p->timestepvars.vmax);
-
-  const float psize =
-      cosmo->a *
-      powf(p->cell.volume / hydro_dimension_unit_sphere, hydro_dimension_inv);
-  float dt = FLT_MAX;
-  if (vmax > 0.) {
-    dt = psize / vmax;
-  }
-  return CFL_condition * dt;
-}
-
-/**
- * @brief Does some extra hydro operations once the actual physical time step
- * for the particle is known.
- *
- * We use this to store the physical time step, since it is used for the flux
- * exchange during the force loop.
+ * @brief Returns the comoving internal energy of a particle at the last
+ * time the particle was kicked.
  *
- * We also set the active flag of the particle to inactive. It will be set to
- * active in hydro_init_part, which is called the next time the particle becomes
- * active.
- *
- * @param p The particle to act upon.
- * @param dt Physical time step of the particle during the next step.
+ * @param p The particle of interest
+ * @param xp The extended data of the particle of interest.
  */
-__attribute__((always_inline)) INLINE static void hydro_timestep_extra(
-    struct part* p, float dt) {
+__attribute__((always_inline)) INLINE static float
+hydro_get_comoving_internal_energy(const struct part *restrict p,
+                                   const struct xpart *restrict xp) {
 
-  p->force.dt = dt;
-  p->force.active = 0;
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Initialises the particles for the first time
+ * @brief Returns the physical internal energy of a particle at the last
+ * time the particle was kicked.
  *
- * This function is called only once just after the ICs have been
- * read in to do some conversions.
- *
- * In this case, we copy the particle velocities into the corresponding
- * primitive variable field. We do this because the particle velocities in GIZMO
- * can be independent of the actual fluid velocity. The latter is stored as a
- * primitive variable and integrated using the linear momentum, a conserved
- * variable.
- *
- * @param p The particle to act upon
- * @param xp The extended particle data to act upon
+ * @param p The particle of interest.
+ * @param xp The extended data of the particle of interest.
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static void hydro_first_init_part(
-    struct part* p, struct xpart* xp) {
-
-  const float mass = p->conserved.mass;
-
-  p->time_bin = 0;
-
-  p->primitives.v[0] = p->v[0];
-  p->primitives.v[1] = p->v[1];
-  p->primitives.v[2] = p->v[2];
-
-  p->conserved.momentum[0] = mass * p->primitives.v[0];
-  p->conserved.momentum[1] = mass * p->primitives.v[1];
-  p->conserved.momentum[2] = mass * p->primitives.v[2];
-
-#ifdef EOS_ISOTHERMAL_GAS
-  p->conserved.energy = mass * gas_internal_energy_from_entropy(0.f, 0.f);
-#else
-  p->conserved.energy *= mass;
-#endif
-
-#ifdef SHADOWFAX_TOTAL_ENERGY
-  p->conserved.energy += 0.5f * (p->conserved.momentum[0] * p->primitives.v[0] +
-                                 p->conserved.momentum[1] * p->primitives.v[1] +
-                                 p->conserved.momentum[2] * p->primitives.v[2]);
-#endif
-
-#if defined(SHADOWFAX_FIX_CELLS)
-  p->v[0] = 0.;
-  p->v[1] = 0.;
-  p->v[2] = 0.;
-#else
-  p->v[0] = p->primitives.v[0];
-  p->v[1] = p->primitives.v[1];
-  p->v[2] = p->primitives.v[2];
-#endif
-
-  /* set the initial velocity of the cells */
-  xp->v_full[0] = p->v[0];
-  xp->v_full[1] = p->v[1];
-  xp->v_full[2] = p->v[2];
+__attribute__((always_inline)) INLINE static float
+hydro_get_physical_internal_energy(const struct part *restrict p,
+                                   const struct xpart *restrict xp,
+                                   const struct cosmology *cosmo) {
 
-  /* ignore accelerations present in the initial condition */
-  p->a_hydro[0] = 0.0f;
-  p->a_hydro[1] = 0.0f;
-  p->a_hydro[2] = 0.0f;
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Prepares a particle for the volume calculation.
+ * @brief Returns the comoving internal energy of a particle drifted to the
+ * current time.
  *
- * Simply makes sure all necessary variables are initialized to zero.
- * Initializes the Voronoi cell.
- *
- * @param p The particle to act upon
- * @param hs #hydro_space containing extra information about the space.
+ * @param p The particle of interest
  */
-__attribute__((always_inline)) INLINE static void hydro_init_part(
-    struct part* p, const struct hydro_space* hs) {
-
-  /* make sure we don't enter the no neighbour case in runner.c */
-  p->density.wcount = 1.0f;
-  p->density.wcount_dh = 0.0f;
-
-  voronoi_cell_init(&p->cell, p->x, hs->anchor, hs->side);
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_comoving_internal_energy(const struct part *restrict p) {
 
-  /* Set the active flag to active. */
-  p->force.active = 1;
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Finishes the volume calculation.
+ * @brief Returns the physical internal energy of a particle drifted to the
+ * current time.
  *
- * Calls the finalize method on the Voronoi cell, which calculates the volume
- * and centroid of the cell. We use the return value of this function to set
- * a new value for the smoothing length and possibly force another iteration
- * of the volume calculation for this particle. We then use the volume to
- * convert conserved variables into primitive variables.
- *
- * This method also initializes the gradient variables (if gradients are used).
- *
- * @param p The particle to act upon.
+ * @param p The particle of interest.
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static void hydro_end_density(
-    struct part* restrict p, const struct cosmology* cosmo) {
-
-  float volume;
-  float m, momentum[3], energy;
-
-  hydro_gradients_init(p);
-
-  float hnew = voronoi_cell_finalize(&p->cell);
-  /* Enforce hnew as new smoothing length in the iteration
-     This is annoyingly difficult, as we do not have access to the variables
-     that govern the loop...
-     So here's an idea: let's force in some method somewhere that makes sure
-     r->e->hydro_properties->target_neighbours is 1, and
-     r->e->hydro_properties->delta_neighbours is 0.
-     This way, we can accept an iteration by setting p->density.wcount to 1.
-     To get the right correction for h, we set wcount to something else
-     (say 0), and then set p->density.wcount_dh to 1/(hnew-h). */
-  if (hnew < p->h) {
-    /* Iteration succesful: we accept, but manually set h to a smaller value
-       for the next time step */
-    const float hinvdim = pow_dimension(1.0f / p->h);
-    p->density.wcount = hinvdim;
-    p->h = 1.1f * hnew;
-  } else {
-    /* Iteration not succesful: we force h to become 1.1*hnew */
-    p->density.wcount = 0.0f;
-    p->density.wcount_dh = p->h / (1.1f * hnew - p->h);
-    return;
-  }
-  volume = p->cell.volume;
-
-#ifdef SWIFT_DEBUG_CHECKS
-  /* the last condition checks for NaN: a NaN value always evaluates to false,
-     even when checked against itself */
-  if (volume == 0. || volume == INFINITY || volume != volume) {
-    error("Invalid value for cell volume (%g)!", volume);
-  }
-#endif
-
-  /* compute primitive variables */
-  /* eqns (3)-(5) */
-  m = p->conserved.mass;
-  if (m > 0.) {
-    momentum[0] = p->conserved.momentum[0];
-    momentum[1] = p->conserved.momentum[1];
-    momentum[2] = p->conserved.momentum[2];
-    p->primitives.rho = m / volume;
-    p->primitives.v[0] = momentum[0] / m;
-    p->primitives.v[1] = momentum[1] / m;
-    p->primitives.v[2] = momentum[2] / m;
-
-    energy = p->conserved.energy;
-
-#ifdef SHADOWFAX_TOTAL_ENERGY
-    energy -= 0.5f * (momentum[0] * p->primitives.v[0] +
-                      momentum[1] * p->primitives.v[1] +
-                      momentum[2] * p->primitives.v[2]);
-#endif
-
-    energy /= m;
-
-    p->primitives.P =
-        gas_pressure_from_internal_energy(p->primitives.rho, energy);
-  } else {
-    p->primitives.rho = 0.;
-    p->primitives.v[0] = 0.;
-    p->primitives.v[1] = 0.;
-    p->primitives.v[2] = 0.;
-    p->primitives.P = 0.;
-  }
-
-#ifdef SWIFT_DEBUG_CHECKS
-  if (p->primitives.rho < 0.) {
-    error("Negative density!");
-  }
-
-  if (p->primitives.P < 0.) {
-    error("Negative pressure!");
-  }
-#endif
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_physical_internal_energy(const struct part *restrict p,
+                                           const struct cosmology *cosmo) {
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Sets all particle fields to sensible values when the #part has 0 ngbs.
+ * @brief Returns the comoving pressure of a particle
  *
- * @param p The particle to act upon
- * @param xp The extended particle data to act upon
+ * Computes the pressure based on the particle's properties.
+ *
+ * @param p The particle of interest
  */
-__attribute__((always_inline)) INLINE static void hydro_part_has_no_neighbours(
-    struct part* restrict p, struct xpart* restrict xp,
-    const struct cosmology* cosmo) {
-
-  /* Some smoothing length multiples. */
-  const float h = p->h;
-  const float h_inv = 1.0f / h;                 /* 1/h */
-  const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
+__attribute__((always_inline)) INLINE static float hydro_get_comoving_pressure(
+    const struct part *restrict p) {
 
-  /* Re-set problematic values */
-  p->density.wcount = kernel_root * h_inv_dim;
-  p->density.wcount_dh = 0.f;
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Prepare a particle for the force calculation.
+ * @brief Returns the physical pressure of a particle
  *
- * This function is called in the ghost task to convert some quantities coming
- * from the density loop over neighbours into quantities ready to be used in the
- * force loop over neighbours. Quantities are typically read from the density
- * sub-structure and written to the force sub-structure.
- * Examples of calculations done here include the calculation of viscosity term
- * constants, thermal conduction terms, hydro conversions, etc.
+ * Computes the pressure based on the particle's properties and
+ * convert it to physical coordinates.
  *
- * @param p The particle to act upon
- * @param xp The extended particle data to act upon
- * @param cosmo The current cosmological model.
- * @param hydro_props Hydrodynamic properties.
- * @param dt_alpha The time-step used to evolve non-cosmological quantities such
- *                 as the artificial viscosity.
- * @param dt_therm The time-step used to evolve hydrodynamical quantities.
+ * @param p The particle of interest
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static void hydro_prepare_force(
-    struct part* restrict p, struct xpart* restrict xp,
-    const struct cosmology* cosmo, const struct hydro_props* hydro_props,
-    const struct pressure_floor_props* pressure_floor, const float dt_alpha,
-    const float dt_therm) {
-
-  /* Initialize time step criterion variables */
-  p->timestepvars.vmax = 0.0f;
-
-  /* Set the actual velocity of the particle */
-  p->force.v_full[0] = xp->v_full[0];
-  p->force.v_full[1] = xp->v_full[1];
-  p->force.v_full[2] = xp->v_full[2];
-
-  p->conserved.flux.mass = 0.0f;
-  p->conserved.flux.momentum[0] = 0.0f;
-  p->conserved.flux.momentum[1] = 0.0f;
-  p->conserved.flux.momentum[2] = 0.0f;
-  p->conserved.flux.energy = 0.0f;
+__attribute__((always_inline)) INLINE static float hydro_get_physical_pressure(
+    const struct part *restrict p, const struct cosmology *cosmo) {
+
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Prepare a particle for the gradient calculation.
- *
- * This function is called after the density loop and before the gradient loop.
+ * @brief Returns the comoving entropy of a particle at the last
+ * time the particle was kicked.
  *
- * We use it to set the physical timestep for the particle and to copy the
- * actual velocities, which we need to boost our interfaces during the flux
- * calculation. We also initialize the variables used for the time step
- * calculation.
- *
- * @param p The particle to act upon.
- * @param xp The extended particle data to act upon.
- * @param cosmo The cosmological model.
- * @param hydro_props Hydrodynamic properties.
+ * @param p The particle of interest.
+ * @param xp The extended data of the particle of interest.
  */
-__attribute__((always_inline)) INLINE static void hydro_prepare_gradient(
-    struct part* restrict p, struct xpart* restrict xp,
-    const struct cosmology* cosmo, const struct hydro_props* hydro_props,
-    const struct pressure_floor_props* pressure_floor) {
+__attribute__((always_inline)) INLINE static float hydro_get_comoving_entropy(
+    const struct part *restrict p, const struct xpart *restrict xp) {
 
-  /* Initialize time step criterion variables */
-  p->timestepvars.vmax = 0.;
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Resets the variables that are required for a gradient calculation.
+ * @brief Returns the physical entropy of a particle at the last
+ * time the particle was kicked.
  *
- * This function is called after hydro_prepare_gradient.
- *
- * @param p The particle to act upon.
- * @param xp The extended particle data to act upon.
+ * @param p The particle of interest.
+ * @param xp The extended data of the particle of interest.
  * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static void hydro_reset_gradient(
-    struct part* restrict p) {}
-
-/**
- * @brief Finishes the gradient calculation.
- *
- * Just a wrapper around hydro_gradients_finalize, which can be an empty method,
- * in which case no gradients are used.
- *
- * @param p The particle to act upon.
- */
-__attribute__((always_inline)) INLINE static void hydro_end_gradient(
-    struct part* p) {
+__attribute__((always_inline)) INLINE static float hydro_get_physical_entropy(
+    const struct part *restrict p, const struct xpart *restrict xp,
+    const struct cosmology *cosmo) {
 
-  hydro_gradients_finalize(p);
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Reset acceleration fields of a particle
- *
- * This is actually not necessary for Shadowswift, since we just set the
- * accelerations after the flux calculation.
+ * @brief Returns the comoving entropy of a particle drifted to the
+ * current time.
  *
- * @param p The particle to act upon.
+ * @param p The particle of interest.
  */
-__attribute__((always_inline)) INLINE static void hydro_reset_acceleration(
-    struct part* p) {
-
-  /* Reset the acceleration. */
-  p->a_hydro[0] = 0.0f;
-  p->a_hydro[1] = 0.0f;
-  p->a_hydro[2] = 0.0f;
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_comoving_entropy(const struct part *restrict p) {
 
-  /* Reset the time derivatives. */
-  p->force.h_dt = 0.0f;
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Sets the values to be predicted in the drifts to their values at a
- * kick time
+ * @brief Returns the physical entropy of a particle drifted to the
+ * current time.
  *
- * @param p The particle.
- * @param xp The extended data of this particle.
+ * @param p The particle of interest.
  * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static void hydro_reset_predicted_values(
-    struct part* restrict p, const struct xpart* restrict xp,
-    const struct cosmology* cosmo,
-    const struct pressure_floor_props* pressure_floor) {}
-
-/**
- * @brief Converts the hydrodynamic variables from the initial condition file to
- * conserved variables that can be used during the integration
- *
- * Requires the volume to be known.
- *
- * The initial condition file contains a mixture of primitive and conserved
- * variables. Mass is a conserved variable, and we just copy the particle
- * mass into the corresponding conserved quantity. We need the volume to
- * also derive a density, which is then used to convert the internal energy
- * to a pressure. However, we do not actually use these variables anymore.
- * We do need to initialize the linear momentum, based on the mass and the
- * velocity of the particle.
- *
- * @param p The particle to act upon.
- * @param xp The extended particle data to act upon.
- */
-__attribute__((always_inline)) INLINE static void hydro_convert_quantities(
-    struct part* p, struct xpart* xp, const struct cosmology* cosmo,
-    const struct hydro_props* hydro_props,
-    const struct pressure_floor_props* pressure_floor) {}
-
-/**
- * @brief Extra operations to be done during the drift
- *
- * Not used for Shadowswift.
- *
- * @param p Particle to act upon.
- * @param xp The extended particle data to act upon.
- * @param dt The drift time-step.
- */
-__attribute__((always_inline)) INLINE static void hydro_predict_extra(
-    struct part* p, struct xpart* xp, float dt_drift, float dt_therm,
-    float dt_kick_grav, const struct cosmology* cosmo,
-    const struct hydro_props* hydro_props,
-    const struct entropy_floor_properties* floor_props,
-    const struct pressure_floor_props* pressure_floor) {}
+__attribute__((always_inline)) INLINE static float
+hydro_get_drifted_physical_entropy(const struct part *restrict p,
+                                   const struct cosmology *cosmo) {
 
-/**
- * @brief Set the particle acceleration after the flux loop.
- *
- * @param p Particle to act upon.
- */
-__attribute__((always_inline)) INLINE static void hydro_end_force(
-    struct part* p, const struct cosmology* cosmo) {}
+  error("Empty implementation");
+  return -1.f;
+}
 
 /**
- * @brief Extra operations done during the kick
- *
- * Not used for Shadowswift.
+ * @brief Returns the comoving sound speed of a particle
  *
- * @param p Particle to act upon.
- * @param xp Extended particle data to act upon.
- * @param dt Physical time step.
+ * @param p The particle of interest
  */
-__attribute__((always_inline)) INLINE static void hydro_kick_extra(
-    struct part* p, struct xpart* xp, float dt, float dt_grav,
-    float dt_grav_mesh, float dt_hydro, float dt_kick_corr,
-    const struct cosmology* cosmo, const struct hydro_props* hydro_props,
-    const struct entropy_floor_properties* floor_props) {
-
-  /* Update the conserved variables. We do this here and not in the kick,
-     since we need the updated variables below. */
-  p->conserved.mass += p->conserved.flux.mass * dt;
-  p->conserved.momentum[0] += p->conserved.flux.momentum[0] * dt;
-  p->conserved.momentum[1] += p->conserved.flux.momentum[1] * dt;
-  p->conserved.momentum[2] += p->conserved.flux.momentum[2] * dt;
-
-#ifdef EOS_ISOTHERMAL_GAS
-  /* reset the thermal energy */
-  p->conserved.energy =
-      p->conserved.mass * gas_internal_energy_from_entropy(0.f, 0.f);
-#else
-  p->conserved.energy += p->conserved.flux.energy * dt;
-#endif
-
-#if defined(SHADOWFAX_FIX_CELLS)
-  p->v[0] = 0.0f;
-  p->v[1] = 0.0f;
-  p->v[2] = 0.0f;
-#else
-  if (p->conserved.mass > 0.0f && p->primitives.rho > 0.0f) {
-
-    const float inverse_mass = 1.f / p->conserved.mass;
-
-    /* Normal case: set particle velocity to fluid velocity. */
-    p->v[0] = p->conserved.momentum[0] * inverse_mass;
-    p->v[1] = p->conserved.momentum[1] * inverse_mass;
-    p->v[2] = p->conserved.momentum[2] * inverse_mass;
-
-#ifdef SHADOWFAX_STEER_CELL_MOTION
-    float centroid[3], d[3];
-    float volume, csnd, R, vfac, fac, dnrm;
-    voronoi_get_centroid(&p->cell, centroid);
-    d[0] = centroid[0] - p->x[0];
-    d[1] = centroid[1] - p->x[1];
-    d[2] = centroid[2] - p->x[2];
-    dnrm = sqrtf(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
-    csnd = sqrtf(hydro_gamma * p->primitives.P / p->primitives.rho);
-    volume = p->cell.volume;
-    R = get_radius_dimension_sphere(volume);
-    fac = 4.0f * dnrm / R;
-    if (fac > 0.9f) {
-      if (fac < 1.1f) {
-        vfac = csnd * (dnrm - 0.225f * R) / dnrm / (0.05f * R);
-      } else {
-        vfac = csnd / dnrm;
-      }
-      p->v[0] += vfac * d[0];
-      p->v[1] += vfac * d[1];
-      p->v[2] += vfac * d[2];
-    }
-#endif
-
-  } else {
-    p->v[0] = 0.;
-    p->v[1] = 0.;
-    p->v[2] = 0.;
-  }
-#endif
-
-  /* Now make sure all velocity variables are up to date. */
-  xp->v_full[0] = p->v[0];
-  xp->v_full[1] = p->v[1];
-  xp->v_full[2] = p->v[2];
+__attribute__((always_inline)) INLINE static float
+hydro_get_comoving_soundspeed(const struct part *restrict p) {
 
-  if (p->gpart) {
-    p->gpart->v_full[0] = p->v[0];
-    p->gpart->v_full[1] = p->v[1];
-    p->gpart->v_full[2] = p->v[2];
-  }
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Returns the internal energy of a particle
+ * @brief Returns the physical sound speed of a particle
  *
- * @param p The particle of interest.
- * @return Internal energy of the particle.
+ * @param p The particle of interest
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_internal_energy(
-    const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static float
+hydro_get_physical_soundspeed(const struct part *restrict p,
+                              const struct cosmology *cosmo) {
 
-  if (p->primitives.rho > 0.) {
-    return gas_internal_energy_from_pressure(p->primitives.rho,
-                                             p->primitives.P);
-  } else {
-    return 0.;
-  }
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Returns the entropy of a particle
+ * @brief Returns the physical density of a particle
  *
- * @param p The particle of interest.
- * @return Entropy of the particle.
+ * @param p The particle of interest
  */
-__attribute__((always_inline)) INLINE static float hydro_get_entropy(
-    const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static float hydro_get_comoving_density(
+    const struct part *restrict p) {
 
-  if (p->primitives.rho > 0.) {
-    return gas_entropy_from_pressure(p->primitives.rho, p->primitives.P);
-  } else {
-    return 0.;
-  }
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Returns the sound speed of a particle
+ * @brief Returns the comoving density of a particle.
  *
- * @param p The particle of interest.
- * @param Sound speed of the particle.
+ * @param p The particle of interest
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_soundspeed(
-    const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static float hydro_get_physical_density(
+    const struct part *restrict p, const struct cosmology *cosmo) {
 
-  if (p->primitives.rho > 0.) {
-    return gas_soundspeed_from_pressure(p->primitives.rho, p->primitives.P);
-  } else {
-    return 0.;
-  }
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Returns the pressure of a particle
+ * @brief Returns the mass of a particle
  *
  * @param p The particle of interest
- * @param Pressure of the particle.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_pressure(
-    const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static float hydro_get_mass(
+    const struct part *restrict p) {
 
-  return p->primitives.P;
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Returns the mass of a particle
+ * @brief Sets the mass of a particle
  *
  * @param p The particle of interest
+ * @param m The mass to set.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_mass(
-    const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static void hydro_set_mass(
+    struct part *restrict p, float m) {
 
-  return p->conserved.mass;
+  error("Empty implementation");
 }
 
 /**
@@ -619,349 +266,478 @@ __attribute__((always_inline)) INLINE static float hydro_get_mass(
  *
  * @param p The particle of interest
  * @param xp The extended data of the particle.
- * @param dt The time since the last kick.
+ * @param dt_kick_hydro The time (for hydro accelerations) since the last kick.
+ * @param dt_kick_grav The time (for gravity accelerations) since the last kick.
  * @param v (return) The velocities at the current time.
  */
 __attribute__((always_inline)) INLINE static void hydro_get_drifted_velocities(
-    const struct part* restrict p, const struct xpart* xp, float dt_kick_hydro,
+    const struct part *restrict p, const struct xpart *xp, float dt_kick_hydro,
     float dt_kick_grav, float v[3]) {
 
-  v[0] = p->v[0];
-  v[1] = p->v[1];
-  v[2] = p->v[2];
+  v[0] = xp->v_full[0] + p->a_hydro[0] * dt_kick_hydro +
+         xp->a_grav[0] * dt_kick_grav;
+  v[1] = xp->v_full[1] + p->a_hydro[1] * dt_kick_hydro +
+         xp->a_grav[1] * dt_kick_grav;
+  v[2] = xp->v_full[2] + p->a_hydro[2] * dt_kick_hydro +
+         xp->a_grav[2] * dt_kick_grav;
 }
 
 /**
- * @brief Modifies the thermal state of a particle to the imposed internal
- * energy
+ * @brief Returns the time derivative of co-moving internal energy of a particle
  *
- * This overrides the current state of the particle but does *not* change its
- * time-derivatives
+ * We assume a constant density.
  *
- * @param p The particle
- * @param u The new internal energy
+ * @param p The particle of interest
  */
-__attribute__((always_inline)) INLINE static void hydro_set_internal_energy(
-    struct part* restrict p, float u) {
-
-  if (p->primitives.rho > 0.) {
-    p->conserved.energy = u * p->conserved.mass;
-
-#ifdef SHADOWFAX_TOTAL_ENERGY
-    p->conserved.energy +=
-        0.5f * (p->conserved.momentum[0] * p->primitives.v[0] +
-                p->conserved.momentum[1] * p->primitives.v[1] +
-                p->conserved.momentum[2] * p->primitives.v[2]);
-#endif
+__attribute__((always_inline)) INLINE static float
+hydro_get_comoving_internal_energy_dt(const struct part *restrict p) {
 
-    p->primitives.P = gas_pressure_from_internal_energy(p->primitives.rho, u);
-  }
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Modifies the thermal state of a particle to the imposed entropy
+ * @brief Returns the time derivative of internal energy of a particle
  *
- * This overrides the current state of the particle but does *not* change its
- * time-derivatives
+ * We assume a constant density.
  *
- * @param p The particle
- * @param S The new entropy
+ * @param p The particle of interest
+ * @param cosmo Cosmology data structure
  */
-__attribute__((always_inline)) INLINE static void hydro_set_entropy(
-    struct part* restrict p, float S) {
-
-  if (p->primitives.rho > 0.) {
-    p->conserved.energy =
-        gas_internal_energy_from_entropy(p->primitives.rho, S) *
-        p->conserved.mass;
-
-#ifdef SHADOWFAX_TOTAL_ENERGY
-    p->conserved.energy +=
-        0.5f * (p->conserved.momentum[0] * p->primitives.v[0] +
-                p->conserved.momentum[1] * p->primitives.v[1] +
-                p->conserved.momentum[2] * p->primitives.v[2]);
-#endif
+__attribute__((always_inline)) INLINE static float
+hydro_get_physical_internal_energy_dt(const struct part *restrict p,
+                                      const struct cosmology *cosmo) {
 
-    p->primitives.P = gas_pressure_from_entropy(p->primitives.rho, S);
-  }
+  error("Empty implementation");
+  return -1.f;
 }
 
 /**
- * @brief Sets the mass of a particle
+ * @brief Sets the time derivative of the co-moving internal energy of a
+ * particle
  *
- * @param p The particle of interest
- * @param m The mass to set.
+ * We assume a constant density for the conversion to entropy.
+ *
+ * @param p The particle of interest.
+ * @param du_dt The new time derivative of the internal energy.
  */
-__attribute__((always_inline)) INLINE static void hydro_set_mass(
-    struct part* restrict p, float m) {
+__attribute__((always_inline)) INLINE static void
+hydro_set_comoving_internal_energy_dt(struct part *restrict p, float du_dt) {
 
-  p->conserved.mass = m;
+  error("Empty implementation");
 }
 
 /**
- * @brief Overwrite the initial internal energy of a particle.
+ * @brief Returns the time derivative of internal energy of a particle
  *
- * Note that in the cases where the thermodynamic variable is not
- * internal energy but gets converted later, we must overwrite that
- * field. The conversion to the actual variable happens later after
- * the initial fake time-step.
+ * We assume a constant density.
  *
- * @param p The #part to write to.
- * @param u_init The new initial internal energy.
+ * @param p The particle of interest.
+ * @param cosmo Cosmology data structure
+ * @param du_dt The new time derivative of the internal energy.
  */
 __attribute__((always_inline)) INLINE static void
-hydro_set_init_internal_energy(struct part* p, float u_init) {
-
-  p->conserved.energy = u_init * p->conserved.mass;
-#ifdef GIZMO_TOTAL_ENERGY
-  /* add the kinetic energy */
-  p->conserved.energy += 0.5f * p->conserved.mass *
-                         (p->conserved.momentum[0] * p->primitives.v[0] +
-                          p->conserved.momentum[1] * p->primitives.v[1] +
-                          p->conserved.momentum[2] * p->primitives.v[2]);
-#endif
-  p->primitives.P = hydro_gamma_minus_one * p->primitives.rho * u_init;
+hydro_set_physical_internal_energy_dt(struct part *restrict p,
+                                      const struct cosmology *cosmo,
+                                      float du_dt) {
+
+  error("Empty implementation");
 }
 
 /**
- * @brief Returns the comoving internal energy of a particle
+ * @brief Sets the physical entropy of a particle
  *
  * @param p The particle of interest.
+ * @param xp The extended particle data.
+ * @param cosmo Cosmology data structure
+ * @param entropy The physical entropy
  */
-__attribute__((always_inline)) INLINE static float
-hydro_get_comoving_internal_energy(const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static void hydro_set_physical_entropy(
+    struct part *p, struct xpart *xp, const struct cosmology *cosmo,
+    const float entropy) {
 
-  if (p->primitives.rho > 0.)
-    return gas_internal_energy_from_pressure(p->primitives.rho,
-                                             p->primitives.P);
-  else
-    return 0.;
+  error("Empty implementation");
 }
 
 /**
- * @brief Returns the comoving entropy of a particle
+ * @brief Sets the physical internal energy of a particle
  *
  * @param p The particle of interest.
+ * @param xp The extended particle data.
+ * @param cosmo Cosmology data structure
+ * @param u The physical internal energy
  */
-__attribute__((always_inline)) INLINE static float hydro_get_comoving_entropy(
-    const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static void
+hydro_set_physical_internal_energy(struct part *p, struct xpart *xp,
+                                   const struct cosmology *cosmo,
+                                   const float u) {
 
-  if (p->primitives.rho > 0.) {
-    return gas_entropy_from_pressure(p->primitives.rho, p->primitives.P);
-  } else {
-    return 0.;
-  }
+  error("Empty implementation");
 }
 
 /**
- * @brief Returns the sound speed of a particle
+ * @brief Sets the drifted physical internal energy of a particle
  *
  * @param p The particle of interest.
+ * @param cosmo Cosmology data structure
+ * @param u The physical internal energy
  */
-__attribute__((always_inline)) INLINE static float
-hydro_get_comoving_soundspeed(const struct part* restrict p) {
-
-  if (p->primitives.rho > 0.)
-    return gas_soundspeed_from_pressure(p->primitives.rho, p->primitives.P);
-  else
-    return 0.;
+__attribute__((always_inline)) INLINE static void
+hydro_set_drifted_physical_internal_energy(
+    struct part *p, const struct cosmology *cosmo,
+    const struct pressure_floor_props *pressure_floor, const float u) {
+  error("Empty implementation");
 }
 
 /**
- * @brief Returns the comoving pressure of a particle
+ * @brief Correct the signal velocity of the particle partaking in
+ * supernova (kinetic) feedback based on the velocity kick the particle receives
  *
- * @param p The particle of interest
+ * @param p The particle of interest.
+ * @param cosmo Cosmology data structure
+ * @param dv_phys The velocity kick received by the particle expressed in
+ * physical units (note that dv_phys must be positive or equal to zero)
  */
-__attribute__((always_inline)) INLINE static float hydro_get_comoving_pressure(
-    const struct part* restrict p) {
+__attribute__((always_inline)) INLINE static void
+hydro_set_v_sig_based_on_velocity_kick(struct part *p,
+                                       const struct cosmology *cosmo,
+                                       const float dv_phys) {
 
-  return p->primitives.P;
+  error("Empty implementation");
 }
 
 /**
- * @brief Returns the comoving density of a particle
+ * @brief Update the value of the viscosity alpha for the scheme.
  *
- * @param p The particle of interest
+ * @param p the particle of interest
+ * @param alpha the new value for the viscosity coefficient.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_comoving_density(
-    const struct part* restrict p) {
-
-  return p->primitives.rho;
+__attribute__((always_inline)) INLINE static void hydro_set_viscosity_alpha(
+    struct part *restrict p, float alpha) {
+  error("Empty implementation");
 }
 
 /**
- * @brief Returns the physical internal energy of a particle
+ * @brief Update the value of the diffusive coefficients to the
+ *        feedback reset value for the scheme.
  *
- * @param p The particle of interest.
- * @param cosmo The cosmological model.
+ * @param p the particle of interest
  */
-__attribute__((always_inline)) INLINE static float
-hydro_get_physical_internal_energy(const struct part* restrict p,
-                                   const struct xpart* restrict xp,
-                                   const struct cosmology* cosmo) {
-
-  return cosmo->a_factor_internal_energy *
-         hydro_get_comoving_internal_energy(p);
+__attribute__((always_inline)) INLINE static void
+hydro_diffusive_feedback_reset(struct part *restrict p) {
+  error("Empty implementation");
 }
 
 /**
- * @brief Returns the physical internal energy of a particle
+ * @brief Computes the hydro time-step of a given particle
  *
- * @param p The particle of interest.
+ * This function returns the time-step of a particle given its hydro-dynamical
+ * state. A typical time-step calculation would be the use of the CFL condition.
+ *
+ * @param p Pointer to the particle data
+ * @param xp Pointer to the extended particle data
+ * @param hydro_properties The SPH parameters
  * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_physical_entropy(
-    const struct part* restrict p, const struct cosmology* cosmo) {
+__attribute__((always_inline)) INLINE static float hydro_compute_timestep(
+    const struct part *restrict p, const struct xpart *restrict xp,
+    const struct hydro_props *restrict hydro_properties,
+    const struct cosmology *restrict cosmo) {
 
-  /* Note: no cosmological conversion required here with our choice of
-   * coordinates. */
-  return hydro_get_comoving_entropy(p);
+  return FLT_MAX;
 }
 
 /**
- * @brief Returns the physical sound speed of a particle
+ * @brief Compute the signal velocity between two gas particles
  *
- * @param p The particle of interest.
- * @param cosmo The cosmological model.
+ * Just return -1 in this empty implementation.
+ *
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @brief pi The first #part.
+ * @brief pj The second #part.
+ * @brief mu_ij The velocity on the axis linking the particles, or zero if the
+ * particles are moving away from each other,
+ * @brief beta The non-linear viscosity constant.
  */
-__attribute__((always_inline)) INLINE static float
-hydro_get_physical_soundspeed(const struct part* restrict p,
-                              const struct cosmology* cosmo) {
+__attribute__((always_inline)) INLINE static float hydro_signal_velocity(
+    const float dx[3], const struct part *restrict pi,
+    const struct part *restrict pj, const float mu_ij, const float beta) {
 
-  return cosmo->a_factor_sound_speed * hydro_get_comoving_soundspeed(p);
+  return -1.;
 }
 
 /**
- * @brief Sets the physical entropy of a particle
+ * @brief Does some extra hydro operations once the actual physical time step
+ * for the particle is known.
  *
- * @param p The particle of interest.
- * @param xp The extended particle data.
- * @param cosmo Cosmology data structure
- * @param entropy The physical entropy
+ * @param p The particle to act upon.
+ * @param dt Physical time step of the particle during the next step.
  */
-__attribute__((always_inline)) INLINE static void hydro_set_physical_entropy(
-    struct part* p, struct xpart* xp, const struct cosmology* cosmo,
-    const float entropy) {
+__attribute__((always_inline)) INLINE static void hydro_timestep_extra(
+    struct part *p, float dt) {}
 
-  error("Needs implementing");
+/**
+ * @brief Prepares a particle for the density calculation.
+ *
+ * Zeroes all the relevant arrays in preparation for the sums taking place in
+ * the various density loop over neighbours. Typically, all fields of the
+ * density sub-structure of a particle get zeroed in here.
+ *
+ * @param p The particle to act upon
+ * @param hs #hydro_space containing hydro specific space information.
+ */
+__attribute__((always_inline)) INLINE static void hydro_init_part(
+    struct part *restrict p, const struct hydro_space *hs) {
+
+  p->density.wcount = 0.f;
+  p->density.wcount_dh = 0.f;
 }
 
 /**
- * @brief Sets the physical internal energy of a particle
+ * @brief Finishes the density calculation.
  *
- * @param p The particle of interest.
- * @param xp The extended particle data.
- * @param cosmo Cosmology data structure
- * @param u The physical internal energy
+ * Multiplies the density and number of neighbours by the appropiate constants
+ * and add the self-contribution term.
+ * Additional quantities such as velocity gradients will also get the final
+ * terms added to them here.
+ *
+ * Also adds/multiplies the cosmological terms if need be.
+ *
+ * @param p The particle to act upon
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static void
-hydro_set_physical_internal_energy(struct part* p, struct xpart* xp,
-                                   const struct cosmology* cosmo,
-                                   const float u) {
-  error("Need implementing");
-}
+__attribute__((always_inline)) INLINE static void hydro_end_density(
+    struct part *restrict p, const struct cosmology *cosmo) {}
 
 /**
- * @brief Sets the drifted physical internal energy of a particle
+ * @brief Prepare a particle for the gradient calculation.
  *
- * @param p The particle of interest.
- * @param cosmo Cosmology data structure
- * @param u The physical internal energy
+ * This function is called after the density loop and before the gradient loop.
+ * Nothing to do in this scheme as the gradient loop is not used.
+ *
+ * @param p The particle to act upon.
+ * @param xp The extended particle data to act upon.
+ * @param cosmo The cosmological model.
+ * @param hydro_props Hydrodynamic properties.
  */
-__attribute__((always_inline)) INLINE static void
-hydro_set_drifted_physical_internal_energy(struct part* p,
-                                           const struct cosmology* cosmo,
-                                           const float u) {
-  error("Need implementing");
-}
+__attribute__((always_inline)) INLINE static void hydro_prepare_gradient(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct pressure_floor_props *pressure_floor) {}
 
 /**
- * @brief Gets the drifted physical internal energy of a particle
+ * @brief Resets the variables that are required for a gradient calculation.
  *
- * @param p The particle of interest.
- * @param cosmo Cosmology data structure
+ * This function is called after hydro_prepare_gradient.
+ * Nothing to do in this scheme as the gradient loop is not used.
  *
- * @return The physical internal energy
+ * @param p The particle to act upon.
+ * @param xp The extended particle data to act upon.
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static float
-hydro_get_drifted_physical_internal_energy(const struct part* p,
-                                           const struct cosmology* cosmo) {
-  error("Need implementing");
+__attribute__((always_inline)) INLINE static void hydro_reset_gradient(
+    struct part *restrict p) {}
 
-  return 0;
-}
+/**
+ * @brief Finishes the gradient calculation.
+ *
+ * Nothing to do in this scheme as the gradient loop is not used.
+ *
+ * @param p The particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void hydro_end_gradient(
+    struct part *p) {}
 
 /**
- * @brief Gets the drifted physical entropy of a particle
+ * @brief Sets all particle fields to sensible values when the #part has 0 ngbs.
  *
- * @param p The particle of interest.
- * @param cosmo Cosmology data structure
+ * In the desperate case where a particle has no neighbours (likely because
+ * of the h_max ceiling), set the particle fields to something sensible to avoid
+ * NaNs in the next calculations.
  *
- * @return The physical entropy
+ * @param p The particle to act upon
+ * @param xp The extended particle data to act upon
+ * @param cosmo The cosmological model.
  */
-__attribute__((always_inline)) INLINE static float
-hydro_get_drifted_physical_entropy(const struct part* p,
-                                   const struct cosmology* cosmo) {
-  error("Need implementing");
+__attribute__((always_inline)) INLINE static void hydro_part_has_no_neighbours(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo) {}
 
-  return 0;
-}
+/**
+ * @brief Prepare a particle for the force calculation.
+ *
+ * This function is called in the ghost task to convert some quantities coming
+ * from the density loop over neighbours into quantities ready to be used in the
+ * force loop over neighbours. Quantities are typically read from the density
+ * sub-structure and written to the force sub-structure.
+ * Examples of calculations done here include the calculation of viscosity term
+ * constants, thermal conduction terms, hydro conversions, etc.
+ *
+ * @param p The particle to act upon
+ * @param xp The extended particle data to act upon
+ * @param cosmo The current cosmological model.
+ * @param hydro_props Hydrodynamic properties.
+ * @param dt_alpha The time-step used to evolve non-cosmological quantities such
+ *                 as the artificial viscosity.
+ * @param dt_therm The time-step used to evolve hydrodynamical quantities.
+ */
+__attribute__((always_inline)) INLINE static void hydro_prepare_force(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct pressure_floor_props *pressure_floor, const float dt_alpha,
+    const float dt_therm) {}
 
 /**
- * @brief Update the value of the viscosity alpha for the scheme.
+ * @brief Reset acceleration fields of a particle
  *
- * @param p the particle of interest
- * @param alpha the new value for the viscosity coefficient.
+ * Resets all hydro acceleration and time derivative fields in preparation
+ * for the sums taking  place in the various force tasks.
+ *
+ * @param p The particle to act upon
  */
-__attribute__((always_inline)) INLINE static void hydro_set_viscosity_alpha(
-    struct part* restrict p, float alpha) {
-  /* Purposefully left empty */
-}
+__attribute__((always_inline)) INLINE static void hydro_reset_acceleration(
+    struct part *restrict p) {}
 
 /**
- * @brief Update the value of the viscosity alpha to the
- *        feedback reset value for the scheme.
+ * @brief Sets the values to be predicted in the drifts to their values at a
+ * kick time
  *
- * @param p the particle of interest
+ * @param p The particle.
+ * @param xp The extended data of this particle.
+ * @param cosmo The cosmological model
  */
-__attribute__((always_inline)) INLINE static void
-hydro_diffusive_feedback_reset(struct part* restrict p) {
-  /* Purposefully left empty */
-}
+__attribute__((always_inline)) INLINE static void hydro_reset_predicted_values(
+    struct part *restrict p, const struct xpart *restrict xp,
+    const struct cosmology *cosmo,
+    const struct pressure_floor_props *pressure_floor) {}
 
 /**
- * @brief Returns the comoving pressure of a particle
+ * @brief Predict additional particle fields forward in time when drifting
  *
- * @param p The particle of interest.
+ * Additional hydrodynamic quantites are drifted forward in time here. These
+ * include thermal quantities (thermal energy or total energy or entropy, ...).
+ *
+ * Note the different time-step sizes used for the different quantities as they
+ * include cosmological factors.
+ *
+ * @param p The particle.
+ * @param xp The extended data of the particle.
+ * @param dt_drift The drift time-step for positions.
+ * @param dt_therm The drift time-step for thermal quantities.
+ * @param dt_kick_grav The time-step for gravity quantities.
  * @param cosmo The cosmological model.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param floor_props The properties of the entropy floor.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_physical_pressure(
-    const struct part* restrict p, const struct cosmology* cosmo) {
+__attribute__((always_inline)) INLINE static void hydro_predict_extra(
+    struct part *restrict p, const struct xpart *restrict xp, float dt_drift,
+    float dt_therm, float dt_kick_grav, const struct cosmology *cosmo,
+    const struct hydro_props *hydro_props,
+    const struct entropy_floor_properties *floor_props,
+    const struct pressure_floor_props *pressure_floor) {}
 
-  return cosmo->a_factor_pressure * p->primitives.P;
-}
+/**
+ * @brief Finishes the force calculation.
+ *
+ * Multiplies the force and accelerations by the appropiate constants
+ * and add the self-contribution term. In most cases, there is little
+ * to do here.
+ *
+ * Cosmological terms are also added/multiplied here.
+ *
+ * @param p The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void hydro_end_force(
+    struct part *restrict p, const struct cosmology *cosmo) {}
 
 /**
- * @brief Returns the physical density of a particle
+ * @brief Kick the additional variables
  *
- * @param p The particle of interest
+ * Additional hydrodynamic quantites are kicked forward in time here. These
+ * include thermal quantities (thermal energy or total energy or entropy, ...).
+ *
+ * @param p The particle to act upon.
+ * @param xp The particle extended data to act upon.
+ * @param dt_therm The time-step for this kick (for thermodynamic quantities).
+ * @param dt_grav The time-step for this kick (for gravity quantities).
+ * @param dt_grav_mesh The time-step for this kick (mesh gravity).
+ * @param dt_hydro The time-step for this kick (for hydro quantities).
+ * @param dt_kick_corr The time-step for this kick (for gravity corrections).
  * @param cosmo The cosmological model.
+ * @param hydro_props The constants used in the scheme.
+ * @param floor_props The properties of the entropy floor.
  */
-__attribute__((always_inline)) INLINE static float hydro_get_physical_density(
-    const struct part* restrict p, const struct cosmology* cosmo) {
+__attribute__((always_inline)) INLINE static void hydro_kick_extra(
+    struct part *restrict p, struct xpart *restrict xp, float dt_therm,
+    float dt_grav, float dt_grav_mesh, float dt_hydro, float dt_kick_corr,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct entropy_floor_properties *floor_props) {}
 
-  return cosmo->a3_inv * p->primitives.rho;
+/**
+ * @brief Converts hydro quantity of a particle at the start of a run
+ *
+ * This function is called once at the end of the engine_init_particle()
+ * routine (at the start of a calculation) after the densities of
+ * particles have been computed.
+ * This can be used to convert internal energy into entropy for instance.
+ *
+ * @param p The particle to act upon
+ * @param xp The extended particle to act upon
+ * @param cosmo The cosmological model.
+ * @param hydro_props The constants used in the scheme.
+ */
+__attribute__((always_inline)) INLINE static void hydro_convert_quantities(
+    struct part *restrict p, struct xpart *restrict xp,
+    const struct cosmology *cosmo, const struct hydro_props *hydro_props,
+    const struct pressure_floor_props *pressure_floor) {}
+
+/**
+ * @brief Initialises the particles for the first time
+ *
+ * This function is called only once just after the ICs have been
+ * read in to do some conversions or assignments between the particle
+ * and extended particle fields.
+ *
+ * @param p The particle to act upon
+ * @param xp The extended particle data to act upon
+ */
+__attribute__((always_inline)) INLINE static void hydro_first_init_part(
+    struct part *restrict p, struct xpart *restrict xp) {
+
+  p->time_bin = 0;
+  xp->v_full[0] = p->v[0];
+  xp->v_full[1] = p->v[1];
+  xp->v_full[2] = p->v[2];
+  xp->a_grav[0] = 0.f;
+  xp->a_grav[1] = 0.f;
+  xp->a_grav[2] = 0.f;
+
+  hydro_init_part(p, NULL);
 }
 
+/**
+ * @brief Overwrite the initial internal energy of a particle.
+ *
+ * Note that in the cases where the thermodynamic variable is not
+ * internal energy but gets converted later, we must overwrite that
+ * field. The conversion to the actual variable happens later after
+ * the initial fake time-step.
+ *
+ * @param p The #part to write to.
+ * @param u_init The new initial internal energy.
+ */
+__attribute__((always_inline)) INLINE static void
+hydro_set_init_internal_energy(struct part *p, float u_init) {}
+
 /**
  * @brief Operations performed when a particle gets removed from the
  * simulation volume.
  *
  * @param p The particle.
  * @param xp The extended particle data.
- * @param time The simulation time.
  */
 __attribute__((always_inline)) INLINE static void hydro_remove_part(
-    const struct part* p, const struct xpart* xp, const double time) {}
+    const struct part *p, const struct xpart *xp, const double time) {}
 
-#endif /* SWIFT_SHADOWSWIFT_HYDRO_H */
+#endif /* SWIFT_MINIMAL_HYDRO_H */
diff --git a/src/hydro/Shadowswift/hydro_debug.h b/src/hydro/Shadowswift/hydro_debug.h
index 91ea90ef82880e51e0947090e48f2cb3c6927225..0ca04085fc465445ae1d4c4ec6c9d5fefb3fa701 100644
--- a/src/hydro/Shadowswift/hydro_debug.h
+++ b/src/hydro/Shadowswift/hydro_debug.h
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2016 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ * Copyright (c) 2020 Matthieu Schaller (schaller@strw.leideuniv.nl)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,56 +16,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
+#ifndef SWIFT_NONE_HYDRO_DEBUG_H
+#define SWIFT_NONE_HYDRO_DEBUG_H
+
+/**
+ * @file Shadowswift/hydro_debug.h
+ * @brief Empty implementation.
+ */
 
 __attribute__((always_inline)) INLINE static void hydro_debug_particle(
     const struct part* p, const struct xpart* xp) {
-  warning(
-      "[PID%lld] x=[%.16e,%.16e,%.16e], "
-      "v=[%.3e,%.3e,%.3e], "
-      "a=[%.3e,%.3e,%.3e], "
-      "time_bin=%d, "
-      "wakeup=%d, "
-      "h=%.3e, "
-      "primitives={"
-      "v=[%.3e,%.3e,%.3e], "
-      "rho=%.3e, "
-      "P=%.3e, "
-      "gradients={"
-      "rho=[%.3e,%.3e,%.3e], "
-      "v=[[%.3e,%.3e,%.3e],[%.3e,%.3e,%.3e],[%.3e,%.3e,%.3e]], "
-      "P=[%.3e,%.3e,%.3e]}, "
-      "limiter={"
-      "rho=[%.3e,%.3e], "
-      "v=[[%.3e,%.3e],[%.3e,%.3e],[%.3e,%.3e]], "
-      "P=[%.3e,%.3e], "
-      "maxr=%.3e}}, "
-      "conserved={"
-      "momentum=[%.3e,%.3e,%.3e], "
-      "mass=%.3e, "
-      "energy=%.3e}, "
-      "timestepvars={"
-      "vmax=%.3e}, "
-      "density={"
-      "wcount_dh=%.3e, "
-      "wcount=%.3e}",
-      p->id, p->x[0], p->x[1], p->x[2], p->v[0], p->v[1], p->v[2],
-      p->a_hydro[0], p->a_hydro[1], p->a_hydro[2], p->time_bin,
-      p->limiter_data.wakeup, p->h, p->primitives.v[0], p->primitives.v[1],
-      p->primitives.v[2], p->primitives.rho, p->primitives.P,
-      p->primitives.gradients.rho[0], p->primitives.gradients.rho[1],
-      p->primitives.gradients.rho[2], p->primitives.gradients.v[0][0],
-      p->primitives.gradients.v[0][1], p->primitives.gradients.v[0][2],
-      p->primitives.gradients.v[1][0], p->primitives.gradients.v[1][1],
-      p->primitives.gradients.v[1][2], p->primitives.gradients.v[2][0],
-      p->primitives.gradients.v[2][1], p->primitives.gradients.v[2][2],
-      p->primitives.gradients.P[0], p->primitives.gradients.P[1],
-      p->primitives.gradients.P[2], p->primitives.limiter.rho[0],
-      p->primitives.limiter.rho[1], p->primitives.limiter.v[0][0],
-      p->primitives.limiter.v[0][1], p->primitives.limiter.v[1][0],
-      p->primitives.limiter.v[1][1], p->primitives.limiter.v[2][0],
-      p->primitives.limiter.v[2][1], p->primitives.limiter.P[0],
-      p->primitives.limiter.P[1], p->primitives.limiter.maxr,
-      p->conserved.momentum[0], p->conserved.momentum[1],
-      p->conserved.momentum[2], p->conserved.mass, p->conserved.energy,
-      p->timestepvars.vmax, p->density.wcount_dh, p->density.wcount);
+  error("Empty implementation");
 }
+
+#endif /* SWIFT_NONE_HYDRO_DEBUG_H */
diff --git a/src/hydro/Shadowswift/hydro_gradients.h b/src/hydro/Shadowswift/hydro_gradients.h
deleted file mode 100644
index 4e7a9911d8d4fc586fe7a56687dd4c4ae9ec8de2..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/hydro_gradients.h
+++ /dev/null
@@ -1,163 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#ifndef SWIFT_HYDRO_GRADIENTS_H
-#define SWIFT_HYDRO_GRADIENTS_H
-
-#include "hydro_slope_limiters.h"
-
-#if defined(SHADOWFAX_GRADIENTS)
-
-#define HYDRO_GRADIENT_IMPLEMENTATION "Shadowfax gradients (Springel 2010)"
-#include "hydro_gradients_shadowfax.h"
-
-#else
-
-/* No gradients. Perfectly acceptable, but we have to provide empty functions */
-#define HYDRO_GRADIENT_IMPLEMENTATION "No gradients (first order scheme)"
-
-/**
- * @brief Initialize gradient variables
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_gradients_init(
-    struct part* p) {}
-
-/**
- * @brief Gradient calculations done during the neighbour loop
- *
- * @param r2 Squared distance between the two particles.
- * @param dx Distance vector (pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
- */
-__attribute__((always_inline)) INLINE static void hydro_gradients_collect(
-    float r2, const float* dx, float hi, float hj, struct part* restrict pi,
-    struct part* restrict pj) {}
-
-/**
- * @brief Gradient calculations done during the neighbour loop: non-symmetric
- * version
- *
- * @param r2 Squared distance between the two particles.
- * @param dx Distance vector (pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_gradients_nonsym_collect(float r2, const float* dx, float hi, float hj,
-                               struct part* restrict pi,
-                               const struct part* restrict pj) {}
-
-/**
- * @brief Finalize the gradient variables after all data have been collected
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_gradients_finalize(
-    struct part* p) {}
-
-#endif
-
-/**
- * @brief Gradients reconstruction. Is the same for all gradient types (although
- * gradients_none does nothing, since all gradients are zero -- are they?).
- */
-__attribute__((always_inline)) INLINE static void hydro_gradients_predict(
-    struct part* pi, struct part* pj, float hi, float hj, const float* dx,
-    float r, float* xij_i, float* Wi, float* Wj) {
-
-  float dWi[5], dWj[5];
-  float xij_j[3];
-
-  /* xij_j = real_midpoint - pj->x
-           = xij_i + pi->x - pj->x
-           = xij_i + dx */
-  xij_j[0] = xij_i[0] + dx[0];
-  xij_j[1] = xij_i[1] + dx[1];
-  xij_j[2] = xij_i[2] + dx[2];
-
-  dWi[0] = pi->primitives.gradients.rho[0] * xij_i[0] +
-           pi->primitives.gradients.rho[1] * xij_i[1] +
-           pi->primitives.gradients.rho[2] * xij_i[2];
-  dWi[1] = pi->primitives.gradients.v[0][0] * xij_i[0] +
-           pi->primitives.gradients.v[0][1] * xij_i[1] +
-           pi->primitives.gradients.v[0][2] * xij_i[2];
-  dWi[2] = pi->primitives.gradients.v[1][0] * xij_i[0] +
-           pi->primitives.gradients.v[1][1] * xij_i[1] +
-           pi->primitives.gradients.v[1][2] * xij_i[2];
-  dWi[3] = pi->primitives.gradients.v[2][0] * xij_i[0] +
-           pi->primitives.gradients.v[2][1] * xij_i[1] +
-           pi->primitives.gradients.v[2][2] * xij_i[2];
-  dWi[4] = pi->primitives.gradients.P[0] * xij_i[0] +
-           pi->primitives.gradients.P[1] * xij_i[1] +
-           pi->primitives.gradients.P[2] * xij_i[2];
-
-  dWj[0] = pj->primitives.gradients.rho[0] * xij_j[0] +
-           pj->primitives.gradients.rho[1] * xij_j[1] +
-           pj->primitives.gradients.rho[2] * xij_j[2];
-  dWj[1] = pj->primitives.gradients.v[0][0] * xij_j[0] +
-           pj->primitives.gradients.v[0][1] * xij_j[1] +
-           pj->primitives.gradients.v[0][2] * xij_j[2];
-  dWj[2] = pj->primitives.gradients.v[1][0] * xij_j[0] +
-           pj->primitives.gradients.v[1][1] * xij_j[1] +
-           pj->primitives.gradients.v[1][2] * xij_j[2];
-  dWj[3] = pj->primitives.gradients.v[2][0] * xij_j[0] +
-           pj->primitives.gradients.v[2][1] * xij_j[1] +
-           pj->primitives.gradients.v[2][2] * xij_j[2];
-  dWj[4] = pj->primitives.gradients.P[0] * xij_j[0] +
-           pj->primitives.gradients.P[1] * xij_j[1] +
-           pj->primitives.gradients.P[2] * xij_j[2];
-
-  hydro_slope_limit_face(Wi, Wj, dWi, dWj, xij_i, xij_j, r);
-
-  Wi[0] += dWi[0];
-  Wi[1] += dWi[1];
-  Wi[2] += dWi[2];
-  Wi[3] += dWi[3];
-  Wi[4] += dWi[4];
-
-  Wj[0] += dWj[0];
-  Wj[1] += dWj[1];
-  Wj[2] += dWj[2];
-  Wj[3] += dWj[3];
-  Wj[4] += dWj[4];
-
-  /* Sanity check: if density or pressure becomes negative after the
-     interpolation, just reset them */
-  if (Wi[0] < 0.0f) {
-    Wi[0] -= dWi[0];
-  }
-  if (Wi[4] < 0.0f) {
-    Wi[4] -= dWi[4];
-  }
-  if (Wj[0] < 0.0f) {
-    Wj[0] -= dWj[0];
-  }
-  if (Wj[4] < 0.0f) {
-    Wj[4] -= dWj[4];
-  }
-}
-
-#endif  // SWIFT_HYDRO_GRADIENTS_H
diff --git a/src/hydro/Shadowswift/hydro_gradients_shadowfax.h b/src/hydro/Shadowswift/hydro_gradients_shadowfax.h
deleted file mode 100644
index d131731907806536e86a03921b1c701f287077f1..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/hydro_gradients_shadowfax.h
+++ /dev/null
@@ -1,218 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#include "voronoi_algorithm.h"
-
-/**
- * @brief Initialize gradient variables
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_gradients_init(
-    struct part *p) {
-
-  p->primitives.gradients.rho[0] = 0.0f;
-  p->primitives.gradients.rho[1] = 0.0f;
-  p->primitives.gradients.rho[2] = 0.0f;
-
-  p->primitives.gradients.v[0][0] = 0.0f;
-  p->primitives.gradients.v[0][1] = 0.0f;
-  p->primitives.gradients.v[0][2] = 0.0f;
-
-  p->primitives.gradients.v[1][0] = 0.0f;
-  p->primitives.gradients.v[1][1] = 0.0f;
-  p->primitives.gradients.v[1][2] = 0.0f;
-
-  p->primitives.gradients.v[2][0] = 0.0f;
-  p->primitives.gradients.v[2][1] = 0.0f;
-  p->primitives.gradients.v[2][2] = 0.0f;
-
-  p->primitives.gradients.P[0] = 0.0f;
-  p->primitives.gradients.P[1] = 0.0f;
-  p->primitives.gradients.P[2] = 0.0f;
-
-  hydro_slope_limit_cell_init(p);
-}
-
-/**
- * @brief Add the gradient estimate for a single quantity due to a particle pair
- * to the total gradient for that quantity
- *
- * This corresponds to one term of equation (21) in Springel (2010).
- *
- * @param qL Value of the quantity on the left.
- * @param qR Value of the quantity on the right.
- * @param cLR Vector pointing from the midpoint of the particle pair to the
- * geometrical centroid of the face in between the particles.
- * @param xLR Vector pointing from the right particle to the left particle.
- * @param A Surface area of the face in between the particles.
- * @param grad Current value of the gradient for the quantity (is updated).
- */
-__attribute__((always_inline)) INLINE void hydro_gradients_single_quantity(
-    float qL, float qR, float *cLR, const float *xLR, float rLR, float A,
-    float *grad) {
-
-  grad[0] += A * ((qR - qL) * cLR[0] / rLR - 0.5f * (qL + qR) * xLR[0] / rLR);
-  grad[1] += A * ((qR - qL) * cLR[1] / rLR - 0.5f * (qL + qR) * xLR[1] / rLR);
-  grad[2] += A * ((qR - qL) * cLR[2] / rLR - 0.5f * (qL + qR) * xLR[2] / rLR);
-}
-
-/**
- * @brief Gradient calculations done during the neighbour loop
- *
- * @param r2 Squared distance between the two particles.
- * @param dx Distance vector (pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
- */
-__attribute__((always_inline)) INLINE static void hydro_gradients_collect(
-    float r2, const float *dx, float hi, float hj, struct part *pi,
-    struct part *pj) {
-
-  float A, midpoint[3];
-
-  A = voronoi_get_face(&pi->cell, pj->id, midpoint);
-  if (!A) {
-    /* particle is not a cell neighbour: do nothing */
-    return;
-  }
-
-  float c[3];
-  /* midpoint is relative w.r.t. pi->x, as is dx */
-  /* c is supposed to be the vector pointing from the midpoint of pi and pj to
-     the midpoint of the face between pi and pj:
-       c = real_midpoint - 0.5*(pi+pj)
-         = midpoint + pi - 0.5*(2*pi - dx)
-         = midpoint + 0.5*dx */
-  c[0] = midpoint[0] + 0.5f * dx[0];
-  c[1] = midpoint[1] + 0.5f * dx[1];
-  c[2] = midpoint[2] + 0.5f * dx[2];
-
-  float r = sqrtf(r2);
-  hydro_gradients_single_quantity(pi->primitives.rho, pj->primitives.rho, c, dx,
-                                  r, A, pi->primitives.gradients.rho);
-  hydro_gradients_single_quantity(pi->primitives.v[0], pj->primitives.v[0], c,
-                                  dx, r, A, pi->primitives.gradients.v[0]);
-  hydro_gradients_single_quantity(pi->primitives.v[1], pj->primitives.v[1], c,
-                                  dx, r, A, pi->primitives.gradients.v[1]);
-  hydro_gradients_single_quantity(pi->primitives.v[2], pj->primitives.v[2], c,
-                                  dx, r, A, pi->primitives.gradients.v[2]);
-  hydro_gradients_single_quantity(pi->primitives.P, pj->primitives.P, c, dx, r,
-                                  A, pi->primitives.gradients.P);
-
-  hydro_slope_limit_cell_collect(pi, pj, r);
-
-  float mindx[3];
-  mindx[0] = -dx[0];
-  mindx[1] = -dx[1];
-  mindx[2] = -dx[2];
-  hydro_gradients_single_quantity(pj->primitives.rho, pi->primitives.rho, c,
-                                  mindx, r, A, pj->primitives.gradients.rho);
-  hydro_gradients_single_quantity(pj->primitives.v[0], pi->primitives.v[0], c,
-                                  mindx, r, A, pj->primitives.gradients.v[0]);
-  hydro_gradients_single_quantity(pj->primitives.v[1], pi->primitives.v[1], c,
-                                  mindx, r, A, pj->primitives.gradients.v[1]);
-  hydro_gradients_single_quantity(pj->primitives.v[2], pi->primitives.v[2], c,
-                                  mindx, r, A, pj->primitives.gradients.v[2]);
-  hydro_gradients_single_quantity(pj->primitives.P, pi->primitives.P, c, mindx,
-                                  r, A, pj->primitives.gradients.P);
-
-  hydro_slope_limit_cell_collect(pj, pi, r);
-}
-
-/**
- * @brief Gradient calculations done during the neighbour loop
- *
- * @param r2 Squared distance between the two particles.
- * @param dx Distance vector (pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_gradients_nonsym_collect(float r2, const float *dx, float hi, float hj,
-                               struct part *pi, const struct part *pj) {
-
-  float A, midpoint[3];
-
-  A = voronoi_get_face(&pi->cell, pj->id, midpoint);
-  if (!A) {
-    /* particle is not a cell neighbour: do nothing */
-    return;
-  }
-
-  float c[3];
-  /* midpoint is relative w.r.t. pi->x, as is dx */
-  /* c is supposed to be the vector pointing from the midpoint of pi and pj to
-     the midpoint of the face between pi and pj:
-       c = real_midpoint - 0.5*(pi+pj)
-         = midpoint + pi - 0.5*(2*pi - dx)
-         = midpoint + 0.5*dx */
-  c[0] = midpoint[0] + 0.5f * dx[0];
-  c[1] = midpoint[1] + 0.5f * dx[1];
-  c[2] = midpoint[2] + 0.5f * dx[2];
-
-  float r = sqrtf(r2);
-  hydro_gradients_single_quantity(pi->primitives.rho, pj->primitives.rho, c, dx,
-                                  r, A, pi->primitives.gradients.rho);
-  hydro_gradients_single_quantity(pi->primitives.v[0], pj->primitives.v[0], c,
-                                  dx, r, A, pi->primitives.gradients.v[0]);
-  hydro_gradients_single_quantity(pi->primitives.v[1], pj->primitives.v[1], c,
-                                  dx, r, A, pi->primitives.gradients.v[1]);
-  hydro_gradients_single_quantity(pi->primitives.v[2], pj->primitives.v[2], c,
-                                  dx, r, A, pi->primitives.gradients.v[2]);
-  hydro_gradients_single_quantity(pi->primitives.P, pj->primitives.P, c, dx, r,
-                                  A, pi->primitives.gradients.P);
-
-  hydro_slope_limit_cell_collect(pi, pj, r);
-}
-
-/**
- * @brief Finalize the gradient variables after all data have been collected
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_gradients_finalize(
-    struct part *p) {
-
-  float volume = p->cell.volume;
-
-  p->primitives.gradients.rho[0] /= volume;
-  p->primitives.gradients.rho[1] /= volume;
-  p->primitives.gradients.rho[2] /= volume;
-
-  p->primitives.gradients.v[0][0] /= volume;
-  p->primitives.gradients.v[0][1] /= volume;
-  p->primitives.gradients.v[0][2] /= volume;
-  p->primitives.gradients.v[1][0] /= volume;
-  p->primitives.gradients.v[1][1] /= volume;
-  p->primitives.gradients.v[1][2] /= volume;
-  p->primitives.gradients.v[2][0] /= volume;
-  p->primitives.gradients.v[2][1] /= volume;
-  p->primitives.gradients.v[2][2] /= volume;
-
-  p->primitives.gradients.P[0] /= volume;
-  p->primitives.gradients.P[1] /= volume;
-  p->primitives.gradients.P[2] /= volume;
-
-  hydro_slope_limit_cell(p);
-}
diff --git a/src/hydro/Shadowswift/hydro_iact.h b/src/hydro/Shadowswift/hydro_iact.h
index e7b93fd9a6b7154e83131dff9ababf938c6ce9f9..d4459b9dfbc0db04055db068f137c38ded0026e7 100644
--- a/src/hydro/Shadowswift/hydro_iact.h
+++ b/src/hydro/Shadowswift/hydro_iact.h
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
+ * Copyright (c) 2020 Matthieu Schaller (schaller@strw.leideuniv.nl)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,336 +16,125 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
+#ifndef SWIFT_NONE_HYDRO_IACT_H
+#define SWIFT_NONE_HYDRO_IACT_H
+
+/**
+ * @file Shadowswift/hydro_iact.h
+ * @brief Empty implementation
+ */
 
 #include "adiabatic_index.h"
-#include "hydro_gradients.h"
-#include "riemann.h"
-#include "voronoi_algorithm.h"
+#include "hydro_parameters.h"
+#include "minmax.h"
 
 /**
- * @brief Calculate the Voronoi cell by interacting particle pi and pj
- *
- * This method wraps around voronoi_cell_interact().
- *
- * @param r2 Squared distance between particle i and particle j.
- * @param dx Distance vector between the particles (dx = pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
+ * @brief Density interaction between two particles.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_density(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, struct part *restrict pj, const float a,
-    const float H) {
-
-  float mindx[3];
-
-  voronoi_cell_interact(&pi->cell, dx, pj->id);
-  mindx[0] = -dx[0];
-  mindx[1] = -dx[1];
-  mindx[2] = -dx[2];
-  voronoi_cell_interact(&pj->cell, mindx, pi->id);
-}
+    const float H) {}
 
 /**
- * @brief Calculate the Voronoi cell by interacting particle pi with pj
- *
- * This method wraps around voronoi_cell_interact().
- *
- * @param r2 Squared distance between particle i and particle j.
- * @param dx Distance vector between the particles (dx = pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
+ * @brief Density interaction between two particles (non-symmetric).
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle (not updated).
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_density(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, const struct part *restrict pj, const float a,
-    const float H) {
-
-  voronoi_cell_interact(&pi->cell, dx, pj->id);
-}
+    const float H) {}
 
 /**
  * @brief Calculate the gradient interaction between particle i and particle j
  *
- * This method wraps around hydro_gradients_collect, which can be an empty
- * method, in which case no gradients are used.
+ * Nothing to do here in this scheme.
  *
- * @param r2 Squared distance between particle i and particle j.
- * @param dx Distance vector between the particles (dx = pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
+ * @param r2 Comoving squared distance between particle i and particle j.
+ * @param dx Comoving distance vector between the particles (dx = pi->x -
+ * pj->x).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
  * @param pi Particle i.
  * @param pj Particle j.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_gradient(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, struct part *restrict pj, const float a,
-    const float H) {
-
-  hydro_gradients_collect(r2, dx, hi, hj, pi, pj);
-}
+    const float H) {}
 
 /**
  * @brief Calculate the gradient interaction between particle i and particle j:
  * non-symmetric version
  *
- * This method wraps around hydro_gradients_nonsym_collect, which can be an
- * empty method, in which case no gradients are used.
+ * Nothing to do here in this scheme.
  *
- * @param r2 Squared distance between particle i and particle j.
- * @param dx Distance vector between the particles (dx = pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
+ * @param r2 Comoving squared distance between particle i and particle j.
+ * @param dx Comoving distance vector between the particles (dx = pi->x -
+ * pj->x).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
  * @param pi Particle i.
  * @param pj Particle j.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_gradient(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, struct part *restrict pj, const float a,
-    const float H) {
-
-  hydro_gradients_nonsym_collect(r2, dx, hi, hj, pi, pj);
-}
+    const float H) {}
 
 /**
- * @brief Common part of the flux calculation between particle i and j
- *
- * Since the only difference between the symmetric and non-symmetric version
- * of the flux calculation  is in the update of the conserved variables at the
- * very end (which is not done for particle j if mode is 0 and particle j is
- * active), both runner_iact_force and runner_iact_nonsym_force call this
- * method, with an appropriate mode.
- *
- * This method retrieves the oriented surface area and face midpoint for the
- * Voronoi face between pi and pj (if it exists). It uses the midpoint position
- * to reconstruct the primitive quantities (if gradients are used) at the face
- * and then uses the face quantities to estimate a flux through the face using
- * a Riemann solver.
- *
- * This method also calculates the maximal velocity used to calculate the time
- * step.
- *
- * @param r2 Squared distance between particle i and particle j.
- * @param dx Distance vector between the particles (dx = pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
- */
-__attribute__((always_inline)) INLINE static void runner_iact_fluxes_common(
-    const float r2, const float dx[3], const float hi, const float hj,
-    struct part *restrict pi, struct part *restrict pj, int mode, const float a,
-    const float H) {
-
-  float r = sqrtf(r2);
-  int k;
-  float A;
-  float xij_i[3];
-  float vmax, dvdotdx;
-  float vi[3], vj[3], vij[3];
-  float Wi[5], Wj[5];
-  float n_unit[3];
-
-  A = voronoi_get_face(&pi->cell, pj->id, xij_i);
-  if (A == 0.0f) {
-    /* this neighbour does not share a face with the cell, return */
-    return;
-  }
-
-  /* Initialize local variables */
-  for (k = 0; k < 3; k++) {
-    vi[k] = pi->force.v_full[k]; /* particle velocities */
-    vj[k] = pj->force.v_full[k];
-  }
-  Wi[0] = pi->primitives.rho;
-  Wi[1] = pi->primitives.v[0];
-  Wi[2] = pi->primitives.v[1];
-  Wi[3] = pi->primitives.v[2];
-  Wi[4] = pi->primitives.P;
-  Wj[0] = pj->primitives.rho;
-  Wj[1] = pj->primitives.v[0];
-  Wj[2] = pj->primitives.v[1];
-  Wj[3] = pj->primitives.v[2];
-  Wj[4] = pj->primitives.P;
-
-  /* calculate the maximal signal velocity */
-  vmax = 0.0f;
-  if (Wi[0] > 0.) {
-    vmax += gas_soundspeed_from_pressure(Wi[0], Wi[4]);
-  }
-
-  if (Wj[0] > 0.) {
-    vmax += gas_soundspeed_from_pressure(Wj[0], Wj[4]);
-  }
-
-  dvdotdx = (Wi[1] - Wj[1]) * dx[0] + (Wi[2] - Wj[2]) * dx[1] +
-            (Wi[3] - Wj[3]) * dx[2];
-  if (dvdotdx > 0.) {
-    vmax -= dvdotdx / r;
-  }
-
-  pi->timestepvars.vmax = fmaxf(pi->timestepvars.vmax, vmax);
-  if (mode == 1) {
-    pj->timestepvars.vmax = fmaxf(pj->timestepvars.vmax, vmax);
-  }
-
-  /* compute the normal vector of the interface */
-  for (k = 0; k < 3; ++k) {
-    n_unit[k] = -dx[k] / r;
-  }
-
-  /* Compute interface velocity */
-  float fac = (vi[0] - vj[0]) * (xij_i[0] + 0.5f * dx[0]) +
-              (vi[1] - vj[1]) * (xij_i[1] + 0.5f * dx[1]) +
-              (vi[2] - vj[2]) * (xij_i[2] + 0.5f * dx[2]);
-  fac /= r;
-  vij[0] = 0.5f * (vi[0] + vj[0]) - fac * dx[0];
-  vij[1] = 0.5f * (vi[1] + vj[1]) - fac * dx[1];
-  vij[2] = 0.5f * (vi[2] + vj[2]) - fac * dx[2];
-
-  /* Boost the primitive variables to the frame of reference of the interface */
-  /* Note that velocities are indices 1-3 in W */
-  Wi[1] -= vij[0];
-  Wi[2] -= vij[1];
-  Wi[3] -= vij[2];
-  Wj[1] -= vij[0];
-  Wj[2] -= vij[1];
-  Wj[3] -= vij[2];
-
-  hydro_gradients_predict(pi, pj, hi, hj, dx, r, xij_i, Wi, Wj);
-
-  /* we don't need to rotate, we can use the unit vector in the Riemann problem
-   * itself (see GIZMO) */
-
-  if (Wi[0] < 0.0f || Wj[0] < 0.0f || Wi[4] < 0.0f || Wj[4] < 0.0f) {
-    printf("WL: %g %g %g %g %g\n", pi->primitives.rho, pi->primitives.v[0],
-           pi->primitives.v[1], pi->primitives.v[2], pi->primitives.P);
-#ifdef USE_GRADIENTS
-    printf("dWL: %g %g %g %g %g\n", dWi[0], dWi[1], dWi[2], dWi[3], dWi[4]);
-#endif
-    printf("gradWL[0]: %g %g %g\n", pi->primitives.gradients.rho[0],
-           pi->primitives.gradients.rho[1], pi->primitives.gradients.rho[2]);
-    printf("gradWL[1]: %g %g %g\n", pi->primitives.gradients.v[0][0],
-           pi->primitives.gradients.v[0][1], pi->primitives.gradients.v[0][2]);
-    printf("gradWL[2]: %g %g %g\n", pi->primitives.gradients.v[1][0],
-           pi->primitives.gradients.v[1][1], pi->primitives.gradients.v[1][2]);
-    printf("gradWL[3]: %g %g %g\n", pi->primitives.gradients.v[2][0],
-           pi->primitives.gradients.v[2][1], pi->primitives.gradients.v[2][2]);
-    printf("gradWL[4]: %g %g %g\n", pi->primitives.gradients.P[0],
-           pi->primitives.gradients.P[1], pi->primitives.gradients.P[2]);
-    printf("WL': %g %g %g %g %g\n", Wi[0], Wi[1], Wi[2], Wi[3], Wi[4]);
-    printf("WR: %g %g %g %g %g\n", pj->primitives.rho, pj->primitives.v[0],
-           pj->primitives.v[1], pj->primitives.v[2], pj->primitives.P);
-#ifdef USE_GRADIENTS
-    printf("dWR: %g %g %g %g %g\n", dWj[0], dWj[1], dWj[2], dWj[3], dWj[4]);
-#endif
-    printf("gradWR[0]: %g %g %g\n", pj->primitives.gradients.rho[0],
-           pj->primitives.gradients.rho[1], pj->primitives.gradients.rho[2]);
-    printf("gradWR[1]: %g %g %g\n", pj->primitives.gradients.v[0][0],
-           pj->primitives.gradients.v[0][1], pj->primitives.gradients.v[0][2]);
-    printf("gradWR[2]: %g %g %g\n", pj->primitives.gradients.v[1][0],
-           pj->primitives.gradients.v[1][1], pj->primitives.gradients.v[1][2]);
-    printf("gradWR[3]: %g %g %g\n", pj->primitives.gradients.v[2][0],
-           pj->primitives.gradients.v[2][1], pj->primitives.gradients.v[2][2]);
-    printf("gradWR[4]: %g %g %g\n", pj->primitives.gradients.P[0],
-           pj->primitives.gradients.P[1], pj->primitives.gradients.P[2]);
-    printf("WR': %g %g %g %g %g\n", Wj[0], Wj[1], Wj[2], Wj[3], Wj[4]);
-    error("Negative density or pressure!\n");
-  }
-
-  float totflux[5];
-  riemann_solve_for_flux(Wi, Wj, n_unit, vij, totflux);
-
-  /* Update conserved variables */
-  /* eqn. (16) */
-  pi->conserved.flux.mass -= A * totflux[0];
-  pi->conserved.flux.momentum[0] -= A * totflux[1];
-  pi->conserved.flux.momentum[1] -= A * totflux[2];
-  pi->conserved.flux.momentum[2] -= A * totflux[3];
-  pi->conserved.flux.energy -= A * totflux[4];
-
-#ifndef SHADOWFAX_TOTAL_ENERGY
-  float ekin = 0.5f * (pi->primitives.v[0] * pi->primitives.v[0] +
-                       pi->primitives.v[1] * pi->primitives.v[1] +
-                       pi->primitives.v[2] * pi->primitives.v[2]);
-  pi->conserved.flux.energy += A * totflux[1] * pi->primitives.v[0];
-  pi->conserved.flux.energy += A * totflux[2] * pi->primitives.v[1];
-  pi->conserved.flux.energy += A * totflux[3] * pi->primitives.v[2];
-  pi->conserved.flux.energy -= A * totflux[0] * ekin;
-#endif
-
-  /* here is how it works:
-     Mode will only be 1 if both particles are ACTIVE and they are in the same
-     cell. In this case, this method IS the flux calculation for particle j, and
-     we HAVE TO UPDATE it.
-     Mode 0 can mean several things: it can mean that particle j is INACTIVE, in
-     which case we NEED TO UPDATE it, since otherwise the flux is lost from the
-     system and the conserved variable is not conserved.
-     It can also mean that particle j sits in another cell and is ACTIVE. In
-     this case, the flux exchange for particle j is done TWICE and we SHOULD NOT
-     UPDATE particle j.
-     ==> we update particle j if (MODE IS 1) OR (j IS INACTIVE)
-  */
-  if (mode == 1 || pj->force.active == 0) {
-    pj->conserved.flux.mass += A * totflux[0];
-    pj->conserved.flux.momentum[0] += A * totflux[1];
-    pj->conserved.flux.momentum[1] += A * totflux[2];
-    pj->conserved.flux.momentum[2] += A * totflux[3];
-    pj->conserved.flux.energy += A * totflux[4];
-
-#ifndef SHADOWFAX_TOTAL_ENERGY
-    ekin = 0.5f * (pj->primitives.v[0] * pj->primitives.v[0] +
-                   pj->primitives.v[1] * pj->primitives.v[1] +
-                   pj->primitives.v[2] * pj->primitives.v[2]);
-    pj->conserved.flux.energy -= A * totflux[1] * pj->primitives.v[0];
-    pj->conserved.flux.energy -= A * totflux[2] * pj->primitives.v[1];
-    pj->conserved.flux.energy -= A * totflux[3] * pj->primitives.v[2];
-    pj->conserved.flux.energy += A * totflux[0] * ekin;
-#endif
-  }
-}
-
-/**
- * @brief Flux calculation between particle i and particle j
- *
- * This method calls runner_iact_fluxes_common with mode 1.
- *
- * @param r2 Squared distance between particle i and particle j.
- * @param dx Distance vector between the particles (dx = pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
+ * @brief Force interaction between two particles.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_force(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, struct part *restrict pj, const float a,
-    const float H) {
-
-  runner_iact_fluxes_common(r2, dx, hi, hj, pi, pj, 1, a, H);
-}
+    const float H) {}
 
 /**
- * @brief Flux calculation between particle i and particle j: non-symmetric
- * version
- *
- * This method calls runner_iact_fluxes_common with mode 0.
- *
- * @param r2 Squared distance between particle i and particle j.
- * @param dx Distance vector between the particles (dx = pi->x - pj->x).
- * @param hi Smoothing length of particle i.
- * @param hj Smoothing length of particle j.
- * @param pi Particle i.
- * @param pj Particle j.
+ * @brief Force interaction between two particles (non-symmetric).
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle (not updated).
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_force(
     const float r2, const float dx[3], const float hi, const float hj,
-    struct part *restrict pi, struct part *restrict pj, const float a,
-    const float H) {
+    struct part *restrict pi, const struct part *restrict pj, const float a,
+    const float H) {}
 
-  runner_iact_fluxes_common(r2, dx, hi, hj, pi, pj, 0, a, H);
-}
+#endif /* SWIFT_NONE_HYDRO_IACT_H */
diff --git a/src/hydro/Shadowswift/hydro_io.h b/src/hydro/Shadowswift/hydro_io.h
index baecc47af05dda85e644186fa9892bd25f052083..3ffb842f372d7fa127663c93718169c3c2e2637c 100644
--- a/src/hydro/Shadowswift/hydro_io.h
+++ b/src/hydro/Shadowswift/hydro_io.h
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
+ * Copyright (c) 2020 Matthieu Schaller (schaller@strw.leideuniv.nl)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,14 +16,19 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
+#ifndef SWIFT_NONE_HYDRO_IO_H
+#define SWIFT_NONE_HYDRO_IO_H
+
+/**
+ * @file Shadowswift/hydro_io.h
+ * @brief Empty implementation.
+ */
 
 #include "adiabatic_index.h"
-#include "equation_of_state.h"
 #include "hydro.h"
-#include "hydro_gradients.h"
-#include "hydro_slope_limiters.h"
+#include "hydro_parameters.h"
 #include "io_properties.h"
-#include "riemann.h"
+#include "kernel_hydro.h"
 
 /**
  * @brief Specifies which particle fields to read from a dataset
@@ -36,137 +41,32 @@ INLINE static void hydro_read_particles(struct part* parts,
                                         struct io_props* list,
                                         int* num_fields) {
 
-  *num_fields = 8;
-
-  /* List what we want to read */
-  list[0] = io_make_input_field("Coordinates", DOUBLE, 3, COMPULSORY,
-                                UNIT_CONV_LENGTH, parts, x);
-  list[1] = io_make_input_field("Velocities", FLOAT, 3, COMPULSORY,
-                                UNIT_CONV_SPEED, parts, v);
-  list[2] = io_make_input_field("Masses", FLOAT, 1, COMPULSORY, UNIT_CONV_MASS,
-                                parts, conserved.mass);
-  list[3] = io_make_input_field("SmoothingLength", FLOAT, 1, COMPULSORY,
-                                UNIT_CONV_LENGTH, parts, h);
-  list[4] = io_make_input_field("InternalEnergy", FLOAT, 1, COMPULSORY,
-                                UNIT_CONV_ENERGY_PER_UNIT_MASS, parts,
-                                conserved.energy);
-  list[5] = io_make_input_field("ParticleIDs", ULONGLONG, 1, COMPULSORY,
-                                UNIT_CONV_NO_UNITS, parts, id);
-  list[6] = io_make_input_field("Accelerations", FLOAT, 3, OPTIONAL,
-                                UNIT_CONV_ACCELERATION, parts, a_hydro);
-  list[7] = io_make_input_field("Density", FLOAT, 1, OPTIONAL,
-                                UNIT_CONV_DENSITY, parts, primitives.rho);
-}
-
-/**
- * @brief Get the internal energy of a particle
- *
- * @param e #engine.
- * @param p Particle.
- * @return Internal energy of the particle
- */
-INLINE static void convert_u(const struct engine* e, const struct part* p,
-                             const struct xpart* xp, float* ret) {
-  ret[0] = hydro_get_internal_energy(p);
-}
-
-/**
- * @brief Get the entropic function of a particle
- *
- * @param e #engine.
- * @param p Particle.
- * @return Entropic function of the particle
- */
-INLINE static void convert_A(const struct engine* e, const struct part* p,
-                             const struct xpart* xp, float* ret) {
-  ret[0] = hydro_get_entropy(p);
-}
-
-/**
- * @brief Get the total energy of a particle
- *
- * @param e #engine.
- * @param p Particle.
- * @return Total energy of the particle
- */
-INLINE static void convert_Etot(const struct engine* e, const struct part* p,
-                                const struct xpart* xp, float* ret) {
-#ifdef SHADOWFAX_TOTAL_ENERGY
-  return p->conserved.energy;
-#else
-  if (p->conserved.mass > 0.) {
-    float momentum2;
-
-    momentum2 = p->conserved.momentum[0] * p->conserved.momentum[0] +
-                p->conserved.momentum[1] * p->conserved.momentum[1] +
-                p->conserved.momentum[2] * p->conserved.momentum[2];
-
-    ret[0] = p->conserved.energy + 0.5f * momentum2 / p->conserved.mass;
-  } else {
-    ret[0] = 0.;
-  }
-#endif
+  *num_fields = 0;
 }
 
 INLINE static void convert_part_pos(const struct engine* e,
                                     const struct part* p,
                                     const struct xpart* xp, double* ret) {
-  const struct space* s = e->s;
-  if (s->periodic) {
-    ret[0] = box_wrap(p->x[0], 0.0, s->dim[0]);
-    ret[1] = box_wrap(p->x[1], 0.0, s->dim[1]);
-    ret[2] = box_wrap(p->x[2], 0.0, s->dim[2]);
-  } else {
-    ret[0] = p->x[0];
-    ret[1] = p->x[1];
-    ret[2] = p->x[2];
-  }
-  if (e->snapshot_use_delta_from_edge) {
-    ret[0] = min(ret[0], s->dim[0] - e->snapshot_delta_from_edge);
-    ret[1] = min(ret[1], s->dim[1] - e->snapshot_delta_from_edge);
-    ret[2] = min(ret[2], s->dim[2] - e->snapshot_delta_from_edge);
-  }
+  ret[0] = 0.;
+  ret[1] = 0.;
+  ret[2] = 0.;
+  error("Not implemented in the 'None' hydro scheme.");
 }
 
 INLINE static void convert_part_vel(const struct engine* e,
                                     const struct part* p,
                                     const struct xpart* xp, float* ret) {
-
-  const int with_cosmology = (e->policy & engine_policy_cosmology);
-  const struct cosmology* cosmo = e->cosmology;
-  const integertime_t ti_current = e->ti_current;
-  const double time_base = e->time_base;
-
-  const integertime_t ti_beg = get_integer_time_begin(ti_current, p->time_bin);
-  const integertime_t ti_end = get_integer_time_end(ti_current, p->time_bin);
-
-  /* Get time-step since the last kick */
-  float dt_kick_grav, dt_kick_hydro;
-  if (with_cosmology) {
-    dt_kick_grav = cosmology_get_grav_kick_factor(cosmo, ti_beg, ti_current);
-    dt_kick_grav -=
-        cosmology_get_grav_kick_factor(cosmo, ti_beg, (ti_beg + ti_end) / 2);
-    dt_kick_hydro = cosmology_get_hydro_kick_factor(cosmo, ti_beg, ti_current);
-    dt_kick_hydro -=
-        cosmology_get_hydro_kick_factor(cosmo, ti_beg, (ti_beg + ti_end) / 2);
-  } else {
-    dt_kick_grav = (ti_current - ((ti_beg + ti_end) / 2)) * time_base;
-    dt_kick_hydro = (ti_current - ((ti_beg + ti_end) / 2)) * time_base;
-  }
-
-  /* Extrapolate the velocites to the current time */
-  hydro_get_drifted_velocities(p, xp, dt_kick_hydro, dt_kick_grav, ret);
-
-  /* Conversion from internal units to peculiar velocities */
-  ret[0] *= cosmo->a_inv;
-  ret[1] *= cosmo->a_inv;
-  ret[2] *= cosmo->a_inv;
+  ret[0] = 0.f;
+  ret[1] = 0.f;
+  ret[2] = 0.f;
+  error("Not implemented in the 'None' hydro scheme.");
 }
 
 /**
  * @brief Specifies which particle fields to write to a dataset
  *
  * @param parts The particle array.
+ * @param xparts The extended particle array.
  * @param list The list of i/o properties to write.
  * @param num_fields The number of i/o fields to write.
  */
@@ -175,84 +75,14 @@ INLINE static void hydro_write_particles(const struct part* parts,
                                          struct io_props* list,
                                          int* num_fields) {
 
-  *num_fields = 13;
-
-  /* List what we want to write */
-  list[0] = io_make_output_field_convert_part(
-      "Coordinates", DOUBLE, 3, UNIT_CONV_LENGTH, 1.f, parts, xparts,
-      convert_part_pos, "Co-moving positions of the particles");
-
-  list[1] = io_make_output_field_convert_part(
-      "Velocities", FLOAT, 3, UNIT_CONV_SPEED, 0.f, parts, xparts,
-      convert_part_vel,
-      "Peculiar velocities of the stars. This is (a * dx/dt) where x is the "
-      "co-moving positions of the particles");
-
-  list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f, parts,
-                                 conserved.mass, "Masses of the particles");
-
-  list[3] = io_make_output_field(
-      "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, parts, h,
-      "Co-moving smoothing lengths (FWHM of the kernel) of the particles");
-
-  list[4] = io_make_output_field_convert_part(
-      "InternalEnergies", FLOAT, 1, UNIT_CONV_ENERGY_PER_UNIT_MASS,
-      -3.f * hydro_gamma_minus_one, parts, xparts, convert_u,
-      "Co-moving thermal energies per unit mass of the particles");
-
-  list[5] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           parts, id, "Unique IDs of the particles");
-
-  list[6] = io_make_output_field("Accelerations", FLOAT, 3,
-                                 UNIT_CONV_ACCELERATION, 1.f, parts, a_hydro,
-                                 "Accelerations of the particles(does not "
-                                 "work in non-cosmological runs).");
-
-  list[7] = io_make_output_field("Densities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f,
-                                 parts, primitives.rho,
-                                 "Co-moving mass densities of the particles");
-
-  list[8] =
-      io_make_output_field("Volumes", FLOAT, 1, UNIT_CONV_VOLUME, -3.f, parts,
-                           cell.volume, "Co-moving volumes of the particles");
-
-  list[9] = io_make_output_field("GradDensities", FLOAT, 3, UNIT_CONV_DENSITY,
-                                 1.f, parts, primitives.gradients.rho,
-                                 "Gradient densities of the particles");
-
-  list[10] = io_make_output_field_convert_part(
-      "Entropies", FLOAT, 1, UNIT_CONV_ENTROPY, 1.f, parts, xparts, convert_A,
-      "Co-moving entropies of the particles");
-
-  list[11] = io_make_output_field("Pressures", FLOAT, 1, UNIT_CONV_PRESSURE,
-                                  -3.f * hydro_gamma, parts, primitives.P,
-                                  "Co-moving pressures of the particles");
-
-  list[12] = io_make_output_field_convert_part(
-      "TotalEnergies", FLOAT, 1, UNIT_CONV_ENERGY, -3.f * hydro_gamma_minus_one,
-      parts, xparts, convert_Etot, "Total (co-moving) energy of the particles");
+  *num_fields = 0;
 }
 
 /**
  * @brief Writes the current model of SPH to the file
  * @param h_grpsph The HDF5 group in which to write
  */
-INLINE static void hydro_write_flavour(hid_t h_grpsph) {
-  /* Gradient information */
-  io_write_attribute_s(h_grpsph, "Gradient reconstruction model",
-                       HYDRO_GRADIENT_IMPLEMENTATION);
-
-  /* Slope limiter information */
-  io_write_attribute_s(h_grpsph, "Cell wide slope limiter model",
-                       HYDRO_SLOPE_LIMITER_CELL_IMPLEMENTATION);
-  io_write_attribute_s(h_grpsph, "Piecewise slope limiter model",
-                       HYDRO_SLOPE_LIMITER_FACE_IMPLEMENTATION);
-
-  /* Riemann solver information */
-  io_write_attribute_s(h_grpsph, "Riemann solver type",
-                       RIEMANN_SOLVER_IMPLEMENTATION);
-}
+INLINE static void hydro_write_flavour(hid_t h_grpsph) {}
 
 /**
  * @brief Are we writing entropy in the internal energy field ?
@@ -260,3 +90,5 @@ INLINE static void hydro_write_flavour(hid_t h_grpsph) {
  * @return 1 if entropy is in 'internal energy', 0 otherwise.
  */
 INLINE static int writeEntropyFlag(void) { return 0; }
+
+#endif /* SWIFT_NONE_HYDRO_IO_H */
diff --git a/src/hydro/Shadowswift/hydro_parameters.h b/src/hydro/Shadowswift/hydro_parameters.h
index 4db2a31bbdc08cbc490ffa308105a0a4fc9783f8..6c5ea3313b7000f0e11c2defcea74cae0caff884 100644
--- a/src/hydro/Shadowswift/hydro_parameters.h
+++ b/src/hydro/Shadowswift/hydro_parameters.h
@@ -1,7 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2019 Josh Borrow (joshua.borrow@durham.ac.uk)
- *
+ * Copyright (c) 2020 Matthieu Schaller (schaller@strw.leideuniv.nl)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -17,9 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
-
-#ifndef SWIFT_SHADOWSWIFT_HYDRO_PARAMETERS_H
-#define SWIFT_SHADOWSWIFT_HYDRO_PARAMETERS_H
+#ifndef SWIFT_NONE_HYDRO_PARAMETERS_H
+#define SWIFT_NONE_HYDRO_PARAMETERS_H
 
 /* Configuration file */
 #include <config.h>
@@ -36,7 +34,7 @@
 
 /**
  * @file Shadowswift/hydro_parameters.h
- * @brief Shadowswift implementation. (default parameters)
+ * @brief Empty implementation
  *
  *        This file defines a number of things that are used in
  *        hydro_properties.c as defaults for run-time parameters
@@ -48,7 +46,6 @@
 /* Cosmology default beta=3.0.
  * Alpha can be set in the parameter file.
  * Beta is defined as in e.g. Price (2010) Eqn (103) */
-#define const_viscosity_beta 3.0f
 
 /* Structs that store the relevant variables */
 
@@ -72,7 +69,7 @@ struct unit_system;
  *        the parameter file, or sets them to defaults.
  *
  * @param params: the pointer to the swift_params file
- * @param unit_system: pointer to the unit system
+ * @param us: pointer to the internal unit system
  * @param phys_const: pointer to the physical constants system
  * @param viscosity: pointer to the viscosity_global_data struct to be filled.
  **/
@@ -107,9 +104,7 @@ static INLINE void viscosity_print(
  * @param viscosity: pointer to the viscosity_global_data struct.
  **/
 static INLINE void viscosity_print_snapshot(
-    hid_t h_grpsph, const struct viscosity_global_data* viscosity) {
-  io_write_attribute_f(h_grpsph, "Beta viscosity", const_viscosity_beta);
-}
+    hid_t h_grpsph, const struct viscosity_global_data* viscosity) {}
 #endif
 
 /* Diffusion */
@@ -119,9 +114,9 @@ static INLINE void viscosity_print_snapshot(
  *        the parameter file, or sets them to defaults.
  *
  * @param params: the pointer to the swift_params file
- * @param unit_system: pointer to the unit system
+ * @param us: pointer to the internal unit system
  * @param phys_const: pointer to the physical constants system
- * @param diffusion_global_data: pointer to the diffusion struct to be filled.
+ * @param diffusion: pointer to the diffusion struct to be filled.
  **/
 static INLINE void diffusion_init(struct swift_params* params,
                                   const struct unit_system* us,
@@ -157,4 +152,4 @@ static INLINE void diffusion_print_snapshot(
     hid_t h_grpsph, const struct diffusion_global_data* diffusion) {}
 #endif
 
-#endif /* SWIFT_SHADOWSWIFT_HYDRO_PARAMETERS_H */
+#endif /* SWIFT_NONE_HYDRO_PARAMETERS_H */
diff --git a/src/hydro/Shadowswift/hydro_part.h b/src/hydro/Shadowswift/hydro_part.h
index 397dba20d6b0d05fd473f6c1dc9c519cb8777493..7546230a670e08da8b5809cebcb0675451c842f4 100644
--- a/src/hydro/Shadowswift/hydro_part.h
+++ b/src/hydro/Shadowswift/hydro_part.h
@@ -1,8 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2012 Pedro Gonnet (pedro.gonnet@durham.ac.uk)
- *                    Matthieu Schaller (schaller@strw.leidenuniv.nl)
- *               2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
+ * Copyright (c) 2020 Matthieu Schaller (schaller@strw.leidenuniv.nl)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -18,178 +16,141 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
-#ifndef SWIFT_SHADOWSWIFT_HYDRO_PART_H
-#define SWIFT_SHADOWSWIFT_HYDRO_PART_H
+#ifndef SWIFT_NONE_HYDRO_PART_H
+#define SWIFT_NONE_HYDRO_PART_H
+
+/**
+ * @file Shadowswift/hydro_part.h
+ * @brief Empty implementation
+ */
 
 #include "black_holes_struct.h"
 #include "chemistry_struct.h"
 #include "cooling_struct.h"
 #include "feedback_struct.h"
+#include "mhd_struct.h"
 #include "particle_splitting_struct.h"
+#include "pressure_floor_struct.h"
 #include "rt_struct.h"
 #include "sink_struct.h"
+#include "star_formation_struct.h"
 #include "timestep_limiter_struct.h"
 #include "tracers_struct.h"
-#include "voronoi_cell.h"
 
-/* Extra particle data not needed during the computation. */
+/**
+ * @brief Particle fields not needed during the SPH loops over neighbours.
+ *
+ * This structure contains the particle fields that are not used in the
+ * density or force loops. Quantities should be used in the kick, drift and
+ * potentially ghost tasks only.
+ */
 struct xpart {
 
-  /* Offset between current position and position at last tree rebuild. */
+  /*! Offset between current position and position at last tree rebuild. */
   float x_diff[3];
 
   /*! Offset between the current position and position at the last sort. */
   float x_diff_sort[3];
 
-  /* Velocity at the last full step. */
+  /*! Velocity at the last full step. */
   float v_full[3];
 
-  /*! Gravitational acceleration at the end of the last step */
+  /*! Gravitational acceleration at the last full step. */
   float a_grav[3];
 
   /*! Additional data used to record particle splits */
   struct particle_splitting_data split_data;
 
-  /* Additional data used to record cooling information */
+  /*! Additional data used to record cooling information */
   struct cooling_xpart_data cooling_data;
 
   /* Additional data used by the tracers */
   struct tracers_xpart_data tracers_data;
 
+  /* Additional data used by the tracers */
+  struct star_formation_xpart_data sf_data;
+
   /* Additional data used by the feedback */
-  struct feedback_part_data feedback_data;
+  struct feedback_xpart_data feedback_data;
+
+  /*! Additional data used by the MHD scheme */
+  struct mhd_xpart_data mhd_data;
 
 } SWIFT_STRUCT_ALIGN;
 
-/* Data of a single particle. */
+/**
+ * @brief Particle fields for the SPH particles
+ *
+ * The density and force substructures are used to contain variables only used
+ * within the density and force loops over neighbours. All more permanent
+ * variables should be declared in the main part of the part structure,
+ */
 struct part {
 
-  /* Particle ID. */
+  /*! Particle unique ID. */
   long long id;
 
-  /* Associated gravitas. */
-  struct gpart *gpart;
+  /*! Pointer to corresponding gravity part. */
+  struct gpart* gpart;
 
-  /* Particle position. */
+  /*! Particle position. */
   double x[3];
 
-  /* Particle predicted velocity. */
+  /*! Particle predicted velocity. */
   float v[3];
 
-  /* Particle acceleration. */
+  /*! Particle acceleration. */
   float a_hydro[3];
 
-  /* Particle cutoff radius. */
-  float h;
-
-  /* The primitive hydrodynamical variables. */
-  struct {
-
-    /* Fluid velocity. */
-    float v[3];
-
-    /* Density. */
-    float rho;
+  /*! Particle mass. */
+  float mass;
 
-    /* Pressure. */
-    float P;
-
-    /* Gradients of the primitive variables. */
-    struct {
-
-      /* Density gradients. */
-      float rho[3];
-
-      /* Fluid velocity gradients. */
-      float v[3][3];
+  /*! Particle smoothing length. */
+  float h;
 
-      /* Pressure gradients. */
-      float P[3];
+  /*! Particle density */
+  float rho;
 
-    } gradients;
+  /* Store density/force specific stuff. */
+  union {
 
-    /* Quantities needed by the slope limiter. */
+    /**
+     * @brief Structure for the variables only used in the density loop over
+     * neighbours.
+     *
+     * Quantities in this sub-structure should only be accessed in the density
+     * loop over neighbours and the ghost task.
+     */
     struct {
 
-      /* Extreme values of the density among the neighbours. */
-      float rho[2];
-
-      /* Extreme values of the fluid velocity among the neighbours. */
-      float v[3][2];
-
-      /* Extreme values of the pressure among the neighbours. */
-      float P[2];
-
-      /* Maximal distance to all neighbouring faces. */
-      float maxr;
-
-    } limiter;
-
-  } primitives;
+      /*! Neighbour number count. */
+      float wcount;
 
-  /* The conserved hydrodynamical variables. */
-  struct {
+      /*! Derivative of the neighbour number with respect to h. */
+      float wcount_dh;
 
-    /* Fluid momentum. */
-    float momentum[3];
+      /*! Derivative of the density with respect to h. */
+      float rho_dh;
 
-    /* Fluid mass (this field already exists outside of this struct as well). */
-    float mass;
+    } density;
 
-    /* Fluid thermal energy (not per unit mass!). */
-    float energy;
-
-    /* Fluxes. */
+    /**
+     * @brief Structure for the variables only used in the force loop over
+     * neighbours.
+     *
+     * Quantities in this sub-structure should only be accessed in the force
+     * loop over neighbours and the ghost, drift and kick tasks.
+     */
     struct {
 
-      /* Mass flux. */
-      float mass;
-
-      /* Momentum flux. */
-      float momentum[3];
-
-      /* Energy flux. */
-      float energy;
-
-    } flux;
-
-  } conserved;
-
-  /* Variables used for timestep calculation (currently not used). */
-  struct {
-
-    /* Maximum fluid velocity among all neighbours. */
-    float vmax;
-
-  } timestepvars;
-
-  /* Quantities used during the volume (=density) loop. */
-  struct {
-
-    /* Derivative of particle number density. */
-    float wcount_dh;
-
-    /* Particle number density. */
-    float wcount;
+      /*! Time derivative of smoothing length  */
+      float h_dt;
 
-  } density;
+    } force;
+  };
 
-  /* Quantities used during the force loop. */
-  struct {
-
-    /* Needed to drift the primitive variables. */
-    float h_dt;
-
-    /* Physical time step of the particle. */
-    float dt;
-
-    /* Active flag. */
-    char active;
-
-    /* Actual velocity of the particle. */
-    float v_full[3];
-
-  } force;
+  /*! Additional data used by the MHD scheme */
+  struct mhd_part_data mhd_data;
 
   /*! Chemistry information */
   struct chemistry_part_data chemistry_data;
@@ -197,12 +158,18 @@ struct part {
   /*! Cooling information */
   struct cooling_part_data cooling_data;
 
+  /*! Additional data used by the feedback */
+  struct feedback_part_data feedback_data;
+
   /*! Black holes information (e.g. swallowing ID) */
   struct black_holes_part_data black_holes_data;
 
   /*! Sink information (e.g. swallowing ID) */
   struct sink_part_data sink_data;
 
+  /*! Additional data used by the pressure floor */
+  struct pressure_floor_part_data pressure_floor_data;
+
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
@@ -212,6 +179,9 @@ struct part {
   /*! Time-step length */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Time-step limiter information */
   struct timestep_limiter_data limiter_data;
 
@@ -225,9 +195,6 @@ struct part {
 
 #endif
 
-  /* Voronoi cell. */
-  struct voronoi_cell cell;
-
 } SWIFT_STRUCT_ALIGN;
 
-#endif /* SWIFT_SHADOWSWIFT_HYDRO_PART_H */
+#endif /* SWIFT_NONE_HYDRO_PART_H */
diff --git a/src/hydro/Shadowswift/hydro_slope_limiters.h b/src/hydro/Shadowswift/hydro_slope_limiters.h
deleted file mode 100644
index a443007b4df3833964b7ec1780e2bc55bf02a03e..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/hydro_slope_limiters.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#ifndef SWIFT_HYDRO_SLOPE_LIMITERS_H
-#define SWIFT_HYDRO_SLOPE_LIMITERS_H
-
-#include "dimension.h"
-#include "kernel_hydro.h"
-
-#ifdef SHADOWFAX_SLOPE_LIMITER_PER_FACE
-
-#define HYDRO_SLOPE_LIMITER_FACE_IMPLEMENTATION \
-  "GIZMO piecewise slope limiter (Hopkins 2015)"
-#include "hydro_slope_limiters_face.h"
-
-#else
-
-#define HYDRO_SLOPE_LIMITER_FACE_IMPLEMENTATION "No piecewise slope limiter"
-
-/**
- * @brief Slope limit the slopes at the interface between two particles
- *
- * @param Wi Hydrodynamic variables of particle i.
- * @param Wj Hydrodynamic variables of particle j.
- * @param dWi Difference between the hydrodynamic variables of particle i at the
- * position of particle i and at the interface position.
- * @param dWj Difference between the hydrodynamic variables of particle j at the
- * position of particle j and at the interface position.
- * @param xij_i Relative position vector of the interface w.r.t. particle i.
- * @param xij_j Relative position vector of the interface w.r.t. partilce j.
- * @param r Distance between particle i and particle j.
- */
-__attribute__((always_inline)) INLINE static void hydro_slope_limit_face(
-    float *Wi, float *Wj, float *dWi, float *dWj, float *xij_i, float *xij_j,
-    float r) {}
-
-#endif
-
-#ifdef SHADOWFAX_SLOPE_LIMITER_CELL_WIDE
-
-#define HYDRO_SLOPE_LIMITER_CELL_IMPLEMENTATION \
-  "Cell wide slope limiter (Springel 2010)"
-#include "hydro_slope_limiters_cell.h"
-
-#else
-
-#define HYDRO_SLOPE_LIMITER_CELL_IMPLEMENTATION "No cell wide slope limiter"
-
-/**
- * @brief Initialize variables for the cell wide slope limiter
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_slope_limit_cell_init(
-    struct part *p) {}
-
-/**
- * @brief Collect information for the cell wide slope limiter during the
- * neighbour loop
- *
- * @param pi Particle i.
- * @param pj Particle j.
- * @param r Distance between particle i and particle j.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_slope_limit_cell_collect(struct part *pi, struct part *pj, float r) {}
-
-/**
- * @brief Slope limit cell gradients
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_slope_limit_cell(
-    struct part *p) {}
-
-#endif
-
-#endif  // SWIFT_HYDRO_SLOPE_LIMITERS_H
diff --git a/src/hydro/Shadowswift/hydro_slope_limiters_cell.h b/src/hydro/Shadowswift/hydro_slope_limiters_cell.h
deleted file mode 100644
index d746ffc59538fcc625a2e095245adfd7a946a95e..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/hydro_slope_limiters_cell.h
+++ /dev/null
@@ -1,142 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#include <float.h>
-
-/**
- * @brief Initialize variables for the cell wide slope limiter
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_slope_limit_cell_init(
-    struct part* p) {
-
-  p->primitives.limiter.rho[0] = FLT_MAX;
-  p->primitives.limiter.rho[1] = -FLT_MAX;
-  p->primitives.limiter.v[0][0] = FLT_MAX;
-  p->primitives.limiter.v[0][1] = -FLT_MAX;
-  p->primitives.limiter.v[1][0] = FLT_MAX;
-  p->primitives.limiter.v[1][1] = -FLT_MAX;
-  p->primitives.limiter.v[2][0] = FLT_MAX;
-  p->primitives.limiter.v[2][1] = -FLT_MAX;
-  p->primitives.limiter.P[0] = FLT_MAX;
-  p->primitives.limiter.P[1] = -FLT_MAX;
-
-  p->primitives.limiter.maxr = -FLT_MAX;
-}
-
-/**
- * @brief Collect information for the cell wide slope limiter during the
- * neighbour loop
- *
- * @param pi Particle i.
- * @param pj Particle j.
- * @param r Distance between particle i and particle j.
- */
-__attribute__((always_inline)) INLINE static void
-hydro_slope_limit_cell_collect(struct part* pi, const struct part* pj,
-                               float r) {
-
-  /* basic slope limiter: collect the maximal and the minimal value for the
-   * primitive variables among the ngbs */
-  pi->primitives.limiter.rho[0] =
-      fmin(pj->primitives.rho, pi->primitives.limiter.rho[0]);
-  pi->primitives.limiter.rho[1] =
-      fmax(pj->primitives.rho, pi->primitives.limiter.rho[1]);
-
-  pi->primitives.limiter.v[0][0] =
-      fmin(pj->primitives.v[0], pi->primitives.limiter.v[0][0]);
-  pi->primitives.limiter.v[0][1] =
-      fmax(pj->primitives.v[0], pi->primitives.limiter.v[0][1]);
-  pi->primitives.limiter.v[1][0] =
-      fmin(pj->primitives.v[1], pi->primitives.limiter.v[1][0]);
-  pi->primitives.limiter.v[1][1] =
-      fmax(pj->primitives.v[1], pi->primitives.limiter.v[1][1]);
-  pi->primitives.limiter.v[2][0] =
-      fmin(pj->primitives.v[2], pi->primitives.limiter.v[2][0]);
-  pi->primitives.limiter.v[2][1] =
-      fmax(pj->primitives.v[2], pi->primitives.limiter.v[2][1]);
-
-  pi->primitives.limiter.P[0] =
-      fmin(pj->primitives.P, pi->primitives.limiter.P[0]);
-  pi->primitives.limiter.P[1] =
-      fmax(pj->primitives.P, pi->primitives.limiter.P[1]);
-
-  pi->primitives.limiter.maxr = fmax(r, pi->primitives.limiter.maxr);
-}
-
-/**
- * @brief Apply the cell wide slope limiter to the gradient of a single quantity
- *
- * This corresponds to equation (B2) in Hopkins (2015).
- *
- * @param grad Gradient to slope limit
- * @param qval Value of the quantity at the cell generator
- * @param qmin Minimal value of the quantity among all cell neighbours
- * @param qmax Maximal value of the quantity among all cell neighbours
- * @param maxr Maximal distance between the generator and all of its neighbours
- */
-__attribute__((always_inline)) INLINE static void
-hydro_slope_limit_cell_quantity(float* grad, float qval, float qmin, float qmax,
-                                float maxr) {
-
-  float gradtrue, gradmax, gradmin, alpha;
-
-  gradtrue = sqrtf(grad[0] * grad[0] + grad[1] * grad[1] + grad[2] * grad[2]);
-  if (gradtrue) {
-    gradtrue *= maxr;
-    gradmax = qmax - qval;
-    gradmin = qval - qmin;
-    alpha = fmin(1.0f, fmin(gradmax / gradtrue, gradmin / gradtrue));
-    grad[0] *= alpha;
-    grad[1] *= alpha;
-    grad[2] *= alpha;
-  }
-}
-
-/**
- * @brief Slope limit cell gradients
- *
- * @param p Particle.
- */
-__attribute__((always_inline)) INLINE static void hydro_slope_limit_cell(
-    struct part* p) {
-
-  hydro_slope_limit_cell_quantity(
-      p->primitives.gradients.rho, p->primitives.rho,
-      p->primitives.limiter.rho[0], p->primitives.limiter.rho[1],
-      p->primitives.limiter.maxr);
-
-  hydro_slope_limit_cell_quantity(
-      p->primitives.gradients.v[0], p->primitives.v[0],
-      p->primitives.limiter.v[0][0], p->primitives.limiter.v[0][1],
-      p->primitives.limiter.maxr);
-  hydro_slope_limit_cell_quantity(
-      p->primitives.gradients.v[1], p->primitives.v[1],
-      p->primitives.limiter.v[1][0], p->primitives.limiter.v[1][1],
-      p->primitives.limiter.maxr);
-  hydro_slope_limit_cell_quantity(
-      p->primitives.gradients.v[2], p->primitives.v[2],
-      p->primitives.limiter.v[2][0], p->primitives.limiter.v[2][1],
-      p->primitives.limiter.maxr);
-
-  hydro_slope_limit_cell_quantity(
-      p->primitives.gradients.P, p->primitives.P, p->primitives.limiter.P[0],
-      p->primitives.limiter.P[1], p->primitives.limiter.maxr);
-}
diff --git a/src/hydro/Shadowswift/hydro_slope_limiters_face.h b/src/hydro/Shadowswift/hydro_slope_limiters_face.h
deleted file mode 100644
index 7ae5dd2eb073d9aae8ab6f2efffdf8df15b4bb4a..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/hydro_slope_limiters_face.h
+++ /dev/null
@@ -1,121 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-/**
- * @brief Slope limit a single quantity at the interface
- *
- * @param phi_i Value of the quantity at the particle position.
- * @param phi_j Value of the quantity at the neighbouring particle position.
- * @param phi_mid0 Extrapolated value of the quantity at the interface position.
- * @param xij_norm Distance between the particle position and the interface
- * position.
- * @param r Distance between the particle and its neighbour.
- * @return The slope limited difference between the quantity at the particle
- * position and the quantity at the interface position.
- */
-__attribute__((always_inline)) INLINE static float
-hydro_slope_limit_face_quantity(float phi_i, float phi_j, float phi_mid0,
-                                float xij_norm, float r) {
-
-  float delta1, delta2, phimin, phimax, phibar, phiplus, phiminus, phi_mid;
-  const float psi1 = 0.5f;
-  const float psi2 = 0.25f;
-
-  if (phi_i == phi_j) {
-    return 0.0f;
-  }
-
-  delta1 = psi1 * fabs(phi_i - phi_j);
-  delta2 = psi2 * fabs(phi_i - phi_j);
-
-  phimin = fmin(phi_i, phi_j);
-  phimax = fmax(phi_i, phi_j);
-
-  phibar = phi_i + xij_norm / r * (phi_j - phi_i);
-
-  /* if sign(phimax+delta1) == sign(phimax) */
-  if ((phimax + delta1) * phimax > 0.0f) {
-    phiplus = phimax + delta1;
-  } else {
-    phiplus = phimax / (1.0f + delta1 / fabs(phimax));
-  }
-
-  /* if sign(phimin-delta1) == sign(phimin) */
-  if ((phimin - delta1) * phimin > 0.0f) {
-    phiminus = phimin - delta1;
-  } else {
-    phiminus = phimin / (1.0f + delta1 / fabs(phimin));
-  }
-
-  if (phi_i < phi_j) {
-    phi_mid = fmax(phiminus, fmin(phibar + delta2, phi_mid0));
-  } else {
-    phi_mid = fmin(phiplus, fmax(phibar - delta2, phi_mid0));
-  }
-
-  return phi_mid - phi_i;
-}
-
-/**
- * @brief Slope limit the slopes at the interface between two particles
- *
- * @param Wi Hydrodynamic variables of particle i.
- * @param Wj Hydrodynamic variables of particle j.
- * @param dWi Difference between the hydrodynamic variables of particle i at the
- * position of particle i and at the interface position.
- * @param dWj Difference between the hydrodynamic variables of particle j at the
- * position of particle j and at the interface position.
- * @param xij_i Relative position vector of the interface w.r.t. particle i.
- * @param xij_j Relative position vector of the interface w.r.t. partilce j.
- * @param r Distance between particle i and particle j.
- */
-__attribute__((always_inline)) INLINE static void hydro_slope_limit_face(
-    float *Wi, float *Wj, float *dWi, float *dWj, float *xij_i, float *xij_j,
-    float r) {
-
-  float xij_i_norm, xij_j_norm;
-
-  xij_i_norm =
-      sqrtf(xij_i[0] * xij_i[0] + xij_i[1] * xij_i[1] + xij_i[2] * xij_i[2]);
-
-  xij_j_norm =
-      sqrtf(xij_j[0] * xij_j[0] + xij_j[1] * xij_j[1] + xij_j[2] * xij_j[2]);
-
-  dWi[0] = hydro_slope_limit_face_quantity(Wi[0], Wj[0], Wi[0] + dWi[0],
-                                           xij_i_norm, r);
-  dWi[1] = hydro_slope_limit_face_quantity(Wi[1], Wj[1], Wi[1] + dWi[1],
-                                           xij_i_norm, r);
-  dWi[2] = hydro_slope_limit_face_quantity(Wi[2], Wj[2], Wi[2] + dWi[2],
-                                           xij_i_norm, r);
-  dWi[3] = hydro_slope_limit_face_quantity(Wi[3], Wj[3], Wi[3] + dWi[3],
-                                           xij_i_norm, r);
-  dWi[4] = hydro_slope_limit_face_quantity(Wi[4], Wj[4], Wi[4] + dWi[4],
-                                           xij_i_norm, r);
-
-  dWj[0] = hydro_slope_limit_face_quantity(Wj[0], Wi[0], Wj[0] + dWj[0],
-                                           xij_j_norm, r);
-  dWj[1] = hydro_slope_limit_face_quantity(Wj[1], Wi[1], Wj[1] + dWj[1],
-                                           xij_j_norm, r);
-  dWj[2] = hydro_slope_limit_face_quantity(Wj[2], Wi[2], Wj[2] + dWj[2],
-                                           xij_j_norm, r);
-  dWj[3] = hydro_slope_limit_face_quantity(Wj[3], Wi[3], Wj[3] + dWj[3],
-                                           xij_j_norm, r);
-  dWj[4] = hydro_slope_limit_face_quantity(Wj[4], Wi[4], Wj[4] + dWj[4],
-                                           xij_j_norm, r);
-}
diff --git a/src/hydro/Shadowswift/voronoi1d_algorithm.h b/src/hydro/Shadowswift/voronoi1d_algorithm.h
deleted file mode 100644
index 6dcdc1355f02bf304e95a4c46d7e4f4c1943100c..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/voronoi1d_algorithm.h
+++ /dev/null
@@ -1,193 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#ifndef SWIFT_VORONOIXD_ALGORITHM_H
-#define SWIFT_VORONOIXD_ALGORITHM_H
-
-#include "error.h"
-#include "inline.h"
-#include "voronoi1d_cell.h"
-
-#include <math.h>
-#include <stdlib.h>
-
-/**
- * @brief Store the extents of the simulation box in the global variables.
- *
- * @param anchor Corner of the simulation box with the lowest coordinate values.
- * @param side Side lengths of the simulation box.
- */
-__attribute__((always_inline)) INLINE static void voronoi_set_box(
-    const float *anchor, const float *side) {}
-
-/**
- * @brief Initialize a 1D Voronoi cell.
- *
- * Sets the positions of left and right neighbours to very large values, the
- * generator position to the given particle position, and all other quantities
- * to zero.
- *
- * @param cell 1D Voronoi cell to initialize.
- * @param x Position of the generator of the cell.
- * @param anchor Anchor of the simulation box.
- * @param side Side lengths of the simulation box.
- */
-__attribute__((always_inline)) INLINE void voronoi_cell_init(
-    struct voronoi_cell *cell, const double *x, const double *anchor,
-    const double *side) {
-  cell->x = x[0];
-  cell->xL = anchor[0] - cell->x;
-  cell->xR = anchor[0] + side[0] - cell->x;
-  cell->idL = 0;
-  cell->idR = 0;
-  cell->volume = 0.0f;
-  cell->centroid = 0.0f;
-}
-
-/**
- * @brief Interact a 1D Voronoi cell with a particle with given relative
- * position and ID.
- *
- * This method checks if the given relative position is closer to the cell
- * generator than the current left or right neighbour and updates neighbours
- * accordingly.
- *
- * @param cell 1D Voronoi cell.
- * @param dx Relative position of the interacting generator w.r.t. the cell
- * generator (in fact: dx = generator - neighbour).
- * @param id ID of the interacting neighbour.
- */
-__attribute__((always_inline)) INLINE void voronoi_cell_interact(
-    struct voronoi_cell *cell, const float *dx, unsigned long long id) {
-
-  /* Check for stupidity */
-  if (dx[0] == 0.0f) {
-    error("Cannot interact a Voronoi cell generator with itself!");
-  }
-
-  if (-dx[0] < 0.0f) {
-    /* New left neighbour? */
-    if (-dx[0] > cell->xL) {
-      cell->xL = -dx[0];
-      cell->idL = id;
-    }
-  } else {
-    /* New right neighbour? */
-    if (-dx[0] < cell->xR) {
-      cell->xR = -dx[0];
-      cell->idR = id;
-    }
-  }
-}
-
-/**
- * @brief Finalize a 1D Voronoi cell.
- *
- * Calculates the relative positions of the midpoints of the faces (which in
- * this case are just the midpoints of the segments connecting the generator
- * with the two neighbours) w.r.t. the generator, and the cell volume (length)
- * and centroid (midpoint of the segment connecting the midpoints of the faces).
- * This function returns the maximal radius at which a particle could still
- * change the structure of the cell, i.e. twice the largest distance between
- * the cell generator and one of its faces. If the cell has been interacted with
- * all neighbours within this radius, we know for sure that the cell is
- * complete.
- *
- * @param cell 1D Voronoi cell.
- * @return Maximal radius that could still change the structure of the cell.
- */
-__attribute__((always_inline)) INLINE float voronoi_cell_finalize(
-    struct voronoi_cell *cell) {
-
-  float xL, xR;
-  float max_radius;
-
-  max_radius = fmax(-cell->xL, cell->xR);
-  cell->xL = xL = 0.5f * cell->xL;
-  cell->xR = xR = 0.5f * cell->xR;
-
-  cell->volume = xR - xL;
-  cell->centroid = cell->x + 0.5f * (xL + xR);
-
-  return max_radius;
-}
-
-/**
- * @brief Get the oriented surface area and midpoint of the face between a
- * 1D Voronoi cell and the given neighbour.
- *
- * This function also checks if the given neighbour is in fact a neighbour of
- * this cell. Since we perform gradient and flux calculations for all neighbour
- * pairs within the smoothing length, which assumes the cell to be spherical,
- * it can happen that this is not the case. It is the responsibility of the
- * routine that calls this function to check for a zero return value and
- * deal with it appropriately.
- *
- * For this specific case, we simply check if the neighbour is the left or
- * right neighbour and set the surface area to 1. The midpoint is set to the
- * relative position vector of the appropriate face.
- *
- * @param cell 1D Voronoi cell.
- * @param ngb ID of a particle that is possibly a neighbour of this cell.
- * @param midpoint Array to store the relative position of the face in.
- * @return 0 if the given neighbour is not a neighbour, surface area 1.0f
- * otherwise.
- */
-__attribute__((always_inline)) INLINE float voronoi_get_face(
-    const struct voronoi_cell *cell, unsigned long long ngb, float *midpoint) {
-
-  if (ngb != cell->idL && ngb != cell->idR) {
-    /* this is perfectly possible: we interact with all particles within the
-       smoothing length, and they do not need to be all neighbours.
-       If this happens, we return 0, so that the flux method can return */
-    return 0.0f;
-  }
-
-  if (ngb == cell->idL) {
-    /* Left face */
-    midpoint[0] = cell->xL;
-  } else {
-    /* Right face */
-    midpoint[0] = cell->xR;
-  }
-  /* The other components of midpoint are just zero */
-  midpoint[1] = 0.0f;
-  midpoint[2] = 0.0f;
-
-  return 1.0f;
-}
-
-/**
- * @brief Get the centroid of a 1D Voronoi cell.
- *
- * We store only the relevant coordinate of the centroid, but need to return
- * a 3D vector.
- *
- * @param cell 1D Voronoi cell.
- * @param centroid Array to store the centroid in.
- */
-__attribute__((always_inline)) INLINE void voronoi_get_centroid(
-    const struct voronoi_cell *cell, float *centroid) {
-
-  centroid[0] = cell->centroid;
-  centroid[1] = 0.0f;
-  centroid[2] = 0.0f;
-}
-
-#endif  // SWIFT_VORONOIXD_ALGORITHM_H
diff --git a/src/hydro/Shadowswift/voronoi2d_algorithm.h b/src/hydro/Shadowswift/voronoi2d_algorithm.h
deleted file mode 100644
index 16efe290a9c8144eb77953676c0c424288a76f63..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/voronoi2d_algorithm.h
+++ /dev/null
@@ -1,548 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2017 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#ifndef SWIFT_VORONOIXD_ALGORITHM_H
-#define SWIFT_VORONOIXD_ALGORITHM_H
-
-#include "error.h"
-#include "inline.h"
-#include "minmax.h"
-#include "voronoi2d_cell.h"
-
-#include <float.h>
-#include <math.h>
-#include <stdlib.h>
-
-/* Check if the number of vertices exceeds the maximal allowed number */
-#define VORONOI_CHECK_SIZE()          \
-  if (nvert > VORONOI2D_MAXNUMVERT) { \
-    error("Too many vertices!");      \
-  }
-
-/* IDs used to keep track of cells neighbouring walls of the simulation box
-   This will only work if these IDs are never used for actual particles (which
-   in practice means you want to have less than 2^63-4 (~9e18) particles in your
-   simulation) */
-#define VORONOI2D_BOX_LEFT 18446744073709551602llu
-#define VORONOI2D_BOX_RIGHT 18446744073709551603llu
-#define VORONOI2D_BOX_TOP 18446744073709551604llu
-#define VORONOI2D_BOX_BOTTOM 18446744073709551605llu
-
-#define VORONOI2D_TOLERANCE 1.e-6f
-
-/**
- * @brief Initialize a 2D Voronoi cell.
- *
- * @param cell 2D Voronoi cell to initialize.
- * @param x Position of the generator of the cell.
- * @param anchor Anchor of the simulation box containing all particles.
- * @param side Side lengths of the simulation box containing all particles.
- */
-__attribute__((always_inline)) INLINE void voronoi_cell_init(
-    struct voronoi_cell *cell, const double *x, const double *anchor,
-    const double *side) {
-
-  /* Set the position of the generator of the cell (for reference) */
-  cell->x[0] = x[0];
-  cell->x[1] = x[1];
-
-  /* Initialize the cell as a box with the same extents as the simulation box
-     (note: all vertex coordinates are relative w.r.t. the cell generator) */
-  cell->nvert = 4;
-
-  cell->vertices[0][0] = anchor[0] - cell->x[0];
-  cell->vertices[0][1] = anchor[1] - cell->x[1];
-
-  cell->vertices[1][0] = anchor[0] - cell->x[0];
-  cell->vertices[1][1] = anchor[1] + side[1] - cell->x[1];
-
-  cell->vertices[2][0] = anchor[0] + side[0] - cell->x[0];
-  cell->vertices[2][1] = anchor[1] + side[1] - cell->x[1];
-
-  cell->vertices[3][0] = anchor[0] + side[0] - cell->x[0];
-  cell->vertices[3][1] = anchor[1] - cell->x[1];
-
-  /* The neighbours are ordered such that neighbour i shares the face in between
-     vertices i and i+1 (with last vertex + 1 = first vertex)
-     We walk around the cell in clockwise direction */
-  cell->ngbs[0] = VORONOI2D_BOX_LEFT;
-  cell->ngbs[1] = VORONOI2D_BOX_TOP;
-  cell->ngbs[2] = VORONOI2D_BOX_RIGHT;
-  cell->ngbs[3] = VORONOI2D_BOX_BOTTOM;
-
-  /* These variables are initialized to zero, we will compute them after the
-     neighbour iteration has finished */
-  cell->volume = 0.0f;
-  cell->centroid[0] = 0.0f;
-  cell->centroid[1] = 0.0f;
-}
-
-/**
- * @brief Interact a 2D Voronoi cell with a particle with given relative
- * position and ID.
- *
- * @param cell 2D Voronoi cell.
- * @param dx Relative position of the interacting generator w.r.t. the cell
- * generator (in fact: dx = generator - neighbour).
- * @param id ID of the interacting neighbour.
- */
-__attribute__((always_inline)) INLINE void voronoi_cell_interact(
-    struct voronoi_cell *cell, const float *dx, unsigned long long id) {
-
-  /* variables used for geometrical tests */
-  float half_dx[2];
-  float r2;
-  /* variables used to store test results */
-  float test, b1, b2, a1, a2;
-  /* general loop index */
-  int i;
-  /* variables used to store indices of intersected edges */
-  int index_above1, index_above2;
-  int index_below1, index_below2;
-  /* variable used to store directionality in edge traversal */
-  int increment;
-  /* new number of vertices and new vertex coordinates */
-  int nvert;
-  float vertices[VORONOI2D_MAXNUMVERT][2];
-  unsigned long long ngbs[VORONOI2D_MAXNUMVERT];
-
-  /* The process of cutting the current cell with the midline of the generator
-     and the given relative neighbour position proceeds in two steps:
-      - first we need to locate an edge of the current cell that is intersected
-        by this midline. Such an edge does not necessarily exist; in this case
-        the given neighbour is not an actual neighbour of this cell
-      - Once we have an intersected edge, we create a new edge starting at the
-        intersection point. We follow the edges connected to the intersected
-        edge until we find another intersected edge, and use its intersection
-        point as end point of the new edge. */
-
-  /* First, we set up some variables that are used to check if a vertex is above
-     or below the midplane. */
-
-  /* we need a vector with half the size of the vector joining generator and
-     neighbour, pointing to the neighbour */
-  half_dx[0] = -0.5f * dx[0];
-  half_dx[1] = -0.5f * dx[1];
-
-  /* we need the squared length of this vector */
-  r2 = half_dx[0] * half_dx[0] + half_dx[1] * half_dx[1];
-
-  /* a vertex v = (vx, vy) is above the midline if
-       vx*half_dx[0] + vy*half_dx[1] > r2
-     i.e., if the length of the projected vertex position is longer than the
-     length of the vector pointing to the closest point on the midline (both
-     vectors originate at the position of the generator)
-     the vertex is below the midline if the projected position vector is shorter
-     if the projected position vector has the same length, the vertex is on the
-     midline */
-
-  /* start testing a random vertex: the first one */
-  test = cell->vertices[0][0] * half_dx[0] + cell->vertices[0][1] * half_dx[1] -
-         r2;
-  if (test < -VORONOI2D_TOLERANCE) {
-/* vertex is below midline */
-#ifdef VORONOI_VERBOSE
-    message("First vertex is below midline (%g %g --> %g)!",
-            cell->vertices[0][0] + cell->x[0],
-            cell->vertices[0][1] + cell->x[1], test);
-#endif
-
-    /* store the test result; we might need it to compute the intersection
-       coordinates */
-    b1 = test;
-
-    /* move on until we find a vertex that is above or on the midline */
-    i = 1;
-    test = cell->vertices[i][0] * half_dx[0] +
-           cell->vertices[i][1] * half_dx[1] - r2;
-    while (i < cell->nvert && test < 0.) {
-      /* make sure we always store the latest test result */
-      b1 = test;
-      ++i;
-      test = cell->vertices[i][0] * half_dx[0] +
-             cell->vertices[i][1] * half_dx[1] - r2;
-    }
-
-    /* loop finished, there are two possibilities:
-        - i == cell->nvert, all vertices lie below the midline and the given
-          neighbour is not an actual neighbour of this cell
-        - test >= 0., we found a vertex above (or on) the midline */
-    if (i == cell->nvert) {
-/* the given neighbour is not an actual neighbour: exit the routine */
-#ifdef VORONOI_VERBOSE
-      message("Not a neighbour!");
-#endif
-      return;
-    }
-
-    /* we have found an intersected edge: i-1 -> i
-       we store the index of the vertices above and below the midline, make sure
-       we store the test result for later intersection computation, and set the
-       increment to positive, so that we look for the other intersected edge in
-       clockwise direction */
-    index_below1 = i - 1;
-    index_above1 = i;
-    a1 = test;
-    increment = 1;
-  } else {
-/* vertex is above or on midline
-   in the case where it is on the midline, we count that as above as well:
-   the vertex will be removed, and a new vertex will be created at the same
-   position */
-#ifdef VORONOI_VERBOSE
-    message("First vertex is above midline (%g %g --> %g)!",
-            cell->vertices[0][0] + cell->x[0],
-            cell->vertices[0][1] + cell->x[1], test);
-#endif
-
-    /* store the test result */
-    a1 = test;
-
-    /* move on until we find a vertex that is below the midline */
-    i = 1;
-    test = cell->vertices[i][0] * half_dx[0] +
-           cell->vertices[i][1] * half_dx[1] - r2;
-    while (i < cell->nvert && test > -VORONOI2D_TOLERANCE) {
-      /* make sure we always store the most recent test result */
-      a1 = test;
-      ++i;
-      test = cell->vertices[i][0] * half_dx[0] +
-             cell->vertices[i][1] * half_dx[1] - r2;
-    }
-
-    /* loop finished, there are two possibilities:
-        - i == cell->nvert, all vertices lie above the midline. This should
-          never happen.
-        - test <= 0., we found a vertex below (or on) the midline */
-    if (i == cell->nvert) {
-      /* fatal error! */
-      error("Could not find a vertex below the midline!");
-    }
-
-    /* we have found an intersected edge: i-1 -> i
-       we store the index of the vertices above and below the midline, make sure
-       we store the test result for later intersection computation, and set the
-       increment to negative, so that we look for the other intersected edge in
-       counterclockwise direction */
-    index_below1 = i;
-    index_above1 = i - 1;
-    increment = -1;
-    b1 = test;
-  }
-
-#ifdef VORONOI_VERBOSE
-  message("First intersected edge: %g %g --> %g %g (%i --> %i)",
-          cell->vertices[index_below1][0] + cell->x[0],
-          cell->vertices[index_below1][1] + cell->x[1],
-          cell->vertices[index_above1][0] + cell->x[0],
-          cell->vertices[index_above1][1] + cell->x[1], index_below1,
-          index_above1);
-#endif
-
-  /* now we need to find the second intersected edge
-     we start from the vertex above (or on) the midline and search in the
-     direction opposite to the intersected edge direction until we find a vertex
-     below the midline */
-
-  /* we make sure we store the test result for the second vertex above the
-     midline as well, since we need this for intersection point computations
-     the second vertex can be equal to the first */
-  a2 = a1;
-  i = index_above1 + increment;
-  if (i < 0) {
-    i = cell->nvert - 1;
-  }
-  if (i == cell->nvert) {
-    i = 0;
-  }
-  test = cell->vertices[i][0] * half_dx[0] + cell->vertices[i][1] * half_dx[1] -
-         r2;
-  /* this loop can never deadlock, as we know there is at least 1 vertex below
-     the midline */
-  while (test > -VORONOI2D_TOLERANCE) {
-    /* make sure we always store the most recent test result */
-    a2 = test;
-    i += increment;
-    if (i < 0) {
-      i = cell->nvert - 1;
-    }
-    if (i == cell->nvert) {
-      i = 0;
-    }
-    test = cell->vertices[i][0] * half_dx[0] +
-           cell->vertices[i][1] * half_dx[1] - r2;
-  }
-
-  index_below2 = i;
-  index_above2 = i - increment;
-  if (index_above2 < 0) {
-    index_above2 = cell->nvert - 1;
-  }
-  if (index_above2 == cell->nvert) {
-    index_above2 = 0;
-  }
-  /* we also store the test result for the second vertex below the midline */
-  b2 = test;
-
-  if (index_above1 == index_above2 && index_below1 == index_below2) {
-    /* There can be only 1 vertex above or below the midline, but we need 2
-       intersected edges, so if the vertices above the midline are the same, the
-       ones below need to be different and vice versa */
-    error("Only 1 intersected edge found!");
-  }
-
-  /* there is exactly one degenerate case we have not addressed yet: the case
-     where index_above1 and index_above2 are the same and are on the midline.
-     In this case we don't want to create 2 new vertices. Instead, we just keep
-     index_above1, which basically means nothing happens at all and we can just
-     return */
-  if (index_above1 == index_above2 && a1 == 0.) {
-    return;
-  }
-
-  /* to make the code below more clear, we make sure index_above1 always holds
-     the first vertex to remove, and index_above2 the last one, in clockwise
-     order
-     This means we need to interchange 1 and 2 if we were searching in counter-
-     clockwise direction above */
-  if (increment < 0) {
-    i = index_below1;
-    index_below1 = index_below2;
-    index_below2 = i;
-    i = index_above1;
-    index_above1 = index_above2;
-    index_above2 = i;
-    test = b1;
-    b1 = b2;
-    b2 = test;
-    test = a1;
-    a1 = a2;
-    a2 = test;
-  }
-
-#ifdef VORONOI_VERBOSE
-  message("First vertex below: %g %g (%i, %g)",
-          cell->vertices[index_below1][0] + cell->x[0],
-          cell->vertices[index_below1][1] + cell->x[1], index_below1, b1);
-  message("First vertex above: %g %g (%i, %g)",
-          cell->vertices[index_above1][0] + cell->x[0],
-          cell->vertices[index_above1][1] + cell->x[1], index_above1, a1);
-  message("Second vertex below: %g %g (%i, %g)",
-          cell->vertices[index_below2][0] + cell->x[0],
-          cell->vertices[index_below2][1] + cell->x[1], index_below2, b2);
-  message("Second vertex above: %g %g (%i, %g)",
-          cell->vertices[index_above2][0] + cell->x[0],
-          cell->vertices[index_above2][1] + cell->x[1], index_above2, a2);
-#endif
-
-  if (b1 == 0. || b2 == 0.) {
-    error("Vertex below midline is on midline!");
-  }
-
-  /* convert the test results (which correspond to the projected distance
-     between the vertex and the midline) to the fractions of the intersected
-     edges above and below the midline */
-  test = a1 / (a1 - b1);
-  a1 = test;
-  b1 = 1.0f - test;
-
-  test = a2 / (a2 - b2);
-  a2 = test;
-  b2 = 1.0f - test;
-
-  /* remove the vertices above the midline, and insert two new vertices,
-     corresponding to the intersection points of the intersected edges and the
-     midline
-     In practice, we just copy all remaining vertices, starting from the first
-     vertex below the midline (in clockwise order) */
-  nvert = 0;
-  i = index_below2;
-  while (i != index_above1) {
-    vertices[nvert][0] = cell->vertices[i][0];
-    vertices[nvert][1] = cell->vertices[i][1];
-    ngbs[nvert] = cell->ngbs[i];
-    ++nvert;
-    VORONOI_CHECK_SIZE();
-    ++i;
-    if (i == cell->nvert) {
-      i = 0;
-    }
-  }
-  /* now add the new vertices, they are always last */
-  vertices[nvert][0] = a1 * cell->vertices[index_below1][0] +
-                       b1 * cell->vertices[index_above1][0];
-  vertices[nvert][1] = a1 * cell->vertices[index_below1][1] +
-                       b1 * cell->vertices[index_above1][1];
-  ngbs[nvert] = id;
-  ++nvert;
-  VORONOI_CHECK_SIZE();
-  vertices[nvert][0] = a2 * cell->vertices[index_below2][0] +
-                       b2 * cell->vertices[index_above2][0];
-  vertices[nvert][1] = a2 * cell->vertices[index_below2][1] +
-                       b2 * cell->vertices[index_above2][1];
-  ngbs[nvert] = cell->ngbs[index_above2];
-  ++nvert;
-  VORONOI_CHECK_SIZE();
-
-  /* overwrite the original vertices */
-  cell->nvert = nvert;
-  for (i = 0; i < cell->nvert; ++i) {
-    cell->vertices[i][0] = vertices[i][0];
-    cell->vertices[i][1] = vertices[i][1];
-    cell->ngbs[i] = ngbs[i];
-  }
-}
-
-/**
- * @brief Finalize a 2D Voronoi cell.
- *
- * @param cell 2D Voronoi cell.
- * @return Maximal radius that could still change the structure of the cell.
- */
-__attribute__((always_inline)) INLINE float voronoi_cell_finalize(
-    struct voronoi_cell *cell) {
-
-  int i;
-  float vertices[VORONOI2D_MAXNUMVERT][2];
-  float A, x[2], y[2], r2, r2max;
-
-  /* make a copy of the vertices (they are overwritten when the face midpoints
-     are computed */
-  for (i = 0; i < cell->nvert; ++i) {
-    vertices[i][0] = cell->vertices[i][0];
-    vertices[i][1] = cell->vertices[i][1];
-  }
-
-  r2max = 0.0f;
-  for (i = 0; i < cell->nvert; ++i) {
-    if (i < cell->nvert - 1) {
-      x[0] = vertices[i][0];
-      y[0] = vertices[i][1];
-      x[1] = vertices[i + 1][0];
-      y[1] = vertices[i + 1][1];
-    } else {
-      x[0] = vertices[i][0];
-      y[0] = vertices[i][1];
-      x[1] = vertices[0][0];
-      y[1] = vertices[0][1];
-    }
-    A = x[1] * y[0] - x[0] * y[1];
-    cell->volume += A;
-    cell->centroid[0] += (x[0] + x[1]) * A;
-    cell->centroid[1] += (y[0] + y[1]) * A;
-
-    /* Note that we only need the RELATIVE positions of the midpoints */
-    cell->face_midpoints[i][0] = 0.5f * (x[0] + x[1]);
-    cell->face_midpoints[i][1] = 0.5f * (y[0] + y[1]);
-
-    r2 = x[0] * x[0] + y[0] * y[0];
-    r2max = max(r2max, r2);
-
-    x[0] -= x[1];
-    y[0] -= y[1];
-    cell->face_lengths[i] = sqrtf(x[0] * x[0] + y[0] * y[0]);
-  }
-
-  cell->volume *= 0.5f;
-  A = 6 * cell->volume;
-  cell->centroid[0] /= A;
-  cell->centroid[1] /= A;
-
-  cell->centroid[0] += cell->x[0];
-  cell->centroid[1] += cell->x[1];
-
-  return 2.0f * sqrtf(r2max);
-}
-
-/**
- * @brief Get the oriented surface area and midpoint of the face between a
- * 2D Voronoi cell and the given neighbour.
- *
- * @param cell 2D Voronoi cell.
- * @param ngb ID of a particle that is possibly a neighbour of this cell.
- * @param midpoint Array to store the relative position of the face in.
- * @return 0 if the given neighbour is not a neighbour, surface area otherwise.
- */
-__attribute__((always_inline)) INLINE float voronoi_get_face(
-    const struct voronoi_cell *cell, unsigned long long ngb, float *midpoint) {
-
-  /* look up the neighbour */
-  int i = 0;
-  while (i < cell->nvert && cell->ngbs[i] != ngb) {
-    ++i;
-  }
-
-  if (i == cell->nvert) {
-    /* The given cell is not a neighbour. */
-    return 0.0f;
-  }
-
-  midpoint[0] = cell->face_midpoints[i][0];
-  midpoint[1] = cell->face_midpoints[i][1];
-  midpoint[2] = 0.0f;
-
-  return cell->face_lengths[i];
-}
-
-/**
- * @brief Get the centroid of a 2D Voronoi cell.
- *
- * @param cell 2D Voronoi cell.
- * @param centroid Array to store the centroid in.
- */
-__attribute__((always_inline)) INLINE void voronoi_get_centroid(
-    const struct voronoi_cell *cell, float *centroid) {
-
-  centroid[0] = cell->centroid[0];
-  centroid[1] = cell->centroid[1];
-  centroid[2] = 0.0f;
-}
-
-/*******************************************************************************
- ** EXTRA FUNCTIONS USED FOR DEBUGGING *****************************************
- ******************************************************************************/
-
-/**
- * @brief Print the given cell to the stdout in a format that can be plotted
- * using gnuplot.
- *
- * @param cell voronoi_cell to print.
- */
-__attribute__((always_inline)) INLINE void voronoi_print_cell(
-    const struct voronoi_cell *cell) {
-
-  int i, ip1;
-
-  /* print cell generator */
-  printf("%g %g\n\n", cell->x[0], cell->x[1]);
-
-  /* print cell vertices */
-  for (i = 0; i < cell->nvert; ++i) {
-    ip1 = i + 1;
-    if (ip1 == cell->nvert) {
-      ip1 = 0;
-    }
-    printf("%g %g\n%g %g\n\n", cell->vertices[i][0] + cell->x[0],
-           cell->vertices[i][1] + cell->x[1],
-           cell->vertices[ip1][0] + cell->x[0],
-           cell->vertices[ip1][1] + cell->x[1]);
-  }
-}
-
-#endif  // SWIFT_VORONOIXD_ALGORITHM_H
diff --git a/src/hydro/Shadowswift/voronoi2d_cell.h b/src/hydro/Shadowswift/voronoi2d_cell.h
deleted file mode 100644
index 3c54ea8d0aa9ca1c915da1f245f889ebab2073d3..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/voronoi2d_cell.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2017 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#ifndef SWIFT_VORONOIXD_CELL_H
-#define SWIFT_VORONOIXD_CELL_H
-
-/* Maximal number of vertices (and neighbours) that can be stored in a
-   voronoi_cell struct. */
-#define VORONOI2D_MAXNUMVERT 100
-
-/* 2D Voronoi cell */
-struct voronoi_cell {
-
-  /* The position of the generator of the cell. */
-  double x[2];
-
-  /* The "volume" of the 2D cell. */
-  float volume;
-
-  /* The centroid of the cell. */
-  float centroid[2];
-
-  /* Number of cell vertices (and neighbours). */
-  int nvert;
-
-  /* We only need to store one of these at the same time. */
-  union {
-    /* The relative positions of the vertices of the cell. */
-    float vertices[VORONOI2D_MAXNUMVERT][2];
-
-    /* The midpoints of the faces. */
-    float face_midpoints[VORONOI2D_MAXNUMVERT][2];
-  };
-
-  /* The ids of the neighbouring cells. */
-  unsigned long long ngbs[VORONOI2D_MAXNUMVERT];
-
-  /* The lengths of the faces. */
-  float face_lengths[VORONOI2D_MAXNUMVERT];
-};
-
-#endif  // SWIFT_VORONOIXD_CELL_H
diff --git a/src/hydro/Shadowswift/voronoi3d_algorithm.h b/src/hydro/Shadowswift/voronoi3d_algorithm.h
deleted file mode 100644
index 37d7730545340a0a7afdb6fed27a351a1b654ff6..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/voronoi3d_algorithm.h
+++ /dev/null
@@ -1,2216 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#ifndef SWIFT_VORONOIXD_ALGORITHM_H
-#define SWIFT_VORONOIXD_ALGORITHM_H
-
-#include "error.h"
-#include "inline.h"
-#include "voronoi3d_cell.h"
-
-#include <float.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-/* For debugging purposes */
-//#define LOOP_CHECK 1000
-
-#ifdef LOOP_CHECK
-/* We need to do the trickery below to get a unique counter for each call to the
-   macro. This only works if the macro is never called twice on the same line.
- */
-#define MERGE(a, b) a##b
-#define LOOPCOUNTER_NAME(line) MERGE(loopcount, line)
-
-/**
- * @brief Increase the given counter variable and check if it is still valid.
- *
- * @param counter Counter to increase.
- * @param line_number Line number where the while is called.
- * @return 1 if the counter is still valid, 0 otherwise.
- */
-__attribute__((always_inline)) INLINE int check_counter(int *counter,
-                                                        int line_number) {
-  ++(*counter);
-  if ((*counter) == LOOP_CHECK) {
-    error("Number of iterations reached maximum (=%i) in while on line %i!",
-          LOOP_CHECK, line_number);
-  }
-  return 1;
-}
-
-/* safewhile is a wrapper around a while that adds a unique counter variable to
-   the loop that is increased by 1 for each time the loop is executed, and
-   causes the code to crash if this number exceeds a given value.
-   We use this to quickly enable or disable number of iterations checks for a
-   large number of while loops */
-#define safewhile(condition)          \
-  int LOOPCOUNTER_NAME(__LINE__) = 0; \
-  while (check_counter(&LOOPCOUNTER_NAME(__LINE__), __LINE__) && (condition))
-
-#else /* LOOP_CHECK */
-
-/* If LOOP_CHECK is not defined, safewhile and while are EXACTLY the same */
-#define safewhile(condition) while (condition)
-
-#endif /* LOOP_CHECK */
-
-/* This flag activates a number of expensive geometrical checks that help
-   finding bugs. */
-//#define VORONOI3D_EXPENSIVE_CHECKS
-
-/* Tolerance parameter used to decide when to use more precise geometric
-   criteria */
-#define VORONOI3D_TOLERANCE 1.e-6f
-
-/* Box boundary flags used to signal cells neighbouring the box boundary
-   These values correspond to the top range of possible 64-bit integers, and
-   we make the strong assumption that there will never be a particle that has
-   one of these as particle ID. */
-#define VORONOI3D_BOX_FRONT 18446744073709551600llu
-#define VORONOI3D_BOX_BACK 18446744073709551601llu
-#define VORONOI3D_BOX_TOP 18446744073709551602llu
-#define VORONOI3D_BOX_BOTTOM 18446744073709551603llu
-#define VORONOI3D_BOX_LEFT 18446744073709551604llu
-#define VORONOI3D_BOX_RIGHT 18446744073709551605llu
-
-/*******************************************************************************
- * 3D specific methods
- *
- * Most of these methods are based on the source code of voro++:
- *  http://math.lbl.gov/voro++/
- ******************************************************************************/
-
-/**
- * @brief Print the given cell to the stderr in a format that can be easily
- * plotted using gnuplot.
- *
- * This method prints to the stderr instead of stdout to make it possible to use
- * it right before crashing the code.
- *
- * @param c Voronoi cell to print.
- */
-__attribute__((always_inline)) INLINE void voronoi_print_gnuplot_c(
-    const struct voronoi_cell *c) {
-
-  int i, j, v;
-  const double *x = c->x;
-
-  fprintf(stderr, "%g\t%g\t%g\n\n", x[0], x[1], x[2]);
-
-  for (i = 0; i < c->nvert; ++i) {
-    for (j = 0; j < c->orders[i]; ++j) {
-      v = c->edges[c->offsets[i] + j];
-      if (v < 0) {
-        v = -v - 1;
-      }
-      fprintf(stderr, "%g\t%g\t%g\n", c->vertices[3 * i + 0] + x[0],
-              c->vertices[3 * i + 1] + x[1], c->vertices[3 * i + 2] + x[2]);
-      fprintf(stderr, "%g\t%g\t%g\n\n", c->vertices[3 * v + 0] + x[0],
-              c->vertices[3 * v + 1] + x[1], c->vertices[3 * v + 2] + x[2]);
-    }
-  }
-  fprintf(stderr, "\n");
-}
-
-/**
- * @brief Print the contents of a 3D Voronoi cell
- *
- * @param cell 3D Voronoi cell
- */
-__attribute__((always_inline)) INLINE void voronoi_print_cell(
-    const struct voronoi_cell *cell) {
-
-  int i, j;
-
-  fprintf(stderr, "x: %g %g %g\n", cell->x[0], cell->x[1], cell->x[2]);
-  fprintf(stderr, "nvert: %i\n", cell->nvert);
-
-  for (i = 0; i < cell->nvert; ++i) {
-    fprintf(stderr, "%i: %g %g %g (%i)\n", i, cell->vertices[3 * i],
-            cell->vertices[3 * i + 1], cell->vertices[3 * i + 2],
-            cell->orders[i]);
-    for (j = 0; j < cell->orders[i]; ++j) {
-      fprintf(stderr, "%i (%i)\n", cell->edges[cell->offsets[i] + j],
-              cell->edgeindices[cell->offsets[i] + j]);
-    }
-  }
-  fprintf(stderr, "\n");
-}
-
-/**
- * @brief Get the index of the vertex pointed to by the given edge of the given
- * vertex.
- *
- * @param c 3D Voronoi cell.
- * @param vertex Index of a vertex of the cell.
- * @param edge Edge of that vertex.
- * @return Index of the vertex on the other side of the edge.
- */
-__attribute__((always_inline)) INLINE int voronoi_get_edge(
-    const struct voronoi_cell *c, int vertex, int edge) {
-  return c->edges[c->offsets[vertex] + edge];
-}
-
-/**
- * @brief Get the index of the given edge in the edge list of the vertex on the
- * other side of the edge of the given vertex.
- *
- * Suppose that the given vertex has edges [edge1, edge2, given_edge], and that
- * the vertex on the other side of given_edge has edges [edge1, given_edge,
- * edge2], then this method returns 1.
- *
- * @param c 3D Voronoi cell.
- * @param vertex Index of a vertex of the cell.
- * @param edge Edge of that vertex.
- * @return Index of that edge in the edge list of the vertex on the other side
- * of the edge.
- */
-__attribute__((always_inline)) INLINE int voronoi_get_edgeindex(
-    const struct voronoi_cell *c, int vertex, int edge) {
-  return c->edgeindices[c->offsets[vertex] + edge];
-}
-
-/**
- * @brief Set the index of the vertex on the other side of the given edge of the
- * given vertex.
- *
- * @param c 3D Voronoi cell.
- * @param vertex Index of a vertex of the cell.
- * @param edge Edge of that vertex.
- * @param value Index of the vertex on the other side of that edge.
- */
-__attribute__((always_inline)) INLINE void voronoi_set_edge(
-    struct voronoi_cell *c, int vertex, int edge, int value) {
-  c->edges[c->offsets[vertex] + edge] = value;
-}
-
-/**
- * @brief Set the index of the given edge in the edge list of the vertex on the
- * other side of the edge of the given vertex.
- *
- * Suppose that the given vertex has edges [edge1, edge2, given_edge], and we
- * want to tell this method that the vertex on the other side of given_edge has
- * edges [edge1, given_edge, edge2], then we need to pass on a value of 1 to
- * this method.
- *
- * @param c 3D Voronoi cell.
- * @param vertex Index of a vertex of that cell.
- * @param edge Edge of that vertex.
- * @param value Index of that edge in the edge list of the vertex on the other
- * side of the edge.
- */
-__attribute__((always_inline)) INLINE void voronoi_set_edgeindex(
-    struct voronoi_cell *c, int vertex, int edge, int value) {
-  c->edgeindices[c->offsets[vertex] + edge] = value;
-}
-
-/**
- * @brief Get the neighbour for the given edge of the given vertex.
- *
- * An edge is shared by two faces, and each face has a neighbour. Luckily, each
- * edge also has two endpoints, so we can get away with storing only one
- * neighbour per endpoint of an edge. We have complete freedom in choosing which
- * neighbour to store in which endpoint, but we need to be consistent about it.
- * Here we use the following convention: if we take a vector pointing away from
- * the given vertex along the given edge direction, then we store the neighbour
- * that corresponds to the face to the right if looking to the cell from the
- * outside. This is the face that you encounter first when rotating counter-
- * clockwise around that vector, starting from outside the cell.
- *
- * @param c 3D Voronoi cell.
- * @param vertex Index of a vertex of that cell.
- * @param edge Edge of that vertex.
- * @return Index of the neighbour corresponding to that edge and vertex.
- */
-__attribute__((always_inline)) INLINE int voronoi_get_ngb(
-    const struct voronoi_cell *c, int vertex, int edge) {
-  return c->ngbs[c->offsets[vertex] + edge];
-}
-
-/**
- * @brief Set the neighbour for the given edge of the given vertex.
- *
- * An edge is shared by two faces, and each face has a neighbour. Luckily, each
- * edge also has two endpoints, so we can get away with storing only one
- * neighbour per endpoint of an edge. We have complete freedom in choosing which
- * neighbour to store in which endpoint, but we need to be consistent about it.
- * Here we use the following convention: if we take a vector pointing away from
- * the given vertex along the given edge direction, then we store the neighbour
- * that corresponds to the face to the right if looking to the cell from the
- * outside. This is the face that you encounter first when rotating counter-
- * clockwise around that vector, starting from outside the cell.
- *
- * @param c 3D Voronoi cell.
- * @param vertex Index of a vertex of that cell.
- * @param edge Edge of that vertex.
- * @param value Index of the neighbour corresponding to that edge and vertex.
- */
-__attribute__((always_inline)) INLINE void voronoi_set_ngb(
-    struct voronoi_cell *c, int vertex, int edge, int value) {
-  c->ngbs[c->offsets[vertex] + edge] = value;
-}
-
-/**
- * @brief Check if the 3D Voronoi cell is still consistent.
- *
- * A cell is consistent if its edges are consistent, i.e. if edge e of vertex v1
- * points to vertex v2, then v2 should have an edge that points to v1 as well,
- * and then the edge index of vertex v1 should contain the index of that edge
- * in the edge list of v2. We also check if all vertices have orders of at least
- * 3, and if all vertices are actually part of the vertex list.
- * Oh, and we check if the cell actually has vertices.
- *
- * @param cell 3D Voronoi cell to check
- */
-__attribute__((always_inline)) INLINE void voronoi_check_cell_consistency(
-    const struct voronoi_cell *c) {
-
-  int i, j, e, l, m;
-
-  if (c->nvert < 4) {
-    error("Found cell with only %i vertices!", c->nvert);
-  }
-
-  for (i = 0; i < c->nvert; ++i) {
-    if (c->orders[i] < 3) {
-      voronoi_print_cell(c);
-      error("Found cell with vertex of order %i!", c->orders[i]);
-    }
-    for (j = 0; j < c->orders[i]; ++j) {
-      e = voronoi_get_edge(c, i, j);
-      if (e >= c->nvert) {
-        voronoi_print_cell(c);
-        error("Found cell with edges that lead to non-existing vertices!");
-      }
-      if (e < 0) {
-        continue;
-      }
-      l = voronoi_get_edgeindex(c, i, j);
-      m = voronoi_get_edge(c, e, l);
-      if (m != i) {
-        /* voronoi_print_gnuplot_c(c); */
-        voronoi_print_cell(c);
-        fprintf(stderr, "i: %i, j: %i, e: %i, l: %i, m: %i\n", i, j, e, l, m);
-        error("Cell inconsistency!");
-      }
-    }
-  }
-}
-
-/**
- * @brief Check if the given vertex is above, below or on the cutting plane
- * defined by the given parameters.
- *
- * @param v Coordinates of a cell vertex, relative w.r.t. the position of the
- * generator of the cell.
- * @param dx Half of the relative distance vector between the position of the
- * generator of the cell and the position of the neighbouring cell that
- * generates the cutting plane, pointing from the generator position to the
- * cutting plane.
- * @param r2 Squared length of dx.
- * @param test Variable to store the result of the geometric test in, which
- * corresponds to the projected distance between the generator and the vertex
- * along dx.
- * @param teststack Stack to store the results of the N last tests in (for
- * debugging purposes only).
- * @param teststack_size Next available field in the teststack, is reset to 0 if
- * the teststack is full (so the N+1th results is overwritten; for debugging
- * purposes only).
- * @return Result of the test: -1 if the vertex is below the cutting plane, +1
- * if it is above, and 0 if it is on the cutting plane.
- */
-__attribute__((always_inline)) INLINE int voronoi_test_vertex(
-    const float *v, const float *dx, float r2, float *test, float *teststack,
-    int *teststack_size) {
-
-  *test = v[0] * dx[0] + v[1] * dx[1] + v[2] * dx[2] - r2;
-
-  teststack[*teststack_size] = *test;
-  *teststack_size = *teststack_size + 1;
-  if (*teststack_size == 2 * VORONOI3D_MAXNUMVERT) {
-    *teststack_size = 0;
-  }
-
-  if (*test < -VORONOI3D_TOLERANCE) {
-    return -1;
-  }
-  if (*test > VORONOI3D_TOLERANCE) {
-    return 1;
-  }
-  return 0;
-}
-
-/**
- * @brief Initialize the cell as a cube that spans the entire simulation box.
- *
- * @param c 3D Voronoi cell to initialize.
- * @param anchor Anchor of the simulation box.
- * @param side Side lengths of the simulation box.
- */
-__attribute__((always_inline)) INLINE void voronoi_initialize(
-    struct voronoi_cell *cell, const double *anchor, const double *side) {
-
-  cell->nvert = 8;
-
-  /* (0, 0, 0) -- 0 */
-  cell->vertices[0] = anchor[0] - cell->x[0];
-  cell->vertices[1] = anchor[1] - cell->x[1];
-  cell->vertices[2] = anchor[2] - cell->x[2];
-
-  /* (0, 0, 1)-- 1 */
-  cell->vertices[3] = anchor[0] - cell->x[0];
-  cell->vertices[4] = anchor[1] - cell->x[1];
-  cell->vertices[5] = anchor[2] + side[2] - cell->x[2];
-
-  /* (0, 1, 0) -- 2 */
-  cell->vertices[6] = anchor[0] - cell->x[0];
-  cell->vertices[7] = anchor[1] + side[1] - cell->x[1];
-  cell->vertices[8] = anchor[2] - cell->x[2];
-
-  /* (0, 1, 1) -- 3 */
-  cell->vertices[9] = anchor[0] - cell->x[0];
-  cell->vertices[10] = anchor[1] + side[1] - cell->x[1];
-  cell->vertices[11] = anchor[2] + side[2] - cell->x[2];
-
-  /* (1, 0, 0) -- 4 */
-  cell->vertices[12] = anchor[0] + side[0] - cell->x[0];
-  cell->vertices[13] = anchor[1] - cell->x[1];
-  cell->vertices[14] = anchor[2] - cell->x[2];
-
-  /* (1, 0, 1) -- 5 */
-  cell->vertices[15] = anchor[0] + side[0] - cell->x[0];
-  cell->vertices[16] = anchor[1] - cell->x[1];
-  cell->vertices[17] = anchor[2] + side[2] - cell->x[2];
-
-  /* (1, 1, 0) -- 6 */
-  cell->vertices[18] = anchor[0] + side[0] - cell->x[0];
-  cell->vertices[19] = anchor[1] + side[1] - cell->x[1];
-  cell->vertices[20] = anchor[2] - cell->x[2];
-
-  /* (1, 1, 1) -- 7 */
-  cell->vertices[21] = anchor[0] + side[0] - cell->x[0];
-  cell->vertices[22] = anchor[1] + side[1] - cell->x[1];
-  cell->vertices[23] = anchor[2] + side[2] - cell->x[2];
-
-  cell->orders[0] = 3;
-  cell->orders[1] = 3;
-  cell->orders[2] = 3;
-  cell->orders[3] = 3;
-  cell->orders[4] = 3;
-  cell->orders[5] = 3;
-  cell->orders[6] = 3;
-  cell->orders[7] = 3;
-
-  /* edges are ordered counterclockwise w.r.t. a vector pointing from the
-     cell generator to the vertex
-     (0, 0, 0) corner */
-  cell->offsets[0] = 0;
-  cell->edges[0] = 1;
-  cell->edges[1] = 2;
-  cell->edges[2] = 4;
-  cell->edgeindices[0] = 0;
-  cell->edgeindices[1] = 2;
-  cell->edgeindices[2] = 0;
-
-  /* (0, 0, 1) corner */
-  cell->offsets[1] = 3;
-  cell->edges[3] = 0;
-  cell->edges[4] = 5;
-  cell->edges[5] = 3;
-  cell->edgeindices[3] = 0;
-  cell->edgeindices[4] = 2;
-  cell->edgeindices[5] = 1;
-
-  /* (0, 1, 0) corner */
-  cell->offsets[2] = 6;
-  cell->edges[6] = 3;
-  cell->edges[7] = 6;
-  cell->edges[8] = 0;
-  cell->edgeindices[6] = 0;
-  cell->edgeindices[7] = 0;
-  cell->edgeindices[8] = 1;
-
-  /* (0, 1, 1) corner */
-  cell->offsets[3] = 9;
-  cell->edges[9] = 2;
-  cell->edges[10] = 1;
-  cell->edges[11] = 7;
-  cell->edgeindices[9] = 0;
-  cell->edgeindices[10] = 2;
-  cell->edgeindices[11] = 0;
-
-  /* (1, 0, 0) corner */
-  cell->offsets[4] = 12;
-  cell->edges[12] = 0;
-  cell->edges[13] = 6;
-  cell->edges[14] = 5;
-  cell->edgeindices[12] = 2;
-  cell->edgeindices[13] = 2;
-  cell->edgeindices[14] = 0;
-
-  /* (1, 0, 1) corner */
-  cell->offsets[5] = 15;
-  cell->edges[15] = 4;
-  cell->edges[16] = 7;
-  cell->edges[17] = 1;
-  cell->edgeindices[15] = 2;
-  cell->edgeindices[16] = 1;
-  cell->edgeindices[17] = 1;
-
-  /* (1, 1, 0) corner */
-  cell->offsets[6] = 18;
-  cell->edges[18] = 2;
-  cell->edges[19] = 7;
-  cell->edges[20] = 4;
-  cell->edgeindices[18] = 1;
-  cell->edgeindices[19] = 2;
-  cell->edgeindices[20] = 1;
-
-  /* (1, 1, 1) corner */
-  cell->offsets[7] = 21;
-  cell->edges[21] = 3;
-  cell->edges[22] = 5;
-  cell->edges[23] = 6;
-  cell->edgeindices[21] = 2;
-  cell->edgeindices[22] = 1;
-  cell->edgeindices[23] = 1;
-
-  /* ngbs[3*i+j] is the neighbour corresponding to the plane clockwise of
-     edge j of vertex i (when going from edge j to vertex i)
-     we set them to a ridiculously large value to be able to track faces without
-     neighbour */
-  cell->ngbs[0] = VORONOI3D_BOX_FRONT;  /* (000) - (001) */
-  cell->ngbs[1] = VORONOI3D_BOX_LEFT;   /* (000) - (010) */
-  cell->ngbs[2] = VORONOI3D_BOX_BOTTOM; /* (000) - (100) */
-
-  cell->ngbs[3] = VORONOI3D_BOX_LEFT;  /* (001) - (000) */
-  cell->ngbs[4] = VORONOI3D_BOX_FRONT; /* (001) - (101) */
-  cell->ngbs[5] = VORONOI3D_BOX_TOP;   /* (001) - (011) */
-
-  cell->ngbs[6] = VORONOI3D_BOX_LEFT;   /* (010) - (011) */
-  cell->ngbs[7] = VORONOI3D_BOX_BACK;   /* (010) - (110) */
-  cell->ngbs[8] = VORONOI3D_BOX_BOTTOM; /* (010) - (000) */
-
-  cell->ngbs[9] = VORONOI3D_BOX_BACK;  /* (011) - (010) */
-  cell->ngbs[10] = VORONOI3D_BOX_LEFT; /* (011) - (001) */
-  cell->ngbs[11] = VORONOI3D_BOX_TOP;  /* (011) - (111) */
-
-  cell->ngbs[12] = VORONOI3D_BOX_FRONT;  /* (100) - (000) */
-  cell->ngbs[13] = VORONOI3D_BOX_BOTTOM; /* (100) - (110) */
-  cell->ngbs[14] = VORONOI3D_BOX_RIGHT;  /* (100) - (101) */
-
-  cell->ngbs[15] = VORONOI3D_BOX_FRONT; /* (101) - (100) */
-  cell->ngbs[16] = VORONOI3D_BOX_RIGHT; /* (101) - (111) */
-  cell->ngbs[17] = VORONOI3D_BOX_TOP;   /* (101) - (001) */
-
-  cell->ngbs[18] = VORONOI3D_BOX_BOTTOM; /* (110) - (010) */
-  cell->ngbs[19] = VORONOI3D_BOX_BACK;   /* (110) - (111) */
-  cell->ngbs[20] = VORONOI3D_BOX_RIGHT;  /* (110) - (100) */
-
-  cell->ngbs[21] = VORONOI3D_BOX_BACK;  /* (111) - (011) */
-  cell->ngbs[22] = VORONOI3D_BOX_TOP;   /* (111) - (101) */
-  cell->ngbs[23] = VORONOI3D_BOX_RIGHT; /* (111) - (110) */
-}
-
-/**
- * @brief Find an edge of the voronoi_cell that intersects the cutting plane.
- *
- * There is a large number of possible paths through this method, each of which
- * is covered by a separate unit test in testVoronoi3D. Paths have been numbered
- * in the inline comments to help identify them.
- *
- * @param c 3D Voronoi cell.
- * @param dx Vector pointing from pj to the midpoint of the line segment between
- * pi and pj.
- * @param r2 Squared length of dx.
- * @param u Projected distance between the plane and the closest vertex above
- * the plane, along dx.
- * @param up Index of the closest vertex above the plane.
- * @param us Index of the edge of vertex up that intersects the plane.
- * @param uw Result of the last test_vertex call for vertex up.
- * @param l Projected distance between the plane and the closest vertex below
- * the plane, along dx.
- * @param lp Index of the closest vertex below the plane.
- * @param ls Index of the edge of vertex lp that intersects the plane.
- * @param lw Result of the last test_vertex call for vertex lp.
- * @param q Projected distance between the plane and a test vertex, along dx.
- * @param qp Index of the test vertex.
- * @param qs Index of the edge of the test vertex that is connected to up.
- * @param qw Result of the last test_vertex call involving qp.
- * @return A negative value if an error occurred, 0 if the plane does not
- * intersect the cell, 1 if nothing special happened and 2 if we have a
- * complicated setup.
- */
-__attribute__((always_inline)) INLINE int voronoi_intersect_find_closest_vertex(
-    struct voronoi_cell *c, const float *dx, float r2, float *u, int *up,
-    int *us, int *uw, float *l, int *lp, int *ls, int *lw, float *q, int *qp,
-    int *qs, int *qw) {
-
-  /* stack to store all vertices that have already been tested (debugging
-     only) */
-  float teststack[2 * VORONOI3D_MAXNUMVERT];
-  /* size of the used part of the stack */
-  int teststack_size = 0;
-  /* flag signalling a complicated setup */
-  int complicated;
-
-  /* test the first vertex: uw = -1 if it is below the plane, 1 if it is above
-     0 if it is very close to the plane, and things become complicated... */
-  *uw = voronoi_test_vertex(&c->vertices[0], dx, r2, u, teststack,
-                            &teststack_size);
-  *up = 0;
-  complicated = 0;
-  if ((*uw) == 0) {
-
-    /* PATH 0 */
-    complicated = 1;
-
-  } else {
-
-    /* two options: either the vertex is above or below the plane */
-
-    if ((*uw) == 1) {
-
-      /* PATH 1 */
-
-      /* above: try to find a vertex below
-         we test all edges of the current vertex stored in up (vertex 0) until
-         we either find one below the plane or closer to the plane */
-      *lp = voronoi_get_edge(c, (*up), 0);
-      *lw = voronoi_test_vertex(&c->vertices[3 * (*lp)], dx, r2, l, teststack,
-                                &teststack_size);
-      *us = 1;
-      /* Not in while: PATH 1.0 */
-      /* somewhere in while: PATH 1.1 */
-      /* last valid option of while: PATH 1.2 */
-      safewhile((*us) < c->orders[(*up)] && (*l) >= (*u)) {
-        *lp = voronoi_get_edge(c, (*up), (*us));
-        *lw = voronoi_test_vertex(&c->vertices[3 * (*lp)], dx, r2, l, teststack,
-                                  &teststack_size);
-        ++(*us);
-      }
-      /* we increased us too much, correct this */
-      --(*us);
-      if ((*l) >= (*u)) {
-        /* PATH 1.3 */
-        /* up is the closest vertex to the plane, but is above the plane
-           since the entire cell is convex, up is the closest vertex of all
-           vertices of the cell
-           this means the entire cell is supposedly above the plane, which is
-           impossible */
-        message(
-            "Cell completely gone! This should not happen. (l >= u, l = %g, u "
-            "= %g)",
-            (*l), (*u));
-        return -1;
-      }
-      /* we know that lp is closer to the plane or below the plane
-         now find the index of the edge up-lp in the edge list of lp */
-      *ls = voronoi_get_edgeindex(c, (*up), (*us));
-
-      /* if lp is also above the plane, replace up by lp and repeat the process
-         until lp is below the plane */
-      safewhile((*lw) == 1) {
-        /* PATH 1.4 */
-        *u = (*l);
-        *up = (*lp);
-        *us = 0;
-        /* no while: PATH 1.4.0 */
-        /* somewhere in while: PATH 1.4.1 */
-        /* last valid option of while: PATH 1.4.2 */
-        safewhile((*us) < (*ls) && (*l) >= (*u)) {
-          *lp = voronoi_get_edge(c, (*up), (*us));
-          *lw = voronoi_test_vertex(&c->vertices[3 * (*lp)], dx, r2, l,
-                                    teststack, &teststack_size);
-          ++(*us);
-        }
-        if ((*l) >= (*u)) {
-          ++(*us);
-          /* no while: PATH 1.4.3 */
-          /* somewhere in while: PATH 1.4.4 */
-          /* last valid option of while: PATH 1.4.5 */
-          safewhile((*us) < c->orders[(*up)] && (*l) >= (*u)) {
-            *lp = voronoi_get_edge(c, (*up), (*us));
-            *lw = voronoi_test_vertex(&c->vertices[3 * (*lp)], dx, r2, l,
-                                      teststack, &teststack_size);
-            ++(*us);
-          }
-          if ((*l) >= (*u)) {
-            /* PATH 1.4.6 */
-            message(
-                "Cell completely gone! This should not happen. (l >= u, l = "
-                "%g, u = %g)",
-                (*l), (*u));
-            return -1;
-          }
-        }
-        --(*us);
-        *ls = voronoi_get_edgeindex(c, (*up), (*us));
-      }
-      /* if lp is too close to the plane, replace up by lp and proceed to
-         complicated setup */
-      if ((*lw) == 0) {
-        /* PATH 1.5 */
-        *up = (*lp);
-        complicated = 1;
-      }
-    } else { /* if(uw == 1) */
-
-      /* PATH 2 */
-
-      /* below: try to find a vertex above
-         we test all edges of the current vertex stored in up (vertex 0) until
-         we either find one above the plane or closer to the plane */
-
-      *qp = voronoi_get_edge(c, (*up), 0);
-      *qw = voronoi_test_vertex(&c->vertices[3 * (*qp)], dx, r2, q, teststack,
-                                &teststack_size);
-      *us = 1;
-      /* not in while: PATH 2.0 */
-      /* somewhere in while: PATH 2.1 */
-      /* last valid option of while: PATH 2.2 */
-      safewhile((*us) < c->orders[(*up)] && (*u) >= (*q)) {
-        *qp = voronoi_get_edge(c, (*up), (*us));
-        *qw = voronoi_test_vertex(&c->vertices[3 * (*qp)], dx, r2, q, teststack,
-                                  &teststack_size);
-        ++(*us);
-      }
-      if ((*u) >= (*q)) {
-        /* PATH 2.3 */
-        /* up is the closest vertex to the plane and is below the plane
-           since the cell is convex, up is the closest vertex of all vertices of
-           the cell
-           this means that the entire cell is below the plane
-           The cell is unaltered. */
-        return 0;
-      } else {
-        /* the last increase in the loop pushed us too far, correct this */
-        --(*us);
-      }
-
-      /* repeat the above process until qp is closer or above the plane */
-      safewhile((*qw) == -1) {
-        /* PATH 2.4 */
-        *qs = voronoi_get_edgeindex(c, (*up), (*us));
-        *u = (*q);
-        *up = (*qp);
-        *us = 0;
-        /* no while: PATH 2.4.0 */
-        /* somewhere in while: PATH 2.4.1 */
-        /* last valid option of while: 2.4.2 */
-        safewhile((*us) < (*qs) && (*u) >= (*q)) {
-          *qp = voronoi_get_edge(c, (*up), (*us));
-          *qw = voronoi_test_vertex(&c->vertices[3 * (*qp)], dx, r2, q,
-                                    teststack, &teststack_size);
-          ++(*us);
-        }
-        if ((*u) >= (*q)) {
-          ++(*us);
-          /* no while: PATH 2.4.3 */
-          /* somewhere in while: PATH 2.4.4 */
-          /* last valid option of while: PATH 2.4.5 */
-          safewhile((*us) < c->orders[(*up)] && (*u) >= (*q)) {
-            *qp = voronoi_get_edge(c, (*up), (*us));
-            *qw = voronoi_test_vertex(&c->vertices[3 * (*qp)], dx, r2, q,
-                                      teststack, &teststack_size);
-            ++(*us);
-          }
-          if ((*u) >= (*q)) {
-            /* PATH 2.4.6 */
-            /* cell unaltered */
-            return 0;
-          }
-        }
-        --(*us);
-      }
-      if ((*qw) == 1) {
-        /* qp is above the plane: initialize lp to up and replace up by qp */
-        *lp = (*up);
-        *ls = (*us);
-        *l = (*u);
-        *up = (*qp);
-        *us = voronoi_get_edgeindex(c, (*lp), (*ls));
-        *u = (*q);
-      } else {
-        /* PATH 2.5 */
-        /* too close to call: go to complicated setup */
-        *up = (*qp);
-        complicated = 1;
-      }
-
-    } /* if(uw == 1) */
-
-  } /* if(uw == 0) */
-
-  if (complicated) {
-    return 2;
-  } else {
-    return 1;
-  }
-}
-
-/**
- * @brief Intersect the given cell with the midplane between the cell generator
- * and a neighbouring cell at the given relative position and with the given ID.
- *
- * This method is the core of the Voronoi algorithm. If anything goes wrong
- * geometrically, it most likely goes wrong somewhere within this method.
- *
- * @param c 3D Voronoi cell.
- * @param odx The original relative distance vector between the cell generator
- * and the intersecting neighbour, as it is passed on to runner_iact_density
- * (remember: odx = pi->x - pj->x).
- * @param ngb ID of the intersecting neighbour (pj->id in runner_iact_density).
- */
-__attribute__((always_inline)) INLINE void voronoi_intersect(
-    struct voronoi_cell *c, const float *odx, unsigned long long ngb) {
-
-  /* vector pointing from pi to the midpoint of the line segment between pi and
-     pj. This corresponds to -0.5*odx */
-  float dx[3];
-  /* squared norm of dx */
-  float r2;
-  /* u: distance between the plane and the closest vertex above the plane (up)
-     l: distance between the plane and the closest vertex below the plane (lp)
-     q: distance between the plane and the vertex that is currently being
-     tested (qp) */
-  float u = 0.0f, l = 0.0f, q = 0.0f;
-  /* up: index of the closest vertex above the plane
-     us: index of the edge of vertex up that intersects the plane
-     uw: result of the last orientation test involving vertex u
-     same naming used for vertex l and vertex q */
-  int up = -1, us = -1, uw = -1, lp = -1, ls = -1, lw = -1, qp = -1, qs = -1,
-      qw = -1;
-  /* auxiliary flag used to capture degeneracies */
-  int complicated = -1;
-
-  /* stack to store all vertices that have already been tested (debugging
-     only) */
-  float teststack[2 * VORONOI3D_MAXNUMVERT];
-  /* size of the used part of the stack */
-  int teststack_size = 0;
-
-#ifdef VORONOI3D_EXPENSIVE_CHECKS
-  voronoi_check_cell_consistency(c);
-#endif
-
-  /* initialize dx and r2 */
-  dx[0] = -0.5f * odx[0];
-  dx[1] = -0.5f * odx[1];
-  dx[2] = -0.5f * odx[2];
-  r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
-
-  /* find an intersected edge of the cell */
-  int result = voronoi_intersect_find_closest_vertex(
-      c, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-  if (result < 0) {
-    /* the closest_vertex test only found vertices above the intersecting plane
-       this would mean that the entire cell lies above the midplane of the line
-       segment connecting a point inside the cell (the generator) and a point
-       that could be inside or outside the cell (the neighbour). This is
-       geometrically absurd and should NEVER happen. */
-    voronoi_print_gnuplot_c(c);
-    error("Error while searching intersected edge!");
-  }
-  if (result == 0) {
-    /* no intersection */
-    return;
-  }
-  if (result == 2) {
-    complicated = 1;
-  } else {
-    complicated = 0;
-  }
-
-  /* At this point:
-      up contains a vertex above the plane
-      lp contains a vertex below the plane
-      us and ls contain the index of the edge that connects up and lp, this edge
-      is intersected by the midplane
-      u and l contain the projected distances of up and lp to the midplane,
-      along dx
-     IF complicated is 1, up contains a vertex that is considered to be on the
-     plane. All other variables can be considered to be uninitialized in this
-     case. */
-
-  int vindex = -1;
-  int visitflags[VORONOI3D_MAXNUMVERT];
-  int dstack[2 * VORONOI3D_MAXNUMVERT];
-  int dstack_size = 0;
-  float r = 0.0f;
-  int cs = -1, rp = -1;
-  int double_edge = 0;
-  int i = -1, j = -1, k = -1;
-
-  /* initialize visitflags */
-  for (i = 0; i < VORONOI3D_MAXNUMVERT; ++i) {
-    visitflags[i] = 0;
-  }
-
-  if (complicated) {
-
-    /* We've entered the complicated setup, which means that somewhere along the
-       way we found a vertex that is on or very close to the midplane. The index
-       of that vertex is stored in up, all other variables are meaningless at
-       this point. */
-
-    /* first of all, we need to find a vertex which has edges that extend below
-       the plane (since the remainder of our algorithm depends on that). This is
-       not necessarily the case: in principle a vertex can only have edges that
-       extend inside or above the plane.
-       we create a stack of vertices to test (we use dstack for this), and add
-       vertex up. For each vertex on the stack, we then traverse its edges. If
-       the edge extends above the plane, we ignore it. If it extends below, we
-       stop. If the edge lies in the plane, we add the vertex on the other end
-       to the stack.
-       We make sure that up contains the index of a vertex extending beyond the
-       plane on exit. */
-    dstack[dstack_size] = up;
-    ++dstack_size;
-    lw = 0;
-    j = 0;
-    safewhile(j < dstack_size && lw != -1) {
-      up = dstack[j];
-      for (i = 0; i < c->orders[up]; ++i) {
-        lp = voronoi_get_edge(c, up, i);
-        lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                                 &teststack_size);
-        if (lw == -1) {
-          /* jump out of the for loop */
-          break;
-        }
-        if (lw == 0) {
-          /* only add each vertex to the stack once */
-          k = 0;
-          safewhile(k < dstack_size && dstack[k] != lp) { ++k; }
-          if (k == dstack_size) {
-            dstack[dstack_size] = lp;
-            ++dstack_size;
-          }
-        }
-      }
-      ++j;
-    }
-
-    /* we increased j after lw was calculated, so only the value of lw should be
-       used to determine whether or not the loop was successful */
-    if (lw != -1) {
-      /* we did not find an edge that extends below the plane. There are two
-         possible reasons for this: either all vertices of the cell lie above
-         or inside the midplane of the segment connecting a point inside the
-         cell (the generator) with a point inside or outside the cell (the
-         neighbour). This is geometrically absurd.
-         Another reason might be that somehow all vertices in the midplane only
-         have edges that extend outwards. This is contradictory to the fact that
-         a Voronoi cell is convex, and therefore also unacceptable.
-         We conclude that we should NEVER end up here. */
-      voronoi_print_cell(c);
-      error("Unable to find a vertex below the midplane!");
-    }
-    /* reset the delete stack, we need it later on */
-    dstack_size = 0;
-
-    /* the search routine detected a vertex very close to or in the midplane
-       the index of this vertex is stored in up
-       we proceed by checking the edges of this vertex */
-
-    lp = voronoi_get_edge(c, up, 0);
-    lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                             &teststack_size);
-
-    /* the first edge can be below, above or on the plane */
-    if (lw != -1) {
-
-      /* above or on the plane: we try to find one below the plane */
-
-      rp = lw;
-      i = 1;
-      lp = voronoi_get_edge(c, up, i);
-      lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                               &teststack_size);
-      safewhile(lw != -1) {
-        ++i;
-        if (i == c->orders[up]) {
-          /* none of the edges of up is below the plane. Since the cell is
-             supposed to be convex, this means the entire cell is above or on
-             the plane. This should not happen...
-             Furthermore, we should really NEVER end up here, as in this case
-             an error should already have be thrown above. */
-          voronoi_print_gnuplot_c(c);
-          error(
-              "Cell completely gone! This should not happen. (i == "
-              "c->order[up], i = %d, c->orders[up] = %d, up = %d)\n"
-              "dx: [%g %g %g]\nv[up]: [%g %g %g]\nx: [%g %g %g]",
-              i, c->orders[up], up, dx[0], dx[1], dx[2], c->vertices[3 * up],
-              c->vertices[3 * up + 1], c->vertices[3 * up + 2], c->x[0],
-              c->x[1], c->x[2]);
-        }
-        lp = voronoi_get_edge(c, up, i);
-        lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                                 &teststack_size);
-      }
-
-      /* lp, l and lw now contain values corresponding to an edge below the
-         plane
-         rp contains the result of test_vertex for the first edge of up, for
-         reference */
-
-      /* we go on to the next edge of up, and see if we can find an edge that
-         does not extend below the plane */
-
-      j = i + 1;
-      safewhile(j < c->orders[up] && lw == -1) {
-        lp = voronoi_get_edge(c, up, j);
-        lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                                 &teststack_size);
-        ++j;
-      }
-
-      if (lw != -1) {
-        /* the last iteration increased j by 1 too many, correct this */
-        --j;
-      }
-
-      /* j-i now contains the number of edges below the plane. We will replace
-         up by a new vertex of order this number + 2 (since 2 new edges will be
-         created inside the plane)
-         however, we do not do this if there is exactly one edge that lies in
-         the plane, and all other edges lie below, because in this case we can
-         just keep vertex up as is */
-
-      if (j == c->orders[up] && i == 1 && rp == 0) {
-        /* keep the order of up, and flag this event for later reference */
-        k = c->orders[up];
-        double_edge = 1;
-      } else {
-        /* general case: keep all edges below the plane, and create 2 new ones
-           in the plane */
-        k = j - i + 2;
-      }
-
-      /* create new order k vertex */
-      vindex = c->nvert;
-      ++c->nvert;
-      if (c->nvert == VORONOI3D_MAXNUMVERT) {
-        error("Too many vertices!");
-      }
-      c->orders[vindex] = k;
-      c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-      if (c->offsets[vindex] + k >= VORONOI3D_MAXNUMEDGE) {
-        error("Too many edges!");
-      }
-
-      visitflags[vindex] = -vindex;
-      /* the new vertex adopts the coordinates of the old vertex */
-      c->vertices[3 * vindex + 0] = c->vertices[3 * up + 0];
-      c->vertices[3 * vindex + 1] = c->vertices[3 * up + 1];
-      c->vertices[3 * vindex + 2] = c->vertices[3 * up + 2];
-
-      /* us contains the index of the last edge NOT below the plane
-         note that i is at least 1, so there is no need to wrap in this case */
-      us = i - 1;
-
-      /* copy all edges of up below the plane into the new vertex, starting from
-         edge 1 (edge 0 is reserved to connect to a newly created vertex
-         below) */
-      k = 1;
-      safewhile(i < j) {
-        qp = voronoi_get_edge(c, up, i);
-        qs = voronoi_get_edgeindex(c, up, i);
-        voronoi_set_ngb(c, vindex, k, voronoi_get_ngb(c, up, i));
-        voronoi_set_edge(c, vindex, k, qp);
-        voronoi_set_edgeindex(c, vindex, k, qs);
-        voronoi_set_edge(c, qp, qs, vindex);
-        voronoi_set_edgeindex(c, qp, qs, k);
-        /* disconnect up, since this vertex will be removed */
-        voronoi_set_edge(c, up, i, -1);
-        ++i;
-        ++k;
-      }
-
-      /* store the index of the first edge not below the plane */
-      if (i == c->orders[up]) {
-        qs = 0;
-      } else {
-        qs = i;
-      }
-    } else { /* if(lw != -1) */
-
-      /* the first edge lies below the plane, try to find one that does not */
-
-      /* we first do a reverse search */
-      i = c->orders[up] - 1;
-      lp = voronoi_get_edge(c, up, i);
-      lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                               &teststack_size);
-      safewhile(lw == -1) {
-        --i;
-        if (i == 0) {
-          /* No edge above or in the plane found: the cell is unaltered */
-          return;
-        }
-        lp = voronoi_get_edge(c, up, i);
-        lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                                 &teststack_size);
-      }
-
-      /* now we do a forward search */
-      j = 1;
-      qp = voronoi_get_edge(c, up, j);
-      qw = voronoi_test_vertex(&c->vertices[3 * qp], dx, r2, &q, teststack,
-                               &teststack_size);
-      safewhile(qw == -1) {
-        ++j;
-        qp = voronoi_get_edge(c, up, j);
-        qw = voronoi_test_vertex(&c->vertices[3 * qp], dx, r2, &l, teststack,
-                                 &teststack_size);
-      }
-
-      /* at this point j contains the index of the first edge not below the
-         plane, i the index of the last edge not below the plane
-         we use this to compute the number of edges below the plane. up is
-         replaced by a new vertex that has that number + 2 edges (since 2 new
-         edges are created inside the plane). We again capture the special event
-         where there is only one edge not below the plane, which lies inside the
-         plane. In this case up is copied as is. */
-
-      if (i == j && qw == 0) {
-        /* we keep up as is, and flag this event */
-        double_edge = 1;
-        k = c->orders[up];
-      } else {
-        /* (c->orders[up]-1 - i) + j is the number of edges below the plane */
-        k = c->orders[up] - i + j + 1;
-      }
-
-      /* create new order k vertex */
-      vindex = c->nvert;
-      ++c->nvert;
-      if (c->nvert == VORONOI3D_MAXNUMVERT) {
-        error("Too many vertices!");
-      }
-      c->orders[vindex] = k;
-      c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-      if (c->offsets[vindex] + k >= VORONOI3D_MAXNUMEDGE) {
-        error("Too many edges!");
-      }
-
-      visitflags[vindex] = -vindex;
-      /* the new vertex is just a copy of vertex up */
-      c->vertices[3 * vindex + 0] = c->vertices[3 * up + 0];
-      c->vertices[3 * vindex + 1] = c->vertices[3 * up + 1];
-      c->vertices[3 * vindex + 2] = c->vertices[3 * up + 2];
-
-      /* as above, us stores the index of the last edge NOT below the plane */
-      us = i;
-
-      /* copy all edges below the plane into the new vertex, starting from edge
-         1 (edge 0 will be connected to a newly created vertex below)
-         We have to do this in two steps: first we copy the high index edges of
-         up, then the low index ones (since the edges below the plane are not a
-         continuous block of indices in this case) */
-      k = 1;
-      ++i;
-      safewhile(i < c->orders[up]) {
-        qp = voronoi_get_edge(c, up, i);
-        qs = voronoi_get_edgeindex(c, up, i);
-        voronoi_set_ngb(c, vindex, k, voronoi_get_ngb(c, up, i));
-        voronoi_set_edge(c, vindex, k, qp);
-        voronoi_set_edgeindex(c, vindex, k, qs);
-        voronoi_set_edge(c, qp, qs, vindex);
-        voronoi_set_edgeindex(c, qp, qs, k);
-        /* disconnect up, it will be removed */
-        voronoi_set_edge(c, up, i, -1);
-        ++i;
-        ++k;
-      }
-      i = 0;
-      safewhile(i < j) {
-        qp = voronoi_get_edge(c, up, i);
-        qs = voronoi_get_edgeindex(c, up, i);
-        voronoi_set_ngb(c, vindex, k, voronoi_get_ngb(c, up, i));
-        voronoi_set_edge(c, vindex, k, qp);
-        voronoi_set_edgeindex(c, vindex, k, qs);
-        voronoi_set_edge(c, qp, qs, vindex);
-        voronoi_set_edgeindex(c, qp, qs, k);
-        voronoi_set_edge(c, up, i, -1);
-        ++i;
-        ++k;
-      }
-      /* qs stores the index of the first edge not below the plane */
-      qs = j;
-    }
-
-    /* at this point, we have created a new vertex that contains all edges of up
-       below the plane, and two dangling edges: 0 and k
-       Furthermore, us stores the index of the last edge not below the plane,
-       qs the index of the first edge not below the plane */
-
-    /* now set the neighbours for the dangling edge(s) */
-    if (!double_edge) {
-      /* the last edge has the same neighbour as the first edge not below the
-         plane */
-      voronoi_set_ngb(c, vindex, k, voronoi_get_ngb(c, up, qs));
-      /* the first edge has the new neighbour as neighbour */
-      voronoi_set_ngb(c, vindex, 0, ngb);
-    } else {
-      /* up is copied as is, so we also copy its last remaining neighbour */
-      voronoi_set_ngb(c, vindex, 0, voronoi_get_ngb(c, up, qs));
-    }
-
-    /* add up to the delete stack */
-    dstack[dstack_size] = up;
-    ++dstack_size;
-
-    /* make sure the variables below have the same meaning as they would have
-       if we had the non complicated setup:
-       cs contains the index of the last dangling edge of the new vertex
-       qp and q correspond to the last vertex that has been deleted
-       qs corresponds to the first edge not below the plane
-       up and us correspond to the last edge not below the plane, i.e. the edge
-       that will be the last one to connect to the new vertex
-       note that the value of i is ignored below, it is just used to temporary
-       store the new value of up */
-    cs = k;
-    qp = up;
-    q = u;
-    i = voronoi_get_edge(c, up, us);
-    us = voronoi_get_edgeindex(c, up, us);
-    up = i;
-    /* we store the index of the newly created vertex in the visitflags of the
-       last deleted vertex */
-    visitflags[qp] = vindex;
-  } else { /* if(complicated) */
-
-    if (u == l) {
-      error("Upper and lower vertex are the same!");
-    }
-
-    /* the line joining up and lp has general (vector) equation
-         x = lp + (up-lp)*t,
-       with t a parameter ranging from 0 to 1
-       we can rewrite this as
-         x = lp*(1-t) + up*t
-       the value for t corresponding to the intersection of the line and the
-       midplane can be found as the ratio of the projected distance between one
-       of the vertices and the midplane, and the total projected distance
-       between the two vertices: u-l (remember that u > 0 and l < 0) */
-    r = u / (u - l);
-    l = 1.0f - r;
-
-    if (r > FLT_MAX || r < -FLT_MAX || l > FLT_MAX || l < -FLT_MAX) {
-      error("Value overflow (r: %g, l: %g)", r, l);
-    }
-
-    /* create a new order 3 vertex */
-    vindex = c->nvert;
-    ++c->nvert;
-    if (c->nvert == VORONOI3D_MAXNUMVERT) {
-      error("Too many vertices!");
-    }
-    c->orders[vindex] = 3;
-    c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-    if (c->offsets[vindex] + 3 >= VORONOI3D_MAXNUMEDGE) {
-      error("Too many edges!");
-    }
-
-    visitflags[vindex] = -vindex;
-    c->vertices[3 * vindex + 0] =
-        c->vertices[3 * lp + 0] * r + c->vertices[3 * up + 0] * l;
-    c->vertices[3 * vindex + 1] =
-        c->vertices[3 * lp + 1] * r + c->vertices[3 * up + 1] * l;
-    c->vertices[3 * vindex + 2] =
-        c->vertices[3 * lp + 2] * r + c->vertices[3 * up + 2] * l;
-
-    /* add vertex up to the delete stack */
-    dstack[dstack_size] = up;
-    ++dstack_size;
-
-    /* connect the new vertex to lp (and update lp as well) */
-    voronoi_set_edge(c, vindex, 1, lp);
-    voronoi_set_edgeindex(c, vindex, 1, ls);
-    voronoi_set_edge(c, lp, ls, vindex);
-    voronoi_set_edgeindex(c, lp, ls, 1);
-    /* disconnect vertex up, it will be deleted */
-    voronoi_set_edge(c, up, us, -1);
-    /* note that we do not connect edges 0 and 2: edge 2 will be connected to
-       the next new vertex that we created, while edge 0 will be connected to
-       the last new vertex */
-
-    /* set neighbour relations for the new vertex:
-        - edge 0 will be connected to the next intersection point (below), and
-          hence has pj as ngb
-        - edge 1 is connected to lp and has the original neighbour of the
-          intersected edge corresponding to up as neighbour
-        - edge 2 has the neighbour on the other side of the original intersected
-          edge as neighbour, which is the same as the neighbour of the edge
-          corresponding to lp */
-    voronoi_set_ngb(c, vindex, 0, ngb);
-    voronoi_set_ngb(c, vindex, 1, voronoi_get_ngb(c, up, us));
-    voronoi_set_ngb(c, vindex, 2, voronoi_get_ngb(c, lp, ls));
-
-    qs = us + 1;
-    if (qs == c->orders[up]) {
-      qs = 0;
-    }
-    qp = up;
-    q = u;
-
-    cs = 2;
-
-  } /* if(complicated) */
-
-  /* at this point:
-      qp corresponds to the last vertex that has been deleted
-      up corresponds to the last vertex that should be used to connect a new
-      vertex to the newly created vertex above. In the normal case, qp and up
-      are the same vertex, but qp and up can be different if the newly created
-      vertex lies in the midplane
-      qs contains the index of the edge of qp that is next in line to be tested:
-      the edge that comes after the intersected edge that was deleted above
-      us corresponds to the edge of up that was connected to the vertex that is
-      now connected to the newly created vertex above
-      q contains the projected distance between qp and the midplane, along dx
-      cs contains the index of the last dangling edge of the last vertex that
-      was created above; we still need to connect this edge to a vertex below */
-
-  /* we have found one intersected edge (or at least an edge that lies inside
-     the midplane) and created one new vertex that lies in the midplane, with
-     dangling edges. We now try to find other intersected edges and create other
-     new vertices that will be connected to the first new vertex. */
-
-  int cp = -1;
-  int iqs = -1;
-  int new_double_edge = -1;
-
-  /* cp and rp both contain the index of the last vertex that was created
-     cp will be updated if we add more vertices, rp will be kept, as we need it
-     to link the last new vertex to the first new vertex in the end */
-  cp = vindex;
-  rp = vindex;
-  /* we traverse connections of the first removed vertex, until we arrive at an
-     edge that links to this vertex (or its equivalent in the degenerate
-     case) */
-  safewhile(qp != up || qs != us) {
-    /* test the next edge of qp */
-    lp = voronoi_get_edge(c, qp, qs);
-    lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                             &teststack_size);
-    if (lw == 0) {
-
-      /* degenerate case: next vertex lies inside the plane */
-
-      k = 2;
-      if (double_edge) {
-        k = 1;
-      }
-      /* store the vertex and edge on the other side of the edge in qp and qs */
-      qs = voronoi_get_edgeindex(c, qp, qs);
-      qp = lp;
-
-      /* move on to the next edge of qp and keep the original edge for
-         reference */
-      iqs = qs;
-      ++qs;
-      if (qs == c->orders[qp]) {
-        qs = 0;
-      }
-
-      /* test the next edges, and try to find one that does NOT lie below the
-         plane */
-      lp = voronoi_get_edge(c, qp, qs);
-      lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                               &teststack_size);
-      safewhile(lw == -1) {
-        ++k;
-        ++qs;
-        if (qs == c->orders[qp]) {
-          qs = 0;
-        }
-        lp = voronoi_get_edge(c, qp, qs);
-        lw = voronoi_test_vertex(&c->vertices[3 * lp], dx, r2, &l, teststack,
-                                 &teststack_size);
-      }
-
-      /* qs now contains the next edge NOT below the plane
-         k contains the order of the new vertex to create: the number of edges
-         below the plane + 2 (+1 if we have a double edge) */
-
-      /* if qp (the vertex in the plane) was already visited before, visitflags
-         will contain the index of the newly created vertex that replaces it */
-      j = visitflags[qp];
-
-      /* we need to find out what the order of the new vertex will be, and if we
-         are dealing with a new double edge or not */
-      if (qp == up && qs == us) {
-        new_double_edge = 0;
-        if (j > 0) {
-          k += c->orders[j];
-        }
-      } else {
-        if (j > 0) {
-          k += c->orders[j];
-          if (lw == 0) {
-            i = -visitflags[lp];
-            if (i > 0) {
-              if (voronoi_get_edge(c, i, c->orders[i] - 1) == j) {
-                new_double_edge = 1;
-                --k;
-              } else {
-                new_double_edge = 0;
-              }
-            } else {
-              if (j == rp && lp == up && voronoi_get_edge(c, qp, qs) == us) {
-                new_double_edge = 1;
-                --k;
-              } else {
-                new_double_edge = 0;
-              }
-            }
-          } else {
-            new_double_edge = 0;
-          }
-        } else {
-          if (lw == 0) {
-            i = -visitflags[lp];
-            if (i == cp) {
-              new_double_edge = 1;
-              --k;
-            } else {
-              new_double_edge = 0;
-            }
-          } else {
-            new_double_edge = 0;
-          }
-        }
-      }
-
-      //      if (j > 0) {
-      //        error("Case not handled!");
-      //      }
-
-      /* create new order k vertex */
-      vindex = c->nvert;
-      ++c->nvert;
-      if (c->nvert == VORONOI3D_MAXNUMVERT) {
-        error("Too many vertices!");
-      }
-      c->orders[vindex] = k;
-      c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-      if (c->offsets[vindex] + k >= VORONOI3D_MAXNUMEDGE) {
-        error("Too many edges!");
-      }
-
-      visitflags[vindex] = -vindex;
-      c->vertices[3 * vindex + 0] = c->vertices[3 * qp + 0];
-      c->vertices[3 * vindex + 1] = c->vertices[3 * qp + 1];
-      c->vertices[3 * vindex + 2] = c->vertices[3 * qp + 2];
-
-      visitflags[qp] = vindex;
-      dstack[dstack_size] = qp;
-      ++dstack_size;
-      j = vindex;
-      i = 0;
-
-      if (!double_edge) {
-        voronoi_set_ngb(c, j, i, ngb);
-        voronoi_set_edge(c, j, i, cp);
-        voronoi_set_edgeindex(c, j, i, cs);
-        voronoi_set_edge(c, cp, cs, j);
-        voronoi_set_edgeindex(c, cp, cs, i);
-        ++i;
-      }
-
-      qs = iqs;
-      iqs = k - 1;
-      if (new_double_edge) {
-        iqs = k;
-      }
-      safewhile(i < iqs) {
-        ++qs;
-        if (qs == c->orders[qp]) {
-          qs = 0;
-        }
-        lp = voronoi_get_edge(c, qp, qs);
-        ls = voronoi_get_edgeindex(c, qp, qs);
-        voronoi_set_ngb(c, j, i, voronoi_get_ngb(c, qp, qs));
-        voronoi_set_edge(c, j, i, lp);
-        voronoi_set_edgeindex(c, j, i, ls);
-        voronoi_set_edge(c, lp, ls, j);
-        voronoi_set_edgeindex(c, lp, ls, i);
-        voronoi_set_edge(c, qp, qs, -1);
-        ++i;
-      }
-      ++qs;
-      if (qs == c->orders[qp]) {
-        qs = 0;
-      }
-      cs = i;
-      cp = j;
-
-      if (new_double_edge) {
-        voronoi_set_ngb(c, j, 0, voronoi_get_ngb(c, qp, qs));
-      } else {
-        voronoi_set_ngb(c, j, cs, voronoi_get_ngb(c, qp, qs));
-      }
-
-      double_edge = new_double_edge;
-    } else { /* if(lw == 0) */
-
-      /* normal case: next vertex lies below or above the plane */
-
-      if (lw == 1) {
-
-        /* vertex lies above the plane */
-
-        /* we just delete the vertex and continue with the next edge of this
-           vertex */
-
-        qs = voronoi_get_edgeindex(c, qp, qs) + 1;
-        if (qs == c->orders[lp]) {
-          qs = 0;
-        }
-        qp = lp;
-        q = l;
-        dstack[dstack_size] = qp;
-        ++dstack_size;
-      } else {
-
-        /* vertex lies below the plane */
-
-        /* we have found our next intersected edge: create a new vertex and link
-           it to the other vertices */
-
-        if (q == l) {
-          error("Upper and lower vertex are the same!");
-        }
-
-        r = q / (q - l);
-        l = 1.0f - r;
-
-        if (r > FLT_MAX || r < -FLT_MAX || l > FLT_MAX || l < -FLT_MAX) {
-          error("Value out of bounds (r: %g, l: %g)!", r, l);
-        }
-
-        /* create new order 3 vertex */
-        vindex = c->nvert;
-        ++c->nvert;
-        if (c->nvert == VORONOI3D_MAXNUMVERT) {
-          error("Too many vertices!");
-        }
-        visitflags[vindex] = -vindex;
-        c->orders[vindex] = 3;
-        c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-        if (c->offsets[vindex] + 3 >= VORONOI3D_MAXNUMEDGE) {
-          error("Too many edges!");
-        }
-
-        c->vertices[3 * vindex + 0] =
-            c->vertices[3 * lp + 0] * r + c->vertices[3 * qp + 0] * l;
-        c->vertices[3 * vindex + 1] =
-            c->vertices[3 * lp + 1] * r + c->vertices[3 * qp + 1] * l;
-        c->vertices[3 * vindex + 2] =
-            c->vertices[3 * lp + 2] * r + c->vertices[3 * qp + 2] * l;
-
-        /* link the edges:
-           the first edge is connected to the last edge of the previous new
-           vertex. The last edge will be connected to the next new vertex, and
-           is left open for the moment */
-        ls = voronoi_get_edgeindex(c, qp, qs);
-        voronoi_set_edge(c, vindex, 0, cp);
-        voronoi_set_edge(c, vindex, 1, lp);
-        voronoi_set_edgeindex(c, vindex, 0, cs);
-        voronoi_set_edgeindex(c, vindex, 1, ls);
-        voronoi_set_edge(c, lp, ls, vindex);
-        voronoi_set_edgeindex(c, lp, ls, 1);
-        voronoi_set_edge(c, cp, cs, vindex);
-        voronoi_set_edgeindex(c, cp, cs, 0);
-        voronoi_set_edge(c, qp, qs, -1);
-
-        voronoi_set_ngb(c, vindex, 0, ngb);
-        voronoi_set_ngb(c, vindex, 1, voronoi_get_ngb(c, qp, qs));
-        voronoi_set_ngb(c, vindex, 2, voronoi_get_ngb(c, lp, ls));
-
-        /* continue with the next edge of qp (the last vertex above the
-           midplane */
-        ++qs;
-        if (qs == c->orders[qp]) {
-          qs = 0;
-        }
-        /* store the last newly created vertex and its dangling edge for the
-           next iteration */
-        cp = vindex;
-        cs = 2;
-      } /* if(lw == 1) */
-
-    } /* if(lw == 0) */
-
-  } /* while() */
-
-  /* we finished adding new vertices. Now connect the last dangling edge of the
-     last newly created vertex to the first dangling edge of the first newly
-     created vertex */
-  voronoi_set_edge(c, cp, cs, rp);
-  voronoi_set_edge(c, rp, 0, cp);
-  voronoi_set_edgeindex(c, cp, cs, 0);
-  voronoi_set_edgeindex(c, rp, 0, cs);
-
-  /* now remove the vertices in the delete stack */
-
-  /* the algorithm above did not necessarily visit all vertices above the plane.
-     here we scan for vertices that are linked to vertices that are to be
-     removed and add them to the delete stack if necessary
-     this only works because we made sure that all deleted vertices no longer
-     have edges that connect them to vertices that need to stay */
-  for (i = 0; i < dstack_size; ++i) {
-    for (j = 0; j < c->orders[dstack[i]]; ++j) {
-      if (voronoi_get_edge(c, dstack[i], j) >= 0) {
-        dstack[dstack_size] = voronoi_get_edge(c, dstack[i], j);
-        ++dstack_size;
-        voronoi_set_edge(c, dstack[i], j, -1);
-        voronoi_set_edgeindex(c, dstack[i], j, -1);
-      }
-    }
-  }
-
-  /* collapse order 1 and 2 vertices: vertices with only 1 edge or 2 edges that
-     can be created during the plane intersection routine */
-  /* first flag them */
-  int low_order_stack[VORONOI3D_MAXNUMVERT];
-  int low_order_index = 0;
-  for (i = 0; i < c->nvert; ++i) {
-    if (voronoi_get_edge(c, i, 0) >= 0 && c->orders[i] < 3) {
-      low_order_stack[low_order_index] = i;
-      ++low_order_index;
-    }
-  }
-
-  /* now remove them */
-  safewhile(low_order_index) {
-    int v = low_order_stack[low_order_index - 1];
-    /* the vertex might already have been deleted by a previous operation */
-    if (voronoi_get_edge(c, v, 0) < 0) {
-      --low_order_index;
-      continue;
-    }
-    if (c->orders[v] == 2) {
-      int jj = voronoi_get_edge(c, v, 0);
-      int kk = voronoi_get_edge(c, v, 1);
-      int bb = voronoi_get_edgeindex(c, v, 1);
-      int ll = 0;
-      safewhile(ll < c->orders[jj] && voronoi_get_edge(c, jj, ll) != kk) {
-        ++ll;
-      }
-      if (ll == c->orders[jj]) {
-        int a = voronoi_get_edgeindex(c, v, 0);
-        /* jj and kk are not joined together. Replace their edges pointing to v
-           with a new edge pointing from jj to kk */
-        voronoi_set_edge(c, jj, a, k);
-        voronoi_set_edgeindex(c, jj, a, bb);
-        voronoi_set_edge(c, kk, bb, jj);
-        voronoi_set_edgeindex(c, kk, bb, a);
-        /* no new elements added to the stack: decrease the counter */
-        --low_order_index;
-      } else {
-        /* just remove the edges from jj to v and from kk to v: create two new
-           vertices */
-        /* vertex jj */
-        vindex = c->nvert;
-        ++c->nvert;
-        c->vertices[3 * vindex] = c->vertices[3 * jj];
-        c->vertices[3 * vindex + 1] = c->vertices[3 * jj + 1];
-        c->vertices[3 * vindex + 2] = c->vertices[3 * jj + 2];
-        c->orders[vindex] = c->orders[jj] - 1;
-        c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-        int m = 0;
-        for (int n = 0; n < c->orders[jj]; ++n) {
-          int lll = voronoi_get_edge(c, jj, n);
-          if (lll != v) {
-            /* make a new edge */
-            voronoi_set_edge(c, vindex, m, lll);
-            voronoi_set_edgeindex(c, vindex, m,
-                                  voronoi_get_edgeindex(c, jj, n));
-            /* update the other vertex */
-            voronoi_set_edge(c, lll, voronoi_get_edgeindex(c, jj, n), vindex);
-            voronoi_set_edgeindex(c, lll, voronoi_get_edgeindex(c, jj, n), m);
-            /* copy ngb information */
-            voronoi_set_ngb(c, vindex, m, voronoi_get_ngb(c, jj, n));
-            ++m;
-          }
-          /* remove the old vertex */
-          voronoi_set_edge(c, jj, n, -1);
-          voronoi_set_edgeindex(c, jj, n, -1);
-        }
-        /* vertex kk */
-        vindex = c->nvert;
-        ++c->nvert;
-        c->vertices[3 * vindex] = c->vertices[3 * kk];
-        c->vertices[3 * vindex + 1] = c->vertices[3 * kk + 1];
-        c->vertices[3 * vindex + 2] = c->vertices[3 * kk + 2];
-        c->orders[vindex] = c->orders[kk] - 1;
-        c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-        m = 0;
-        for (int n = 0; n < c->orders[kk]; ++n) {
-          int lll = voronoi_get_edge(c, kk, n);
-          if (lll != v) {
-            /* make a new edge */
-            voronoi_set_edge(c, vindex, m, lll);
-            voronoi_set_edgeindex(c, vindex, m,
-                                  voronoi_get_edgeindex(c, kk, n));
-            /* update the other vertex */
-            voronoi_set_edge(c, lll, voronoi_get_edgeindex(c, kk, n), vindex);
-            voronoi_set_edgeindex(c, lll, voronoi_get_edgeindex(c, kk, n), m);
-            /* copy ngb information */
-            /* this one is special: we copy the ngb corresponding to the
-               deleted edge and skip the one after that */
-            if (n == bb + 1) {
-              voronoi_set_ngb(c, vindex, m, voronoi_get_ngb(c, kk, bb));
-            } else {
-              voronoi_set_ngb(c, vindex, m, voronoi_get_ngb(c, kk, n));
-            }
-            ++m;
-          }
-          /* remove the old vertex */
-          voronoi_set_edge(c, kk, n, -1);
-          voronoi_set_edgeindex(c, kk, n, -1);
-        }
-        /* check if jj or kk has become an order 2 vertex */
-        /* if they have become an order 1 vertex, they were already an order 2
-           vertex, and they should already be in the list... */
-        if (c->orders[vindex] == 2) {
-          if (c->orders[vindex - 1] == 2) {
-            low_order_stack[low_order_index] = vindex - 1;
-            ++low_order_index;
-            low_order_stack[low_order_index] = vindex;
-            /* we do not increase the index here: we want this element to be the
-               next element that is processed */
-          } else {
-            low_order_stack[low_order_index] = vindex;
-          }
-        } else {
-          if (c->orders[vindex - 1] == 2) {
-            low_order_stack[low_order_index] = vindex - 1;
-          } else {
-            /* no new vertices added to the stack: decrease the counter */
-            --low_order_index;
-          }
-        }
-      }
-      /* Remove the vertex */
-      voronoi_set_edge(c, v, 0, -1);
-      voronoi_set_edgeindex(c, v, 0, -1);
-      voronoi_set_edge(c, v, 1, -1);
-      voronoi_set_edgeindex(c, v, 1, -1);
-    } else if (c->orders[v] == 1) {
-      int jj = voronoi_get_edge(c, v, 0);
-      /* we have to remove the edge between j and v. We create a new vertex */
-      vindex = c->nvert;
-      ++c->nvert;
-      c->vertices[3 * vindex] = c->vertices[3 * jj];
-      c->vertices[3 * vindex + 1] = c->vertices[3 * jj + 1];
-      c->vertices[3 * vindex + 2] = c->vertices[3 * jj + 2];
-      c->orders[vindex] = c->orders[j] - 1;
-      c->offsets[vindex] = c->offsets[vindex - 1] + c->orders[vindex - 1];
-      int m = 0;
-      for (int kk = 0; kk < c->orders[j]; ++kk) {
-        int ll = voronoi_get_edge(c, jj, kk);
-        if (ll != v) {
-          /* make a new edge */
-          voronoi_set_edge(c, vindex, m, ll);
-          voronoi_set_edgeindex(c, vindex, m, voronoi_get_edgeindex(c, jj, kk));
-          /* update the other vertex */
-          voronoi_set_edge(c, ll, voronoi_get_edgeindex(c, jj, kk), vindex);
-          voronoi_set_edgeindex(c, ll, voronoi_get_edgeindex(c, jj, kk), m);
-          /* copy ngb information */
-          voronoi_set_ngb(c, vindex, m, voronoi_get_ngb(c, jj, kk));
-          ++m;
-        }
-        /* remove the old vertex */
-        voronoi_set_edge(c, jj, kk, -1);
-        voronoi_set_edgeindex(c, jj, kk, -1);
-      }
-      /* if the new vertex is a new order 2 vertex, add it to the stack */
-      if (c->orders[vindex] == 2) {
-        low_order_stack[low_order_index - 1] = vindex;
-      } else {
-        --low_order_index;
-      }
-      /* remove the order 1 vertex */
-      voronoi_set_edge(c, v, 0, -1);
-      voronoi_set_edgeindex(c, v, 0, -1);
-    } else {
-      error("Vertex with order %i. This should not happen!", c->orders[v]);
-    }
-  }
-
-  /* remove deleted vertices from all arrays */
-  struct voronoi_cell new_cell;
-  /* make sure the contents of the new cell are the same as for the old cell */
-  memcpy(&new_cell, c, sizeof(struct voronoi_cell));
-  int m, n;
-  for (vindex = 0; vindex < c->nvert; ++vindex) {
-    j = vindex;
-    /* find next edge that is not deleted */
-    safewhile(j < c->nvert && voronoi_get_edge(c, j, 0) < 0) { ++j; }
-
-    if (j == c->nvert) {
-      /* ready */
-      break;
-    }
-
-    /* copy vertices */
-    new_cell.vertices[3 * vindex + 0] = c->vertices[3 * j + 0];
-    new_cell.vertices[3 * vindex + 1] = c->vertices[3 * j + 1];
-    new_cell.vertices[3 * vindex + 2] = c->vertices[3 * j + 2];
-
-    /* copy order */
-    new_cell.orders[vindex] = c->orders[j];
-
-    /* set offset */
-    if (vindex) {
-      new_cell.offsets[vindex] =
-          new_cell.offsets[vindex - 1] + new_cell.orders[vindex - 1];
-    } else {
-      new_cell.offsets[vindex] = 0;
-    }
-
-    /* copy edges, edgeindices and ngbs */
-    for (k = 0; k < c->orders[j]; ++k) {
-      voronoi_set_edge(&new_cell, vindex, k, voronoi_get_edge(c, j, k));
-      voronoi_set_edgeindex(&new_cell, vindex, k,
-                            voronoi_get_edgeindex(c, j, k));
-      voronoi_set_ngb(&new_cell, vindex, k, voronoi_get_ngb(c, j, k));
-    }
-
-    /* update other edges */
-    for (k = 0; k < c->orders[j]; ++k) {
-      m = voronoi_get_edge(c, j, k);
-      n = voronoi_get_edgeindex(c, j, k);
-      if (m < vindex) {
-        voronoi_set_edge(&new_cell, m, n, vindex);
-      } else {
-        voronoi_set_edge(c, m, n, vindex);
-      }
-    }
-
-    /* deactivate edge */
-    voronoi_set_edge(c, j, 0, -1);
-  }
-  new_cell.nvert = vindex;
-
-  new_cell.x[0] = c->x[0];
-  new_cell.x[1] = c->x[1];
-  new_cell.x[2] = c->x[2];
-  new_cell.centroid[0] = c->centroid[0];
-  new_cell.centroid[1] = c->centroid[1];
-  new_cell.centroid[2] = c->centroid[2];
-  new_cell.volume = c->volume;
-  new_cell.nface = c->nface;
-
-  /* Update the cell values. */
-  voronoi3d_cell_copy(&new_cell, c);
-
-#ifdef VORONOI3D_EXPENSIVE_CHECKS
-  voronoi_check_cell_consistency(c);
-#endif
-}
-
-/**
- * @brief Get the volume of the tetrahedron made up by the four given vertices.
- *
- * The vertices are not expected to be oriented in a specific way. If the input
- * happens to be coplanar or colinear, the returned volume will just be zero.
- *
- * @param v1 First vertex.
- * @param v2 Second vertex.
- * @param v3 Third vertex.
- * @param v4 Fourth vertex.
- * @return Volume of the tetrahedron.
- */
-__attribute__((always_inline)) INLINE float voronoi_volume_tetrahedron(
-    const float *v1, const float *v2, const float *v3, const float *v4) {
-
-  float V;
-  float r1[3], r2[3], r3[3];
-
-  r1[0] = v2[0] - v1[0];
-  r1[1] = v2[1] - v1[1];
-  r1[2] = v2[2] - v1[2];
-  r2[0] = v3[0] - v1[0];
-  r2[1] = v3[1] - v1[1];
-  r2[2] = v3[2] - v1[2];
-  r3[0] = v4[0] - v1[0];
-  r3[1] = v4[1] - v1[1];
-  r3[2] = v4[2] - v1[2];
-  V = fabs(r1[0] * r2[1] * r3[2] + r1[1] * r2[2] * r3[0] +
-           r1[2] * r2[0] * r3[1] - r1[2] * r2[1] * r3[0] -
-           r2[2] * r3[1] * r1[0] - r3[2] * r1[1] * r2[0]);
-  V /= 6.;
-  return V;
-}
-
-/**
- * @brief Get the centroid of the tetrahedron made up by the four given
- * vertices.
- *
- * The centroid is just the average of four vertex coordinates.
- *
- * @param centroid Array to store the centroid in.
- * @param v1 First vertex.
- * @param v2 Second vertex.
- * @param v3 Third vertex.
- * @param v4 Fourth vertex.
- */
-__attribute__((always_inline)) INLINE void voronoi_centroid_tetrahedron(
-    float *centroid, const float *v1, const float *v2, const float *v3,
-    const float *v4) {
-
-  centroid[0] = 0.25f * (v1[0] + v2[0] + v3[0] + v4[0]);
-  centroid[1] = 0.25f * (v1[1] + v2[1] + v3[1] + v4[1]);
-  centroid[2] = 0.25f * (v1[2] + v2[2] + v3[2] + v4[2]);
-}
-
-/**
- * @brief Calculate the volume and centroid of a 3D Voronoi cell.
- *
- * @param cell 3D Voronoi cell.
- */
-__attribute__((always_inline)) INLINE void voronoi_calculate_cell(
-    struct voronoi_cell *cell) {
-
-  float v1[3], v2[3], v3[3], v4[3];
-  int i, j, k, l, m, n;
-  float tcentroid[3];
-  float tvol;
-
-  /* we need to calculate the volume of the tetrahedra formed by the first
-     vertex and the triangles that make up the other faces
-     since we do not store faces explicitly, this means keeping track of the
-     edges that have been processed somehow
-     we follow the method used in voro++ and "flip" processed edges to
-     negative values
-     this also means that we need to process all triangles corresponding to
-     an edge at once */
-  cell->volume = 0.0f;
-  v1[0] = cell->vertices[0];
-  v1[1] = cell->vertices[1];
-  v1[2] = cell->vertices[2];
-  cell->centroid[0] = 0.0f;
-  cell->centroid[1] = 0.0f;
-  cell->centroid[2] = 0.0f;
-
-  /* loop over all vertices (except the first one) */
-  for (i = 1; i < cell->nvert; ++i) {
-
-    v2[0] = cell->vertices[3 * i + 0];
-    v2[1] = cell->vertices[3 * i + 1];
-    v2[2] = cell->vertices[3 * i + 2];
-
-    /*  loop over the edges of the vertex*/
-    for (j = 0; j < cell->orders[i]; ++j) {
-
-      k = voronoi_get_edge(cell, i, j);
-
-      if (k >= 0) {
-
-        /* mark the edge as processed */
-        voronoi_set_edge(cell, i, j, -k - 1);
-
-        l = voronoi_get_edgeindex(cell, i, j) + 1;
-        if (l == cell->orders[k]) {
-          l = 0;
-        }
-        v3[0] = cell->vertices[3 * k + 0];
-        v3[1] = cell->vertices[3 * k + 1];
-        v3[2] = cell->vertices[3 * k + 2];
-        m = voronoi_get_edge(cell, k, l);
-        voronoi_set_edge(cell, k, l, -1 - m);
-
-        int loopcount = 0;
-        safewhile(m != i) {
-          if (loopcount == 999) {
-            voronoi_print_cell(cell);
-            voronoi_print_gnuplot_c(cell);
-          }
-          ++loopcount;
-          n = voronoi_get_edgeindex(cell, k, l) + 1;
-          if (n == cell->orders[m]) {
-            n = 0;
-          }
-          v4[0] = cell->vertices[3 * m + 0];
-          v4[1] = cell->vertices[3 * m + 1];
-          v4[2] = cell->vertices[3 * m + 2];
-          tvol = voronoi_volume_tetrahedron(v1, v2, v3, v4);
-          cell->volume += tvol;
-          voronoi_centroid_tetrahedron(tcentroid, v1, v2, v3, v4);
-          cell->centroid[0] += tcentroid[0] * tvol;
-          cell->centroid[1] += tcentroid[1] * tvol;
-          cell->centroid[2] += tcentroid[2] * tvol;
-          k = m;
-          l = n;
-          v3[0] = v4[0];
-          v3[1] = v4[1];
-          v3[2] = v4[2];
-          m = voronoi_get_edge(cell, k, l);
-          voronoi_set_edge(cell, k, l, -1 - m);
-        } /* while() */
-
-      } /* if(k >= 0) */
-
-    } /* for(j) */
-
-  } /* for(i) */
-
-  cell->centroid[0] /= cell->volume;
-  cell->centroid[1] /= cell->volume;
-  cell->centroid[2] /= cell->volume;
-
-  /* centroid was calculated relative w.r.t. particle position */
-  cell->centroid[0] += cell->x[0];
-  cell->centroid[1] += cell->x[1];
-  cell->centroid[2] += cell->x[2];
-
-  /* Reset the edges: we still need them for the face calculation */
-  for (i = 0; i < VORONOI3D_MAXNUMEDGE; ++i) {
-    if (cell->edges[i] < 0) {
-      cell->edges[i] = -1 - cell->edges[i];
-    }
-  }
-}
-
-/**
- * @brief Calculate the faces for a 3D Voronoi cell. This reorganizes the
- * internal variables of the cell, so no new neighbours can be added after
- * this method has been called!
- *
- * Note that the face midpoints are calculated relative w.r.t. the cell
- * generator!
- *
- * @param cell 3D Voronoi cell.
- */
-__attribute__((always_inline)) INLINE void voronoi_calculate_faces(
-    struct voronoi_cell *cell) {
-
-  int i, j, k, l, m, n;
-  float area;
-  float midpoint[3];
-  float u[3], v[3], w[3];
-  float loc_area;
-  unsigned long long newngbs[VORONOI3D_MAXNUMEDGE];
-
-  cell->nface = 0;
-  for (i = 0; i < cell->nvert; ++i) {
-
-    for (j = 0; j < cell->orders[i]; ++j) {
-
-      k = voronoi_get_edge(cell, i, j);
-
-      if (k >= 0) {
-
-        newngbs[cell->nface] = voronoi_get_ngb(cell, i, j);
-        area = 0.;
-        midpoint[0] = 0.;
-        midpoint[1] = 0.;
-        midpoint[2] = 0.;
-        voronoi_set_edge(cell, i, j, -1 - k);
-        l = voronoi_get_edgeindex(cell, i, j) + 1;
-        if (l == cell->orders[k]) {
-          l = 0;
-        }
-        m = voronoi_get_edge(cell, k, l);
-        voronoi_set_edge(cell, k, l, -1 - m);
-
-        safewhile(m != i) {
-          n = voronoi_get_edgeindex(cell, k, l) + 1;
-          if (n == cell->orders[m]) {
-            n = 0;
-          }
-          u[0] = cell->vertices[3 * k + 0] - cell->vertices[3 * i + 0];
-          u[1] = cell->vertices[3 * k + 1] - cell->vertices[3 * i + 1];
-          u[2] = cell->vertices[3 * k + 2] - cell->vertices[3 * i + 2];
-          v[0] = cell->vertices[3 * m + 0] - cell->vertices[3 * i + 0];
-          v[1] = cell->vertices[3 * m + 1] - cell->vertices[3 * i + 1];
-          v[2] = cell->vertices[3 * m + 2] - cell->vertices[3 * i + 2];
-          w[0] = u[1] * v[2] - u[2] * v[1];
-          w[1] = u[2] * v[0] - u[0] * v[2];
-          w[2] = u[0] * v[1] - u[1] * v[0];
-          loc_area = sqrtf(w[0] * w[0] + w[1] * w[1] + w[2] * w[2]);
-          area += loc_area;
-          midpoint[0] += loc_area * (cell->vertices[3 * k + 0] +
-                                     cell->vertices[3 * i + 0] +
-                                     cell->vertices[3 * m + 0]);
-          midpoint[1] += loc_area * (cell->vertices[3 * k + 1] +
-                                     cell->vertices[3 * i + 1] +
-                                     cell->vertices[3 * m + 1]);
-          midpoint[2] += loc_area * (cell->vertices[3 * k + 2] +
-                                     cell->vertices[3 * i + 2] +
-                                     cell->vertices[3 * m + 2]);
-          k = m;
-          l = n;
-          m = voronoi_get_edge(cell, k, l);
-          voronoi_set_edge(cell, k, l, -1 - m);
-        }
-
-        cell->face_areas[cell->nface] = 0.5f * area;
-        cell->face_midpoints[cell->nface][0] = midpoint[0] / area / 3.0f;
-        cell->face_midpoints[cell->nface][1] = midpoint[1] / area / 3.0f;
-        cell->face_midpoints[cell->nface][2] = midpoint[2] / area / 3.0f;
-        ++cell->nface;
-
-        if (cell->nface == VORONOI3D_MAXFACE) {
-          error("Too many faces!");
-        }
-
-      } /* if(k >= 0) */
-
-    } /* for(j) */
-
-  } /* for(i) */
-
-  /* Overwrite the old neighbour array. */
-  for (i = 0; i < cell->nface; ++i) {
-    cell->ngbs[i] = newngbs[i];
-  }
-}
-
-/*******************************************************************************
- * voronoi_algorithm interface implementations
- *
- * If you change any function parameters below, you also have to change them in
- * the 1D and 2D algorithm!
- ******************************************************************************/
-
-/**
- * @brief Initialize a 3D Voronoi cell.
- *
- * @param cell 3D Voronoi cell to initialize.
- * @param x Position of the generator of the cell.
- * @param anchor Anchor of the simulation box.
- * @param side Side lengths of the simulation box.
- */
-__attribute__((always_inline)) INLINE void voronoi_cell_init(
-    struct voronoi_cell *cell, const double *x, const double *anchor,
-    const double *side) {
-
-  cell->x[0] = x[0];
-  cell->x[1] = x[1];
-  cell->x[2] = x[2];
-
-  voronoi_initialize(cell, anchor, side);
-
-  cell->volume = 0.0f;
-  cell->centroid[0] = 0.0f;
-  cell->centroid[1] = 0.0f;
-  cell->centroid[2] = 0.0f;
-  cell->nface = 0;
-}
-
-/**
- * @brief Interact a 3D Voronoi cell with a particle with given relative
- * position and ID.
- *
- * @param cell 3D Voronoi cell.
- * @param dx Relative position of the interacting generator w.r.t. the cell
- * generator (in fact: dx = generator - neighbour).
- * @param id ID of the interacting neighbour.
- */
-__attribute__((always_inline)) INLINE void voronoi_cell_interact(
-    struct voronoi_cell *cell, const float *dx, unsigned long long id) {
-
-  voronoi_intersect(cell, dx, id);
-}
-
-/**
- * @brief Finalize a 3D Voronoi cell.
- *
- * @param cell 3D Voronoi cell.
- * @return Maximal radius that could still change the structure of the cell.
- */
-__attribute__((always_inline)) INLINE float voronoi_cell_finalize(
-    struct voronoi_cell *cell) {
-
-  int i;
-  float max_radius, v[3], v2;
-
-  /* Calculate the volume and centroid of the cell. */
-  voronoi_calculate_cell(cell);
-  /* Calculate the faces. */
-  voronoi_calculate_faces(cell);
-
-  /* Loop over the vertices and calculate the maximum radius. */
-  max_radius = 0.0f;
-  for (i = 0; i < cell->nvert; ++i) {
-    v[0] = cell->vertices[3 * i];
-    v[1] = cell->vertices[3 * i + 1];
-    v[2] = cell->vertices[3 * i + 2];
-    v2 = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
-    max_radius = fmaxf(max_radius, v2);
-  }
-  max_radius = sqrtf(max_radius);
-
-  return 2.0f * max_radius;
-}
-
-/**
- * @brief Get the surface area and midpoint of the face between a 3D Voronoi
- * cell and the given neighbour.
- *
- * @param cell 3D Voronoi cell.
- * @param ngb ID of a particle that is possibly a neighbour of this cell.
- * @param midpoint Array to store the relative position of the face in.
- * @return 0 if the given neighbour is not a neighbour, the surface area of
- * the face otherwise.
- */
-__attribute__((always_inline)) INLINE float voronoi_get_face(
-    const struct voronoi_cell *cell, unsigned long long ngb, float *midpoint) {
-
-  int i = 0;
-  while (i < cell->nface && cell->ngbs[i] != ngb) {
-    ++i;
-  }
-  if (i == cell->nface) {
-    /* Ngb not found */
-    return 0.0f;
-  }
-
-  midpoint[0] = cell->face_midpoints[i][0];
-  midpoint[1] = cell->face_midpoints[i][1];
-  midpoint[2] = cell->face_midpoints[i][2];
-
-  return cell->face_areas[i];
-}
-
-/**
- * @brief Get the centroid of a 3D Voronoi cell.
- *
- * @param cell 3D Voronoi cell.
- * @param centroid Array to store the centroid in.
- */
-__attribute__((always_inline)) INLINE void voronoi_get_centroid(
-    const struct voronoi_cell *cell, float *centroid) {
-
-  centroid[0] = cell->centroid[0];
-  centroid[1] = cell->centroid[1];
-  centroid[2] = cell->centroid[2];
-}
-
-#endif  // SWIFT_VORONOIXD_ALGORITHM_H
diff --git a/src/hydro/Shadowswift/voronoi3d_cell.h b/src/hydro/Shadowswift/voronoi3d_cell.h
deleted file mode 100644
index ef43eff1745f48219af14aec2455aaa5e5b0d47a..0000000000000000000000000000000000000000
--- a/src/hydro/Shadowswift/voronoi3d_cell.h
+++ /dev/null
@@ -1,143 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-#ifndef SWIFT_VORONOIXD_CELL_H
-#define SWIFT_VORONOIXD_CELL_H
-
-/* Maximal number of neighbours that can be stored in a voronoi_cell struct */
-#define VORONOI3D_MAXNUMNGB 100
-/* Maximal number of vertices that can be stored in a voronoi_cell struct */
-#define VORONOI3D_MAXNUMVERT 500
-/* Maximal number of edges that can be stored in a voronoi_cell struct */
-#define VORONOI3D_MAXNUMEDGE 1500
-/* Maximal number of faces that can be stored in a voronoi_cell struct */
-#define VORONOI3D_MAXFACE 100
-
-/* 3D Voronoi cell */
-struct voronoi_cell {
-
-  /* The position of the generator of the cell. */
-  double x[3];
-
-  /* The volume of the 3D cell. */
-  float volume;
-
-  /* The centroid of the cell. */
-  float centroid[3];
-
-  /* Number of cell vertices. */
-  int nvert;
-
-  /* Vertex coordinates. */
-  float vertices[3 * VORONOI3D_MAXNUMVERT];
-
-  /* Number of edges for every vertex. */
-  char orders[VORONOI3D_MAXNUMVERT];
-
-  /* Offsets of the edges, edgeindices and neighbours corresponding to a
-     particular vertex in the internal arrays */
-  int offsets[VORONOI3D_MAXNUMVERT];
-
-  /* Edge information. Edges are ordered counterclockwise w.r.t. a vector
-     pointing from the cell generator to the vertex. */
-  int edges[VORONOI3D_MAXNUMEDGE];
-
-  /* Additional edge information. */
-  char edgeindices[VORONOI3D_MAXNUMEDGE];
-
-  /* Neighbour information. This field is used differently depending on where we
-     are in the algorithm. During cell construction, it contains, for every edge
-     of every vertex, the index of the neighbour that generates the face
-     counterclockwise of the edge w.r.t. a vector pointing from the vertex along
-     the edge. After cell finalization, it contains a neighbour for every face,
-     in the same order as the face_areas and face_midpoints arrays. */
-  unsigned long long ngbs[VORONOI3D_MAXNUMEDGE];
-
-  /* Number of faces of the cell. */
-  unsigned char nface;
-
-  /* Surface areas of the cell faces. */
-  float face_areas[VORONOI3D_MAXFACE];
-
-  /* Midpoints of the cell faces. */
-  float face_midpoints[VORONOI3D_MAXFACE][3];
-};
-
-/**
- * @brief Copy the contents of the 3D Voronoi cell pointed to by source into the
- * 3D Voronoi cell pointed to by destination
- *
- * @param source Pointer to a 3D Voronoi cell to read from.
- * @param destination Pointer to a 3D Voronoi cell to write to.
- */
-__attribute__((always_inline)) INLINE void voronoi3d_cell_copy(
-    struct voronoi_cell *source, struct voronoi_cell *destination) {
-
-  /* Copy the position of the generator of the cell. */
-  destination->x[0] = source->x[0];
-  destination->x[1] = source->x[1];
-  destination->x[2] = source->x[2];
-
-  /* Copy the volume of the 3D cell. */
-  destination->volume = source->volume;
-
-  /* Copy the centroid of the cell. */
-  destination->centroid[0] = source->centroid[0];
-  destination->centroid[1] = source->centroid[1];
-  destination->centroid[2] = source->centroid[2];
-
-  /* Copy the number of cell vertices. */
-  destination->nvert = source->nvert;
-
-  /* Copy the vertex coordinates. We only copy the 3*nvert first coordinates. */
-  for (int i = 0; i < 3 * source->nvert; ++i) {
-    destination->vertices[i] = source->vertices[i];
-  }
-
-  /* Copy the number of edges for every vertex. Again, we only copy the nvert
-     first values. */
-  for (int i = 0; i < source->nvert; ++i) {
-    destination->orders[i] = source->orders[i];
-  }
-
-  /* Copy the nvert first values of the offsets. */
-  for (int i = 0; i < source->nvert; ++i) {
-    destination->offsets[i] = source->offsets[i];
-  }
-
-  /* Copy the edge information. No idea how many edges we have, so we copy
-     everything. */
-  for (int i = 0; i < VORONOI3D_MAXNUMEDGE; ++i) {
-    destination->edges[i] = source->edges[i];
-  }
-
-  /* Copy all additional edge information. */
-  for (int i = 0; i < VORONOI3D_MAXNUMEDGE; ++i) {
-    destination->edgeindices[i] = source->edgeindices[i];
-  }
-
-  /* Copy neighbour information. Since neighbours are stored per edge, the total
-     number of neighbours in this list is larger than numngb and we copy
-     everything. */
-  for (int i = 0; i < VORONOI3D_MAXNUMEDGE; ++i) {
-    destination->ngbs[i] = source->ngbs[i];
-  }
-}
-
-#endif  // SWIFT_VORONOIXD_CELL_H
diff --git a/src/hydro_csds.h b/src/hydro_csds.h
index 96cb374423a438c5c42ea1f6a284c6d06f6815b6..a0593ca7b327fef1f98d8c3cf84590733fa67a40 100644
--- a/src/hydro_csds.h
+++ b/src/hydro_csds.h
@@ -47,6 +47,8 @@
 #error TODO
 #elif defined(PLANETARY_SPH)
 #error TODO
+#elif defined(REMIX_SPH)
+#error TODO
 #elif defined(SPHENIX_SPH)
 #include "./hydro/SPHENIX/hydro_csds.h"
 #elif defined(GASOLINE_SPH)
diff --git a/src/hydro_io.h b/src/hydro_io.h
index c0f82c2e7b4f52adc870b59f41d12c0e14e84a46..5a64a284cc4b7121bb50dec4f92df717cdef3ad5 100644
--- a/src/hydro_io.h
+++ b/src/hydro_io.h
@@ -39,10 +39,12 @@
 #include "./hydro/Phantom/hydro_io.h"
 #elif defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
 #include "./hydro/Gizmo/hydro_io.h"
-#elif defined(SHADOWFAX_SPH)
+#elif defined(SHADOWSWIFT)
 #include "./hydro/Shadowswift/hydro_io.h"
 #elif defined(PLANETARY_SPH)
 #include "./hydro/Planetary/hydro_io.h"
+#elif defined(REMIX_SPH)
+#include "./hydro/REMIX/hydro_io.h"
 #elif defined(SPHENIX_SPH)
 #include "./hydro/SPHENIX/hydro_io.h"
 #elif defined(GASOLINE_SPH)
diff --git a/src/hydro_parameters.h b/src/hydro_parameters.h
index 5d1f3909632ac055b00587c01c276242b7bf76f9..46d93ad43a212f3db84597d3a4cfccbaff834605 100644
--- a/src/hydro_parameters.h
+++ b/src/hydro_parameters.h
@@ -48,10 +48,12 @@
 #include "./hydro/Phantom/hydro_parameters.h"
 #elif defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
 #include "./hydro/Gizmo/hydro_parameters.h"
-#elif defined(SHADOWFAX_SPH)
+#elif defined(SHADOWSWIFT)
 #include "./hydro/Shadowswift/hydro_parameters.h"
 #elif defined(PLANETARY_SPH)
 #include "./hydro/Planetary/hydro_parameters.h"
+#elif defined(REMIX_SPH)
+#include "./hydro/REMIX/hydro_parameters.h"
 #elif defined(SPHENIX_SPH)
 #include "./hydro/SPHENIX/hydro_parameters.h"
 #elif defined(GASOLINE_SPH)
diff --git a/src/hydro_properties.c b/src/hydro_properties.c
index c160e6c9891f4729907465c5f54c6a9477a74d96..f8b547bc567a73be010365dcf46e11f299e5f02a 100644
--- a/src/hydro_properties.c
+++ b/src/hydro_properties.c
@@ -207,6 +207,9 @@ void hydro_props_init(struct hydro_props *p,
 
     p->generate_random_ids = parser_get_opt_param_int(
         params, "SPH:particle_splitting_generate_random_ids", 0);
+
+    p->log_extra_splits_in_file = parser_get_opt_param_int(
+        params, "SPH:particle_splitting_log_extra_splits", 0);
   }
 }
 
diff --git a/src/hydro_properties.h b/src/hydro_properties.h
index b86329737edaf7964367bf1260eefd0ea63a57b0..5815f58f3bf170da3d5a2655f2a9f5be975fcd8a 100644
--- a/src/hydro_properties.h
+++ b/src/hydro_properties.h
@@ -123,6 +123,10 @@ struct hydro_props {
   /*! Are we generating random IDs when splitting particles? */
   int generate_random_ids;
 
+  /*! Are we logging the particle splits beyond the limit in a file before
+   * reseting? */
+  int log_extra_splits_in_file;
+
   /* ------ Viscosity and diffusion ---------------- */
 
   /*! Artificial viscosity parameters */
diff --git a/src/io_compression.c b/src/io_compression.c
index 649674cd6cfb294825e9cf66ce745e6ae3af5d68..37826fb2a2e649eeab81f9b4b222c79fa4a516ae 100644
--- a/src/io_compression.c
+++ b/src/io_compression.c
@@ -35,10 +35,11 @@
  *        parameter file.
  **/
 const char* lossy_compression_schemes_names[compression_level_count] = {
-    "off",        "on",          "DScale1",   "DScale2",    "DScale3",
-    "DScale4",    "DScale5",     "DScale6",   "DMantissa9", "DMantissa13",
-    "FMantissa9", "FMantissa13", "HalfFloat", "BFloat16",   "Nbit32",
-    "Nbit36",     "Nbit40",      "Nbit44",    "Nbit48",     "Nbit56"};
+    "off",         "on",         "DScale1",     "DScale2",    "DScale3",
+    "DScale4",     "DScale5",    "DScale6",     "DMantissa9", "DMantissa13",
+    "DMantissa21", "FMantissa9", "FMantissa13", "HalfFloat",  "BFloat16",
+    "Nbit32",      "Nbit36",     "Nbit40",      "Nbit44",     "Nbit48",
+    "Nbit56"};
 
 /**
  * @brief Returns the lossy compression scheme given its name
@@ -149,7 +150,7 @@ void set_hdf5_lossy_compression(hid_t* h_prop, hid_t* h_type,
     /* Double numbers with 9-bits mantissa and 11-bits exponent
      *
      * This has a relative accuracy of log10(2^(9+1)) = 3.01 decimal digits
-     * and the same range as a regular float.
+     * and the same range as a regular double.
      *
      * This leads to a compression ratio of 3.05 */
 
@@ -205,7 +206,7 @@ void set_hdf5_lossy_compression(hid_t* h_prop, hid_t* h_type,
     /* Double numbers with 13-bits mantissa and 11-bits exponent
      *
      * This has a relative accuracy of log10(2^(13+1)) = 4.21 decimal digits
-     * and the same range as a regular float.
+     * and the same range as a regular double.
      *
      * This leads to a compression ratio of 2.56 */
 
@@ -256,6 +257,62 @@ void set_hdf5_lossy_compression(hid_t* h_prop, hid_t* h_type,
       error("Error while setting n-bit filter for field '%s'.", field_name);
   }
 
+  else if (comp == compression_write_d_mantissa_21) {
+
+    /* Double numbers with 21-bits mantissa and 11-bits exponent
+     *
+     * This has a relative accuracy of log10(2^(21+1)) = 6.62 decimal digits
+     * and the same range as a regular double.
+     *
+     * This leads to a compression ratio of 1.93 */
+
+    /* Note a regular IEEE-754 double has:
+     * - size = 8
+     * - m_size = 52
+     * - e_size = 11
+     * i.e. 52 + 11 + 1 (the sign bit) == 64 bits (== 8 bytes) */
+
+    const int size = 8;
+    const int m_size = 21;
+    const int e_size = 11;
+    const int offset = 0;
+    const int precision = m_size + e_size + 1;
+    const int e_pos = offset + m_size;
+    const int s_pos = e_pos + e_size;
+    const int m_pos = offset;
+    const int bias = (1 << (e_size - 1)) - 1;
+
+    H5Tclose(*h_type);
+    *h_type = H5Tcopy(H5T_NATIVE_DOUBLE);
+    hid_t h_err = H5Tset_fields(*h_type, s_pos, e_pos, e_size, m_pos, m_size);
+    if (h_err < 0)
+      error("Error while setting type properties for field '%s'.", field_name);
+
+    h_err = H5Tset_offset(*h_type, offset);
+    if (h_err < 0)
+      error("Error while setting type offset properties for field '%s'.",
+            field_name);
+
+    h_err = H5Tset_precision(*h_type, precision);
+    if (h_err < 0)
+      error("Error while setting type precision properties for field '%s'.",
+            field_name);
+
+    h_err = H5Tset_size(*h_type, size);
+    if (h_err < 0)
+      error("Error while setting type size properties for field '%s'.",
+            field_name);
+
+    h_err = H5Tset_ebias(*h_type, bias);
+    if (h_err < 0)
+      error("Error while setting type bias properties for field '%s'.",
+            field_name);
+
+    h_err = H5Pset_nbit(*h_prop);
+    if (h_err < 0)
+      error("Error while setting n-bit filter for field '%s'.", field_name);
+  }
+
   else if (comp == compression_write_f_mantissa_9) {
 
     /* Float numbers with 9-bits mantissa and 8-bits exponent
diff --git a/src/io_compression.h b/src/io_compression.h
index 5ebc68caea1c91489ea7ce3dea871c8938158acf..0b643116f61eb032e79984836c71d2c9f2760379 100644
--- a/src/io_compression.h
+++ b/src/io_compression.h
@@ -36,6 +36,7 @@ enum lossy_compression_schemes {
   compression_write_d_scale_6,     /*!< D-scale filter of magnitude 10^6 */
   compression_write_d_mantissa_9,  /*!< Conversion to 9-bits mantissa double */
   compression_write_d_mantissa_13, /*!< Conversion to 13-bits mantissa double */
+  compression_write_d_mantissa_21, /*!< Conversion to 21-bits mantissa double */
   compression_write_f_mantissa_9,  /*!< Conversion to 9-bits mantissa float */
   compression_write_f_mantissa_13, /*!< Conversion to 13-bits mantissa float */
   compression_write_half_float,    /*!< Conversion to IEEE754 half-float */
diff --git a/src/io_properties.h b/src/io_properties.h
index 5c69a6ad760e7eb45a1fd5f1eda25a9427f08a4b..e4a0689dfec54c7465ccad9c709d6f08a9be7f56 100644
--- a/src/io_properties.h
+++ b/src/io_properties.h
@@ -107,6 +107,16 @@ struct io_props {
   /* Dimension (1D, 3D, ...) */
   int dimension;
 
+  /* Has this entry been filled */
+  int is_used;
+
+  /* Is the entry actually written in the physical frame? */
+  int is_physical;
+
+  /* Is the entry not convertible to comoving (only meaningful if the entry is
+   * physical) */
+  int is_convertible_to_comoving;
+
   /* Is it compulsory ? (input only) */
   enum DATA_IMPORTANCE importance;
 
@@ -149,35 +159,40 @@ struct io_props {
   /* Are we converting? */
   int conversion;
 
-  /* Conversion function for part */
-  conversion_func_part_float convert_part_f;
-  conversion_func_part_int convert_part_i;
-  conversion_func_part_double convert_part_d;
-  conversion_func_part_long_long convert_part_l;
-
-  /* Conversion function for gpart */
-  conversion_func_gpart_float convert_gpart_f;
-  conversion_func_gpart_int convert_gpart_i;
-  conversion_func_gpart_double convert_gpart_d;
-  conversion_func_gpart_long_long convert_gpart_l;
-
-  /* Conversion function for spart */
-  conversion_func_spart_float convert_spart_f;
-  conversion_func_spart_int convert_spart_i;
-  conversion_func_spart_double convert_spart_d;
-  conversion_func_spart_long_long convert_spart_l;
-
-  /* Conversion function for bpart */
-  conversion_func_bpart_float convert_bpart_f;
-  conversion_func_bpart_int convert_bpart_i;
-  conversion_func_bpart_double convert_bpart_d;
-  conversion_func_bpart_long_long convert_bpart_l;
-
-  /* Conversion function for sink */
-  conversion_func_sink_float convert_sink_f;
-  conversion_func_sink_int convert_sink_i;
-  conversion_func_sink_double convert_sink_d;
-  conversion_func_sink_long_long convert_sink_l;
+  union {
+
+    void *ptr_func;
+
+    /* Conversion function for part */
+    conversion_func_part_float convert_part_f;
+    conversion_func_part_int convert_part_i;
+    conversion_func_part_double convert_part_d;
+    conversion_func_part_long_long convert_part_l;
+
+    /* Conversion function for gpart */
+    conversion_func_gpart_float convert_gpart_f;
+    conversion_func_gpart_int convert_gpart_i;
+    conversion_func_gpart_double convert_gpart_d;
+    conversion_func_gpart_long_long convert_gpart_l;
+
+    /* Conversion function for spart */
+    conversion_func_spart_float convert_spart_f;
+    conversion_func_spart_int convert_spart_i;
+    conversion_func_spart_double convert_spart_d;
+    conversion_func_spart_long_long convert_spart_l;
+
+    /* Conversion function for bpart */
+    conversion_func_bpart_float convert_bpart_f;
+    conversion_func_bpart_int convert_bpart_i;
+    conversion_func_bpart_double convert_bpart_d;
+    conversion_func_bpart_long_long convert_bpart_l;
+
+    /* Conversion function for sink */
+    conversion_func_sink_float convert_sink_f;
+    conversion_func_sink_int convert_sink_i;
+    conversion_func_sink_double convert_sink_d;
+    conversion_func_sink_long_long convert_sink_l;
+  };
 };
 
 /**
@@ -207,7 +222,7 @@ INLINE static void safe_strcpy(char *restrict dst, const char *restrict src,
  */
 #define io_make_input_field(name, type, dim, importance, units, part, field) \
   io_make_input_field_(name, type, dim, importance, units,                   \
-                       (char *)(&(part[0]).field), sizeof(part[0]), 0.)
+                       (char *)(&(part[0]).field), sizeof(part[0]), NULL)
 
 /**
  * @brief Constructs an #io_props from its parameters with a user-defined
@@ -226,7 +241,8 @@ INLINE static void safe_strcpy(char *restrict dst, const char *restrict src,
 #define io_make_input_field_default(name, type, dim, importance, units, part, \
                                     field, def)                               \
   io_make_input_field_(name, type, dim, importance, units,                    \
-                       (char *)(&(part[0]).field), sizeof(part[0]), def)
+                       (char *)(&(part[0]).field), sizeof(part[0]),           \
+                       (const void *)&(def))
 
 /**
  * @brief Construct an #io_props from its parameters
@@ -238,31 +254,74 @@ INLINE static void safe_strcpy(char *restrict dst, const char *restrict src,
  * @param units The units of the dataset
  * @param field Pointer to the field of the first particle
  * @param partSize The size in byte of the particle
+ * @param default_value The default value. It must be adressable.
  *
  * Do not call this function directly. Use the macro defined above.
  */
 INLINE static struct io_props io_make_input_field_(
     const char *name, enum IO_DATA_TYPE type, int dimension,
     enum DATA_IMPORTANCE importance, enum unit_conversion_factor units,
-    char *field, size_t partSize, const float default_value) {
+    char *field, size_t partSize, const void *default_value) {
   struct io_props r;
   bzero(&r, sizeof(struct io_props));
 
   safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
   r.type = type;
+  r.is_used = 1;
   r.dimension = dimension;
   r.importance = importance;
   r.units = units;
   r.field = field;
   r.partSize = partSize;
-  r.default_value = default_value;
 
-  if (default_value != 0.f && importance != OPTIONAL)
+  if (default_value != NULL && importance != OPTIONAL)
     error("Cannot set a non-zero default value for a compulsory field!");
-  if (default_value != 0.f && type != FLOAT)
-    error(
-        "Can only set non-zero default value for a field using a FLOAT type!");
 
+  if (default_value) {
+    switch (type) {
+      case INT:
+        r.default_value = *(int *)default_value;
+        break;
+      case LONG:
+        r.default_value = *(long *)default_value;
+        break;
+      case LONGLONG:
+        r.default_value = *(long long *)default_value;
+        break;
+      case UINT8:
+        r.default_value = *(uint8_t *)default_value;
+        break;
+      case UINT:
+        r.default_value = *(unsigned int *)default_value;
+        break;
+      case UINT64:
+        r.default_value = *(uint64_t *)default_value;
+        break;
+      case ULONG:
+        r.default_value = *(unsigned long *)default_value;
+        break;
+      case ULONGLONG:
+        r.default_value = *(unsigned long long *)default_value;
+        break;
+      case FLOAT:
+        r.default_value = *(float *)default_value;
+        break;
+      case DOUBLE:
+        r.default_value = *(double *)default_value;
+        break;
+      case CHAR:
+        r.default_value = *(char *)default_value;
+        break;
+      case BOOL:
+        r.default_value = *(bool *)default_value;
+        break;
+      case SIZE_T:
+        r.default_value = *(size_t *)default_value;
+        break;
+      default:
+        error("Unsupported type for default value!");
+    }
+  }
   return r;
 }
 
@@ -272,56 +331,27 @@ INLINE static struct io_props io_make_input_field_(
 #define io_make_output_field(name, type, dim, units, a_exponent, part, field, \
                              desc)                                            \
   io_make_output_field_(name, type, dim, units, a_exponent,                   \
-                        (char *)(&(part[0]).field), sizeof(part[0]), desc)
+                        (char *)(&(part[0]).field), sizeof(part[0]), desc,    \
+                        /*physical=*/0, /*convertible_to_physical=*/1);
 
 /**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param field Pointer to the field of the first particle
- * @param partSize The size in byte of the particle
- * @param description Description of the field added to the meta-data.
+ * @brief Constructs an #io_props from its parameters for a comoving quantity
  *
- * Do not call this function directly. Use the macro defined above.
+ * An alias of io_make_output_field().
  */
-INLINE static struct io_props io_make_output_field_(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, char *field,
-    size_t partSize, const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.field = field;
-  r.partSize = partSize;
-  r.conversion = 0;
-
-  return r;
-}
+#define io_make_comoving_output_field(name, type, dim, units, a_exponent, \
+                                      part, field, desc)                  \
+  io_make_output_field(name, type, dim, units, a_exponent, part, field, desc)
 
 /**
- * @brief Constructs an #io_props (with conversion) from its parameters
+ * @brief Constructs an #io_props from its parameters for a physical quantity
  */
-#define io_make_output_field_convert_part(name, type, dim, units, a_exponent,  \
-                                          part, xpart, convert, desc)          \
-  io_make_output_field_convert_part_##type(name, type, dim, units, a_exponent, \
-                                           sizeof(part[0]), part, xpart,       \
-                                           convert, desc)
+#define io_make_physical_output_field(name, type, dim, units, a_exponent,  \
+                                      part, field, convertible, desc)      \
+  io_make_output_field_(name, type, dim, units, a_exponent,                \
+                        (char *)(&(part[0]).field), sizeof(part[0]), desc, \
+                        /*physical=*/1,                                    \
+                        /*convertible_to_physical=*/convertible);
 
 /**
  * @brief Construct an #io_props from its parameters
@@ -331,64 +361,22 @@ INLINE static struct io_props io_make_output_field_(
  * @param dimension Dataset dimension (1D, 3D, ...)
  * @param units The units of the dataset
  * @param a_exponent Exponent of the scale-factor to convert to physical units.
+ * @param field Pointer to the field of the first particle
  * @param partSize The size in byte of the particle
- * @param parts The particle array
- * @param xparts The xparticle array
- * @param functionPtr The function used to convert a particle to an int
  * @param description Description of the field added to the meta-data.
+ * @param is_physical Is the quantity written in the physical frame?
+ * @param is_convertible_to_comoving Is the quantity convertible to comoving?
  *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_part_INT(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t partSize,
-    const struct part *parts, const struct xpart *xparts,
-    conversion_func_part_int functionPtr, const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = partSize;
-  r.parts = parts;
-  r.xparts = xparts;
-  r.conversion = 1;
-  r.convert_part_i = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param partSize The size in byte of the particle
- * @param parts The particle array
- * @param xparts The xparticle array
- * @param functionPtr The function used to convert a particle to a float
- * @param description Description of the field added to the meta-data.
+ * The last argument only makes sense if the quantity is written to
+ * in physical.
  *
  * Do not call this function directly. Use the macro defined above.
  */
-INLINE static struct io_props io_make_output_field_convert_part_FLOAT(
+INLINE static struct io_props io_make_output_field_(
     const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t partSize,
-    const struct part *parts, const struct xpart *xparts,
-    conversion_func_part_float functionPtr, const char *description) {
+    enum unit_conversion_factor units, float a_exponent, char *field,
+    size_t partSize, const char *description, const int is_physical,
+    const int is_convertible_to_comoving) {
 
   struct io_props r;
   bzero(&r, sizeof(struct io_props));
@@ -400,63 +388,40 @@ INLINE static struct io_props io_make_output_field_convert_part_FLOAT(
     safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
   }
   r.type = type;
+  r.is_used = 1;
+  r.is_physical = is_physical;
+  r.is_convertible_to_comoving = is_convertible_to_comoving;
   r.dimension = dimension;
   r.importance = UNUSED;
   r.units = units;
   r.scale_factor_exponent = a_exponent;
+  r.field = field;
   r.partSize = partSize;
-  r.parts = parts;
-  r.xparts = xparts;
-  r.conversion = 1;
-  r.convert_part_f = functionPtr;
+  r.conversion = 0;
 
   return r;
 }
 
 /**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param partSize The size in byte of the particle
- * @param parts The particle array
- * @param xparts The xparticle array
- * @param functionPtr The function used to convert a particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
+ * @brief Constructs an #io_props (with conversion) from its parameters
  */
-INLINE static struct io_props io_make_output_field_convert_part_DOUBLE(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t partSize,
-    const struct part *parts, const struct xpart *xparts,
-    conversion_func_part_double functionPtr, const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
+#define io_make_output_field_convert_part(name, type, dim, units, a_exponent, \
+                                          part, xpart, convert, desc)         \
+  io_make_output_field_convert_part_(                                         \
+      name, type, dim, units, a_exponent, sizeof(part[0]), part, xpart,       \
+      convert, desc, /*physical=*/0, /*convertible_to_physical=*/1);
 
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = partSize;
-  r.parts = parts;
-  r.xparts = xparts;
-  r.conversion = 1;
-  r.convert_part_d = functionPtr;
+#define io_make_comoving_output_field_convert_part(                           \
+    name, type, dim, units, a_exponent, part, xpart, convert, desc)           \
+  io_make_output_field_convert_part(name, type, dim, units, a_exponent, part, \
+                                    xpart, convert, desc)
 
-  return r;
-}
+#define io_make_physical_output_field_convert_part(name, type, dim, units,     \
+                                                   a_exponent, part, xpart,    \
+                                                   convertible, convert, desc) \
+  io_make_output_field_convert_part_(name, type, dim, units, a_exponent,       \
+                                     sizeof(part[0]), part, xpart, convert,    \
+                                     desc, /*physical=*/1, convertible);
 
 /**
  * @brief Construct an #io_props from its parameters
@@ -469,16 +434,17 @@ INLINE static struct io_props io_make_output_field_convert_part_DOUBLE(
  * @param partSize The size in byte of the particle
  * @param parts The particle array
  * @param xparts The xparticle array
- * @param functionPtr The function used to convert a particle to a double
+ * @param functionPtr The function used to convert a particle to an int
  * @param description Description of the field added to the meta-data.
  *
  * Do not call this function directly. Use the macro defined above.
  */
-INLINE static struct io_props io_make_output_field_convert_part_LONGLONG(
+INLINE static struct io_props io_make_output_field_convert_part_(
     const char *name, enum IO_DATA_TYPE type, int dimension,
     enum unit_conversion_factor units, float a_exponent, size_t partSize,
-    const struct part *parts, const struct xpart *xparts,
-    conversion_func_part_long_long functionPtr, const char *description) {
+    const struct part *parts, const struct xpart *xparts, void *functionPtr,
+    const char *description, const int is_physical,
+    const int is_convertible_to_comoving) {
 
   struct io_props r;
   bzero(&r, sizeof(struct io_props));
@@ -490,6 +456,9 @@ INLINE static struct io_props io_make_output_field_convert_part_LONGLONG(
     safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
   }
   r.type = type;
+  r.is_used = 1;
+  r.is_physical = is_physical;
+  r.is_convertible_to_comoving = is_convertible_to_comoving;
   r.dimension = dimension;
   r.importance = UNUSED;
   r.units = units;
@@ -498,7 +467,7 @@ INLINE static struct io_props io_make_output_field_convert_part_LONGLONG(
   r.parts = parts;
   r.xparts = xparts;
   r.conversion = 1;
-  r.convert_part_l = functionPtr;
+  r.ptr_func = functionPtr;
 
   return r;
 }
@@ -508,52 +477,20 @@ INLINE static struct io_props io_make_output_field_convert_part_LONGLONG(
  */
 #define io_make_output_field_convert_gpart(name, type, dim, units, a_exponent, \
                                            gpart, convert, desc)               \
-  io_make_output_field_convert_gpart_##type(name, type, dim, units,            \
-                                            a_exponent, sizeof(gpart[0]),      \
-                                            gpart, convert, desc)
+  io_make_output_field_convert_gpart_(                                         \
+      name, type, dim, units, a_exponent, sizeof(gpart[0]), gpart, convert,    \
+      desc, /*physical=*/0, /*convertible_to_physical=*/1)
 
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param gpartSize The size in byte of the particle
- * @param gparts The particle array
- * @param functionPtr The function used to convert a g-particle to a float
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_gpart_INT(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t gpartSize,
-    const struct gpart *gparts, conversion_func_gpart_int functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = gpartSize;
-  r.gparts = gparts;
-  r.conversion = 1;
-  r.convert_gpart_i = functionPtr;
+#define io_make_comoving_output_field_convert_gpart(                     \
+    name, type, dim, units, a_exponent, gpart, convert, desc)            \
+  io_make_output_field_convert_gpart(name, type, dim, units, a_exponent, \
+                                     gpart, convert, desc)
 
-  return r;
-}
+#define io_make_physical_output_field_convert_gpart(                          \
+    name, type, dim, units, a_exponent, gpart, convertible, convert, desc)    \
+  io_make_output_field_convert_gpart_(name, type, dim, units, a_exponent,     \
+                                      sizeof(gpart[0]), gpart, convert, desc, \
+                                      /*physical=*/1, convertible);
 
 /**
  * @brief Construct an #io_props from its parameters
@@ -570,11 +507,11 @@ INLINE static struct io_props io_make_output_field_convert_gpart_INT(
  *
  * Do not call this function directly. Use the macro defined above.
  */
-INLINE static struct io_props io_make_output_field_convert_gpart_FLOAT(
+INLINE static struct io_props io_make_output_field_convert_gpart_(
     const char *name, enum IO_DATA_TYPE type, int dimension,
     enum unit_conversion_factor units, float a_exponent, size_t gpartSize,
-    const struct gpart *gparts, conversion_func_gpart_float functionPtr,
-    const char *description) {
+    const struct gpart *gparts, void *functionPtr, const char *description,
+    const int is_physical, const int is_convertible_to_comoving) {
 
   struct io_props r;
   bzero(&r, sizeof(struct io_props));
@@ -586,6 +523,9 @@ INLINE static struct io_props io_make_output_field_convert_gpart_FLOAT(
     safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
   }
   r.type = type;
+  r.is_used = 1;
+  r.is_physical = is_physical;
+  r.is_convertible_to_comoving = is_convertible_to_comoving;
   r.dimension = dimension;
   r.importance = UNUSED;
   r.units = units;
@@ -593,53 +533,30 @@ INLINE static struct io_props io_make_output_field_convert_gpart_FLOAT(
   r.partSize = gpartSize;
   r.gparts = gparts;
   r.conversion = 1;
-  r.convert_gpart_f = functionPtr;
+  r.ptr_func = functionPtr;
 
   return r;
 }
 
 /**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param gpartSize The size in byte of the particle
- * @param gparts The particle array
- * @param functionPtr The function used to convert a g-particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
+ * @brief Constructs an #io_props (with conversion) from its parameters
  */
-INLINE static struct io_props io_make_output_field_convert_gpart_DOUBLE(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t gpartSize,
-    const struct gpart *gparts, conversion_func_gpart_double functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
+#define io_make_output_field_convert_spart(name, type, dim, units, a_exponent, \
+                                           spart, convert, desc)               \
+  io_make_output_field_convert_spart_(                                         \
+      name, type, dim, units, a_exponent, sizeof(spart[0]), spart, convert,    \
+      desc, /*physical=*/0, /*convertible_to_physical=*/1)
 
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = gpartSize;
-  r.gparts = gparts;
-  r.conversion = 1;
-  r.convert_gpart_d = functionPtr;
+#define io_make_comoving_output_field_convert_spart(                     \
+    name, type, dim, units, a_exponent, spart, convert, desc)            \
+  io_make_output_field_convert_spart(name, type, dim, units, a_exponent, \
+                                     spart, convert, desc)
 
-  return r;
-}
+#define io_make_physical_output_field_convert_spart(                          \
+    name, type, dim, units, a_exponent, spart, convertible, convert, desc)    \
+  io_make_output_field_convert_spart_(name, type, dim, units, a_exponent,     \
+                                      sizeof(spart[0]), spart, convert, desc, \
+                                      /*physical=*/1, convertible);
 
 /**
  * @brief Construct an #io_props from its parameters
@@ -649,18 +566,18 @@ INLINE static struct io_props io_make_output_field_convert_gpart_DOUBLE(
  * @param dimension Dataset dimension (1D, 3D, ...)
  * @param units The units of the dataset
  * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param gpartSize The size in byte of the particle
- * @param gparts The particle array
- * @param functionPtr The function used to convert a g-particle to a double
+ * @param spartSize The size in byte of the particle
+ * @param sparts The particle array
+ * @param functionPtr The function used to convert a s-particle to a float
  * @param description Description of the field added to the meta-data.
  *
  * Do not call this function directly. Use the macro defined above.
  */
-INLINE static struct io_props io_make_output_field_convert_gpart_LONGLONG(
+INLINE static struct io_props io_make_output_field_convert_spart_(
     const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t gpartSize,
-    const struct gpart *gparts, conversion_func_gpart_long_long functionPtr,
-    const char *description) {
+    enum unit_conversion_factor units, float a_exponent, size_t spartSize,
+    const struct spart *sparts, void *functionPtr, const char *description,
+    const int is_physical, const int is_convertible_to_comoving) {
 
   struct io_props r;
   bzero(&r, sizeof(struct io_props));
@@ -672,14 +589,17 @@ INLINE static struct io_props io_make_output_field_convert_gpart_LONGLONG(
     safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
   }
   r.type = type;
+  r.is_used = 1;
+  r.is_physical = is_physical;
+  r.is_convertible_to_comoving = is_convertible_to_comoving;
   r.dimension = dimension;
   r.importance = UNUSED;
   r.units = units;
   r.scale_factor_exponent = a_exponent;
-  r.partSize = gpartSize;
-  r.gparts = gparts;
+  r.partSize = spartSize;
+  r.sparts = sparts;
   r.conversion = 1;
-  r.convert_gpart_l = functionPtr;
+  r.ptr_func = functionPtr;
 
   return r;
 }
@@ -687,11 +607,22 @@ INLINE static struct io_props io_make_output_field_convert_gpart_LONGLONG(
 /**
  * @brief Constructs an #io_props (with conversion) from its parameters
  */
-#define io_make_output_field_convert_spart(name, type, dim, units, a_exponent, \
-                                           spart, convert, desc)               \
-  io_make_output_field_convert_spart_##type(name, type, dim, units,            \
-                                            a_exponent, sizeof(spart[0]),      \
-                                            spart, convert, desc)
+#define io_make_output_field_convert_bpart(name, type, dim, units, a_exponent, \
+                                           bpart, convert, desc)               \
+  io_make_output_field_convert_bpart_(                                         \
+      name, type, dim, units, a_exponent, sizeof(bpart[0]), bpart, convert,    \
+      desc, /*physical=*/0, /*convertible_to_physical=*/1)
+
+#define io_make_comoving_output_field_convert_bpart(                     \
+    name, type, dim, units, a_exponent, bpart, convert, desc)            \
+  io_make_output_field_convert_bpart(name, type, dim, units, a_exponent, \
+                                     bpart, convert, desc)
+
+#define io_make_physical_output_field_convert_bpart(                          \
+    name, type, dim, units, a_exponent, bpart, convertible, convert, desc)    \
+  io_make_output_field_convert_bpart_(name, type, dim, units, a_exponent,     \
+                                      sizeof(bpart[0]), bpart, convert, desc, \
+                                      /*physical=*/1, convertible);
 
 /**
  * @brief Construct an #io_props from its parameters
@@ -701,18 +632,18 @@ INLINE static struct io_props io_make_output_field_convert_gpart_LONGLONG(
  * @param dimension Dataset dimension (1D, 3D, ...)
  * @param units The units of the dataset
  * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param spartSize The size in byte of the particle
- * @param sparts The particle array
- * @param functionPtr The function used to convert a s-particle to a float
+ * @param bpartSize The size in byte of the particle
+ * @param bparts The particle array
+ * @param functionPtr The function used to convert a b-particle to a float
  * @param description Description of the field added to the meta-data.
  *
  * Do not call this function directly. Use the macro defined above.
  */
-INLINE static struct io_props io_make_output_field_convert_spart_INT(
+INLINE static struct io_props io_make_output_field_convert_bpart_(
     const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t spartSize,
-    const struct spart *sparts, conversion_func_spart_int functionPtr,
-    const char *description) {
+    enum unit_conversion_factor units, float a_exponent, size_t bpartSize,
+    const struct bpart *bparts, void *functionPtr, const char *description,
+    const int is_physical, const int is_convertible_to_comoving) {
 
   struct io_props r;
   bzero(&r, sizeof(struct io_props));
@@ -724,336 +655,40 @@ INLINE static struct io_props io_make_output_field_convert_spart_INT(
     safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
   }
   r.type = type;
+  r.is_used = 1;
+  r.is_physical = is_physical;
+  r.is_convertible_to_comoving = is_convertible_to_comoving;
   r.dimension = dimension;
   r.importance = UNUSED;
   r.units = units;
   r.scale_factor_exponent = a_exponent;
-  r.partSize = spartSize;
-  r.sparts = sparts;
+  r.partSize = bpartSize;
+  r.bparts = bparts;
   r.conversion = 1;
-  r.convert_spart_i = functionPtr;
+  r.ptr_func = functionPtr;
 
   return r;
 }
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param spartSize The size in byte of the particle
- * @param sparts The particle array
- * @param functionPtr The function used to convert a g-particle to a float
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_spart_FLOAT(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t spartSize,
-    const struct spart *sparts, conversion_func_spart_float functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = spartSize;
-  r.sparts = sparts;
-  r.conversion = 1;
-  r.convert_spart_f = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param spartSize The size in byte of the particle
- * @param sparts The particle array
- * @param functionPtr The function used to convert a s-particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_spart_DOUBLE(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t spartSize,
-    const struct spart *sparts, conversion_func_spart_double functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = spartSize;
-  r.sparts = sparts;
-  r.conversion = 1;
-  r.convert_spart_d = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param spartSize The size in byte of the particle
- * @param sparts The particle array
- * @param functionPtr The function used to convert a s-particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_spart_LONGLONG(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t spartSize,
-    const struct spart *sparts, conversion_func_spart_long_long functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = spartSize;
-  r.sparts = sparts;
-  r.conversion = 1;
-  r.convert_spart_l = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Constructs an #io_props (with conversion) from its parameters
- */
-#define io_make_output_field_convert_bpart(name, type, dim, units, a_exponent, \
-                                           bpart, convert, desc)               \
-  io_make_output_field_convert_bpart_##type(name, type, dim, units,            \
-                                            a_exponent, sizeof(bpart[0]),      \
-                                            bpart, convert, desc)
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param bpartSize The size in byte of the particle
- * @param bparts The particle array
- * @param functionPtr The function used to convert a b-particle to a float
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_bpart_INT(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t bpartSize,
-    const struct bpart *bparts, conversion_func_bpart_int functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = bpartSize;
-  r.bparts = bparts;
-  r.conversion = 1;
-  r.convert_bpart_i = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param bpartSize The size in byte of the particle
- * @param bparts The particle array
- * @param functionPtr The function used to convert a g-particle to a float
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_bpart_FLOAT(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t bpartSize,
-    const struct bpart *bparts, conversion_func_bpart_float functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = bpartSize;
-  r.bparts = bparts;
-  r.conversion = 1;
-  r.convert_bpart_f = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param bpartSize The size in byte of the particle
- * @param bparts The particle array
- * @param functionPtr The function used to convert a s-particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_bpart_DOUBLE(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t bpartSize,
-    const struct bpart *bparts, conversion_func_bpart_double functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = bpartSize;
-  r.bparts = bparts;
-  r.conversion = 1;
-  r.convert_bpart_d = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param bpartSize The size in byte of the particle
- * @param bparts The particle array
- * @param functionPtr The function used to convert a s-particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_bpart_LONGLONG(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t bpartSize,
-    const struct bpart *bparts, conversion_func_bpart_long_long functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = bpartSize;
-  r.bparts = bparts;
-  r.conversion = 1;
-  r.convert_bpart_l = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Constructs an #io_props (with conversion) from its parameters
- */
-#define io_make_output_field_convert_sink(name, type, dim, units, a_exponent,  \
-                                          sink, convert, desc)                 \
-  io_make_output_field_convert_sink_##type(name, type, dim, units, a_exponent, \
-                                           sizeof(sink[0]), sink, convert,     \
-                                           desc)
+
+/**
+ * @brief Constructs an #io_props (with conversion) from its parameters
+ */
+#define io_make_output_field_convert_sink(name, type, dim, units, a_exponent, \
+                                          sink, convert, desc)                \
+  io_make_output_field_convert_sink_(                                         \
+      name, type, dim, units, a_exponent, sizeof(sink[0]), sink, convert,     \
+      desc, /*physical=*/0, /*convertible_to_physical=*/1)
+
+#define io_make_comoving_output_field_convert_sink(                           \
+    name, type, dim, units, a_exponent, ink, convert, desc)                   \
+  io_make_output_field_convert_sink(name, type, dim, units, a_exponent, sink, \
+                                    convert, desc)
+
+#define io_make_physical_output_field_convert_sink(                        \
+    name, type, dim, units, a_exponent, sink, convertible, convert, desc)  \
+  io_make_output_field_convert_sink_(name, type, dim, units, a_exponent,   \
+                                     sizeof(sink[0]), sink, convert, desc, \
+                                     /*physical=*/1, convertible);
 
 /**
  * @brief Construct an #io_props from its parameters
@@ -1070,140 +705,11 @@ INLINE static struct io_props io_make_output_field_convert_bpart_LONGLONG(
  *
  * Do not call this function directly. Use the macro defined above.
  */
-INLINE static struct io_props io_make_output_field_convert_sink_INT(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t sinkSize,
-    const struct sink *sinks, conversion_func_sink_int functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = sinkSize;
-  r.sinks = sinks;
-  r.conversion = 1;
-  r.convert_sink_i = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param sinkSize The size in byte of the particle
- * @param sinks The particle array
- * @param functionPtr The function used to convert a sink-particle to a float
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_sink_FLOAT(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t sinkSize,
-    const struct sink *sinks, conversion_func_sink_float functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = sinkSize;
-  r.sinks = sinks;
-  r.conversion = 1;
-  r.convert_sink_f = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param sinkSize The size in byte of the particle
- * @param sinks The particle array
- * @param functionPtr The function used to convert a sink-particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_sink_DOUBLE(
-    const char *name, enum IO_DATA_TYPE type, int dimension,
-    enum unit_conversion_factor units, float a_exponent, size_t sinkSize,
-    const struct sink *sinks, conversion_func_sink_double functionPtr,
-    const char *description) {
-
-  struct io_props r;
-  bzero(&r, sizeof(struct io_props));
-
-  safe_strcpy(r.name, name, FIELD_BUFFER_SIZE);
-  if (strlen(description) == 0) {
-    sprintf(r.description, "No description given");
-  } else {
-    safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
-  }
-  r.type = type;
-  r.dimension = dimension;
-  r.importance = UNUSED;
-  r.units = units;
-  r.scale_factor_exponent = a_exponent;
-  r.partSize = sinkSize;
-  r.sinks = sinks;
-  r.conversion = 1;
-  r.convert_sink_d = functionPtr;
-
-  return r;
-}
-
-/**
- * @brief Construct an #io_props from its parameters
- *
- * @param name Name of the field to read
- * @param type The type of the data
- * @param dimension Dataset dimension (1D, 3D, ...)
- * @param units The units of the dataset
- * @param a_exponent Exponent of the scale-factor to convert to physical units.
- * @param sinkSize The size in byte of the particle
- * @param sinks The particle array
- * @param functionPtr The function used to convert a sink-particle to a double
- * @param description Description of the field added to the meta-data.
- *
- * Do not call this function directly. Use the macro defined above.
- */
-INLINE static struct io_props io_make_output_field_convert_sink_LONGLONG(
+INLINE static struct io_props io_make_output_field_convert_sink_(
     const char *name, enum IO_DATA_TYPE type, int dimension,
     enum unit_conversion_factor units, float a_exponent, size_t sinkSize,
-    const struct sink *sinks, conversion_func_sink_long_long functionPtr,
-    const char *description) {
+    const struct sink *sinks, void *functionPtr, const char *description,
+    const int is_physical, const int is_convertible_to_comoving) {
 
   struct io_props r;
   bzero(&r, sizeof(struct io_props));
@@ -1215,6 +721,9 @@ INLINE static struct io_props io_make_output_field_convert_sink_LONGLONG(
     safe_strcpy(r.description, description, DESCRIPTION_BUFFER_SIZE);
   }
   r.type = type;
+  r.is_used = 1;
+  r.is_physical = is_physical;
+  r.is_convertible_to_comoving = is_convertible_to_comoving;
   r.dimension = dimension;
   r.importance = UNUSED;
   r.units = units;
@@ -1222,7 +731,7 @@ INLINE static struct io_props io_make_output_field_convert_sink_LONGLONG(
   r.partSize = sinkSize;
   r.sinks = sinks;
   r.conversion = 1;
-  r.convert_sink_l = functionPtr;
+  r.ptr_func = functionPtr;
 
   return r;
 }
diff --git a/src/kernel_hydro.h b/src/kernel_hydro.h
index dc0c721e5b17528ad28ebd12a05fa8d8a004420d..19e4c01fa47c6331986f5c75ad90ed3be5019f9b 100644
--- a/src/kernel_hydro.h
+++ b/src/kernel_hydro.h
@@ -391,6 +391,40 @@ __attribute__((always_inline)) INLINE static void kernel_eval_dWdx(
   *dW_dx = dw_dx * kernel_constant * kernel_gamma_inv_dim_plus_one;
 }
 
+#ifdef WENDLAND_C2_KERNEL
+
+/**
+ * Computes dphi/dh for the chosen kernel.
+ *
+ * This corresponds to Appendix A of Price & Monaghan 2007, MNRAS, 374, 4
+ * but for a Wendland-C2 kernel.
+ *
+ * Assumes r < H (i.e. u < kernel_gamma)
+ *
+ * @param u The ratio r / h.
+ * @param h_inv 1 / h.
+ */
+__attribute__((always_inline, const)) INLINE static float potential_dh(
+    const float u, const float h_inv) {
+
+  /* Ratio of r to kernel support
+   * Recall that in our definition the kernel edge is H = kernel_gamma * h. */
+  const float q = fminf(u * kernel_gamma_inv, 1.f);
+
+  /* -24 q^7 + 105 q^6 -168 q^5 + 105 q^4 - 21 q^2 */
+  float dphi_dh = -24.f * q + 105.f;
+  dphi_dh = dphi_dh * q - 168.f;
+  dphi_dh = dphi_dh * q + 105.f;
+  dphi_dh = dphi_dh * q;
+  dphi_dh = dphi_dh * q - 21.f;
+  dphi_dh = dphi_dh * q;
+  dphi_dh = dphi_dh * q;
+
+  return dphi_dh * h_inv * h_inv * kernel_gamma_inv * 0.25f;
+}
+
+#endif
+
 /* -------------------------------------------------------------------------
  */
 
diff --git a/src/kick.h b/src/kick.h
index d81f8fb5e622d8ffc9a5db36b705c5ca43bb2dd7..20fca47873d12aeb7c6d3c45f93a568ed2df2fd6 100644
--- a/src/kick.h
+++ b/src/kick.h
@@ -24,6 +24,7 @@
 
 /* Local headers. */
 #include "black_holes.h"
+#include "chemistry_additions.h"
 #include "const.h"
 #include "debug.h"
 #include "mhd.h"
@@ -271,6 +272,10 @@ __attribute__((always_inline)) INLINE static void kick_part(
    * the particle masses in hydro_kick_extra */
   rt_kick_extra(p, dt_kick_therm, dt_kick_grav, dt_kick_hydro, dt_kick_corr,
                 cosmo, hydro_props);
+  /* Similarly, we must apply the chemistry metal fluxes before updating the
+   * particle masses */
+  chemistry_kick_extra(p, dt_kick_therm, dt_kick_grav, dt_kick_hydro,
+                       dt_kick_corr, cosmo, hydro_props);
   hydro_kick_extra(p, xp, dt_kick_therm, dt_kick_grav, dt_kick_mesh_grav,
                    dt_kick_hydro, dt_kick_corr, cosmo, hydro_props,
                    floor_props);
diff --git a/src/lightcone/lightcone.c b/src/lightcone/lightcone.c
index 8c244b0c71a3d3e00bdcf1a8baab16887c1af9ea..7a431ed83791dcac27079e66a2fb079be9141f78 100644
--- a/src/lightcone/lightcone.c
+++ b/src/lightcone/lightcone.c
@@ -59,7 +59,7 @@
 #include "units.h"
 
 /* Whether to dump the replication list */
-//#define DUMP_REPLICATIONS
+// #define DUMP_REPLICATIONS
 #ifdef DUMP_REPLICATIONS
 static int output_nr = 0;
 #endif
@@ -864,7 +864,7 @@ void lightcone_flush_particle_buffers(struct lightcone_props *props, double a,
   if ((types_to_flush > 0) || (end_file && props->file_needs_finalizing)) {
 
     /* We have data to flush, so open or create the output file */
-    hid_t file_id;
+    hid_t file_id, h_props;
     char fname[FILENAME_BUFFER_SIZE];
     if (props->start_new_file) {
 
@@ -873,8 +873,14 @@ void lightcone_flush_particle_buffers(struct lightcone_props *props, double a,
       particle_file_name(fname, FILENAME_BUFFER_SIZE, props->subdir,
                          props->basename, props->current_file, engine_rank);
 
+      h_props = H5Pcreate(H5P_FILE_ACCESS);
+      herr_t err =
+          H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                               HDF5_HIGHEST_FILE_FORMAT_VERSION);
+      if (err < 0) error("Error setting the hdf5 API version");
+
       /* Create the file */
-      file_id = H5Fcreate(fname, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+      file_id = H5Fcreate(fname, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
       if (file_id < 0) error("Unable to create new lightcone file: %s", fname);
 
       /* This new file has not been finalized yet */
@@ -924,10 +930,16 @@ void lightcone_flush_particle_buffers(struct lightcone_props *props, double a,
 
     } else {
 
+      h_props = H5Pcreate(H5P_FILE_ACCESS);
+      herr_t err =
+          H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                               HDF5_HIGHEST_FILE_FORMAT_VERSION);
+      if (err < 0) error("Error setting the hdf5 API version");
+
       /* Re-open an existing file */
       particle_file_name(fname, FILENAME_BUFFER_SIZE, props->subdir,
                          props->basename, props->current_file, engine_rank);
-      file_id = H5Fopen(fname, H5F_ACC_RDWR, H5P_DEFAULT);
+      file_id = H5Fopen(fname, H5F_ACC_RDWR, h_props);
       if (file_id < 0)
         error("Unable to open current lightcone file: %s", fname);
     }
@@ -969,6 +981,7 @@ void lightcone_flush_particle_buffers(struct lightcone_props *props, double a,
 
     /* We're done updating the output file */
     H5Fclose(file_id);
+    H5Pclose(h_props);
   }
 
   /* If we need to start a new file next time, record this */
@@ -1101,6 +1114,12 @@ void lightcone_dump_completed_shells(struct lightcone_props *props,
         /* Create the output file for this shell */
         hid_t fapl_id = H5Pcreate(H5P_FILE_ACCESS);
 
+        /* Set the minimal API version to avoid issues with advanced features */
+        herr_t err =
+            H5Pset_libver_bounds(fapl_id, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                 HDF5_HIGHEST_FILE_FORMAT_VERSION);
+        if (err < 0) error("Error setting the hdf5 API version");
+
         /* Set MPI collective mode, if necessary */
         int collective = 0;
 #ifdef WITH_MPI
@@ -1710,8 +1729,13 @@ void lightcone_write_index(struct lightcone_props *props,
     check_snprintf(fname, FILENAME_BUFFER_SIZE, "%s/%s_index.hdf5",
                    props->subdir, props->basename);
 
+    hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+    herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                      HDF5_HIGHEST_FILE_FORMAT_VERSION);
+    if (err < 0) error("Error setting the hdf5 API version");
+
     /* Create the file */
-    hid_t file_id = H5Fcreate(fname, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+    hid_t file_id = H5Fcreate(fname, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
 
     /* Write number of MPI ranks and number of files */
     hid_t group_id =
@@ -1755,6 +1779,7 @@ void lightcone_write_index(struct lightcone_props *props,
 
     H5Gclose(group_id);
     H5Fclose(file_id);
+    H5Pclose(h_props);
   }
 
   free(current_file_on_rank);
diff --git a/src/lightcone/lightcone_crossing.h b/src/lightcone/lightcone_crossing.h
index 78777f1ff09b805074110601701c4c85aa6ffc2a..226fbfa3b840678bd2cafa2e0173eb25d142a808 100644
--- a/src/lightcone/lightcone_crossing.h
+++ b/src/lightcone/lightcone_crossing.h
@@ -246,7 +246,7 @@ lightcone_check_particle_crosses(
         lightcone_buffer_map_update(props, e, gp, a_cross, x_cross);
 
     } /* Next periodic replication*/
-  }   /* Next lightcone */
+  } /* Next lightcone */
 }
 
 #endif /* SWIFT_LIGHTCONE_CROSSING_H */
diff --git a/src/lightcone/lightcone_map.c b/src/lightcone/lightcone_map.c
index 0f4ae43b922d8ff146e2bf21bbc07b10190ef98a..9bb030e24b3d11700aa2e01c105a2fed84485249 100644
--- a/src/lightcone/lightcone_map.c
+++ b/src/lightcone/lightcone_map.c
@@ -223,14 +223,11 @@ void lightcone_map_write(struct lightcone_map *map, const hid_t loc_id,
   /* Data type to write in the file */
   hid_t dtype_id = H5Tcopy(H5T_NATIVE_DOUBLE);
 
-  /* Use chunked writes and possibly filters in non-collective mode */
+  /* In non-collective mode we may want to enable compression, which requires
+     a chunked dataset layout. If no compression filters are in use we use
+     a contiguous layout. */
   if (!collective) {
 
-    /* Set the chunk size */
-    const hsize_t dim[1] = {(hsize_t)chunk_size};
-    if (H5Pset_chunk(prop_id, 1, dim) < 0)
-      error("Unable to set HDF5 chunk size for healpix map");
-
     /* Set lossy compression, if requested. This might change the
        output data type and add to the property list. */
     char filter_name[32];
@@ -243,6 +240,18 @@ void lightcone_map_write(struct lightcone_map *map, const hid_t loc_id,
       if (H5Pset_deflate(prop_id, gzip_level) < 0)
         error("Unable to set HDF5 deflate filter for healpix map");
     }
+
+    /* Set the chunk size, but only if we're using filters */
+    if (H5Pget_nfilters(prop_id) > 0) {
+      hsize_t dim[1];
+      if (map->local_nr_pix > chunk_size) {
+        dim[0] = (hsize_t)chunk_size;
+      } else {
+        dim[0] = (hsize_t)map->local_nr_pix;
+      }
+      if (H5Pset_chunk(prop_id, 1, dim) < 0)
+        error("Unable to set HDF5 chunk size for healpix map");
+    }
   }
 
   /* Create the dataset */
@@ -275,6 +284,8 @@ void lightcone_map_write(struct lightcone_map *map, const hid_t loc_id,
   io_write_attribute_f(dset_id, "h-scale exponent", 0.f);
   io_write_attribute_f(dset_id, "a-scale exponent", 0.f);
   io_write_attribute_s(dset_id, "Expression for physical CGS units", buffer);
+  io_write_attribute_b(dset_id, "Value stored as physical", 0);
+  io_write_attribute_b(dset_id, "Property can be converted to comoving", 1);
 
   /* Write the actual number this conversion factor corresponds to */
   const double cgs_factor =
diff --git a/src/lightcone/lightcone_particle_io.c b/src/lightcone/lightcone_particle_io.c
index 9d3584412a56ca60e077fb9f3d8d59e24f7276a7..b568b86c26fe28fda5c7aaccfaffc81fa5907d0b 100644
--- a/src/lightcone/lightcone_particle_io.c
+++ b/src/lightcone/lightcone_particle_io.c
@@ -145,7 +145,7 @@ void lightcone_io_append_gas_output_fields(
 #if defined(TRACERS_EAGLE) || defined(TRACERS_FLAMINGO)
   lightcone_io_field_list_append(list, "LastAGNFeedbackScaleFactors", FLOAT, 1,
                                  OFFSET(last_AGN_injection_scale_factor),
-                                 UNIT_CONV_NO_UNITS, 0.0, "BFloat16");
+                                 UNIT_CONV_NO_UNITS, 0.0, "FMantissa13");
 #endif
 #ifdef STAR_FORMATION_EAGLE
   lightcone_io_field_list_append(list, "StarFormationRates", FLOAT, 1,
@@ -642,34 +642,32 @@ void append_dataset(const struct unit_system *snapshot_units,
                     hid_t mem_type_id, hsize_t chunk_size,
                     int lossy_compression,
                     enum lossy_compression_schemes compression_scheme,
-                    int gzip_level, const int rank, const hsize_t *dims,
+                    int gzip_level, const int rank, const hsize_t dims[2],
                     const hsize_t num_written, const void *data) {
 
-  const int max_rank = 2;
-  if (rank > max_rank)
-    error("HDF5 dataset has too may dimensions. Increase max_rank.");
+  if (rank > 2) error("HDF5 dataset has too may dimensions.");
   if (rank < 1) error("HDF5 dataset must be at least one dimensional");
 
   /* If we have zero elements to append, there's nothing to do */
   if (dims[0] == 0) return;
 
   /* Determine size of the dataset after we append our data */
-  hsize_t full_dims[max_rank];
+  hsize_t full_dims[2];
   for (int i = 0; i < rank; i += 1) full_dims[i] = dims[i];
   full_dims[0] += num_written;
 
   /* Determine maximum size in each dimension */
-  hsize_t max_dims[max_rank];
+  hsize_t max_dims[2];
   for (int i = 1; i < rank; i += 1) max_dims[i] = full_dims[i];
   max_dims[0] = H5S_UNLIMITED;
 
   /* Determine chunk size in each dimension */
-  hsize_t chunk_dims[max_rank];
+  hsize_t chunk_dims[2];
   for (int i = 1; i < rank; i += 1) chunk_dims[i] = full_dims[i];
   chunk_dims[0] = (hsize_t)chunk_size;
 
   /* Find offset to region to write in each dimension */
-  hsize_t offset[max_rank];
+  hsize_t offset[2];
   for (int i = 1; i < rank; i += 1) offset[i] = 0;
   offset[0] = num_written;
 
diff --git a/src/lightcone/lightcone_replications.c b/src/lightcone/lightcone_replications.c
index f65044814c71fb72298bf3da83404f8ac9c8ef09..22ec701d92365313a1c4dbe8e043616ab6607044 100644
--- a/src/lightcone/lightcone_replications.c
+++ b/src/lightcone/lightcone_replications.c
@@ -128,8 +128,8 @@ void replication_list_init(struct replication_list *replication_list,
             replication_list->nrep += 1;
           }
         } /* Next replication in z */
-      }   /* Next replication in y */
-    }     /* Next replication in x */
+      } /* Next replication in y */
+    } /* Next replication in x */
 
     /* Allocate storage after first pass */
     if (ipass == 0) {
diff --git a/src/lightcone/lightcone_shell.c b/src/lightcone/lightcone_shell.c
index e3ac45885fd57efa2b14a67b1d376ee44f0d1568..51d07bd8b7cc94d4d9fad8e02b94a1f375bdfb52 100644
--- a/src/lightcone/lightcone_shell.c
+++ b/src/lightcone/lightcone_shell.c
@@ -762,7 +762,7 @@ void healpix_smoothing_mapper(void *map_data, int num_elements,
               } /* Next smoothed map */
             }
           } /* Next pixel in this range */
-        }   /* Next range of pixels */
+        } /* Next range of pixels */
 
         /* Free array of pixel ranges */
         free(range);
diff --git a/src/line_of_sight.c b/src/line_of_sight.c
index 01680ab80721e183b33c9fad17ec1968fe494eec..1a63a17954d67da46594e9563ebefe29a9f0e711 100644
--- a/src/line_of_sight.c
+++ b/src/line_of_sight.c
@@ -152,6 +152,40 @@ void los_init(const double dim[3], struct los_props *los_params,
       los_params->range_when_shooting_down_axis[2]);
 }
 
+void los_io_output_check(const struct engine *e) {
+
+  /* What kind of run are we working with? */
+  struct swift_params *params = e->parameter_file;
+  const int with_cosmology = e->policy & engine_policy_cosmology;
+  const int with_cooling = e->policy & engine_policy_cooling;
+  const int with_temperature = e->policy & engine_policy_temperature;
+  const int with_fof = e->policy & engine_policy_fof;
+#ifdef HAVE_VELOCIRAPTOR
+  const int with_stf = (e->policy & engine_policy_structure_finding) &&
+                       (e->s->gpart_group_data != NULL);
+#else
+  const int with_stf = 0;
+#endif
+  const int with_rt = e->policy & engine_policy_rt;
+
+  int num_fields = 0;
+  struct io_props list[100];
+
+  /* Find all the gas output fields */
+  io_select_hydro_fields(e->s->parts, e->s->xparts, with_cosmology,
+                         with_cooling, with_temperature, with_fof, with_stf,
+                         with_rt, e, &num_fields, list);
+
+  /* Loop over each output field */
+  for (int i = 0; i < num_fields; i++) {
+
+    /* Did the user cancel this field? */
+    char field[PARSER_MAX_LINE_SIZE];
+    sprintf(field, "SelectOutputLOS:%.*s", FIELD_BUFFER_SIZE, list[i].name);
+    parser_get_opt_param_int(params, field, 1);
+  }
+}
+
 /**
  *  @brief Create a #line_of_sight object from its attributes
  */
@@ -376,6 +410,9 @@ void write_los_hdf5_dataset(const struct io_props props, const size_t N,
   io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
   io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
   io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -551,7 +588,8 @@ void write_hdf5_header(hid_t h_file, const struct engine *e,
   ic_info_write_hdf5(e->ics_metadata, h_file);
 
   /* Write all the meta-data */
-  io_write_meta_data(h_file, e, e->internal_units, e->snapshot_units);
+  io_write_meta_data(h_file, e, e->internal_units, e->snapshot_units,
+                     /*fof=*/0);
 
   /* Print the LOS properties */
   h_grp = H5Gcreate(h_file, "/LineOfSightParameters", H5P_DEFAULT, H5P_DEFAULT,
@@ -732,14 +770,21 @@ void do_line_of_sight(struct engine *e) {
 #endif
 
   /* Node 0 creates the HDF5 file. */
-  hid_t h_file = -1, h_grp = -1;
+  hid_t h_file = -1, h_grp = -1, h_props = -1;
   char fileName[256], groupName[200];
 
   if (e->nodeID == 0) {
     sprintf(fileName, "%s_%04i.hdf5", LOS_params->basename,
             e->los_output_count);
     if (verbose) message("Creating LOS file: %s", fileName);
-    h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+
+    /* Set the minimal API version to avoid issues with advanced features */
+    h_props = H5Pcreate(H5P_FILE_ACCESS);
+    hid_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                     HDF5_HIGHEST_FILE_FORMAT_VERSION);
+    if (err < 0) error("Error setting the hdf5 API version");
+
+    h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
     if (h_file < 0) error("Error while opening file '%s'.", fileName);
   }
 
@@ -1044,6 +1089,7 @@ void do_line_of_sight(struct engine *e) {
 
     /* Close HDF5 file */
     H5Fclose(h_file);
+    H5Pclose(h_props);
   }
 
   /* Up the LOS counter. */
diff --git a/src/line_of_sight.h b/src/line_of_sight.h
index 158c0fce6cb348f9dde95da0302b170d3091de79..6abf0832f1d472d117de4f15b5c1886d89d4b3c8 100644
--- a/src/line_of_sight.h
+++ b/src/line_of_sight.h
@@ -25,9 +25,11 @@
 #include <config.h>
 
 /* Local includes. */
-#include "engine.h"
 #include "io_properties.h"
 
+/* Pre-declarations */
+struct engine;
+
 /**
  * @brief Maps the LOS axis geometry to the simulation axis geometry.
  *
@@ -117,6 +119,7 @@ void print_los_info(const struct line_of_sight *Los, const int i);
 void do_line_of_sight(struct engine *e);
 void los_init(const double dim[3], struct los_props *los_params,
               struct swift_params *params);
+void los_io_output_check(const struct engine *e);
 
 void los_struct_dump(const struct los_props *internal_los, FILE *stream);
 void los_struct_restore(const struct los_props *internal_los, FILE *stream);
diff --git a/src/lock.h b/src/lock.h
index 39601b0c52e414dad1a507b406c54640a254df30..e350303fe9a0da11bf02a688a3b1ac32116dde83 100644
--- a/src/lock.h
+++ b/src/lock.h
@@ -52,8 +52,9 @@
 #define lock_init(l) (*(l) = 0)
 #define lock_destroy(l) 0
 INLINE static int lock_lock(volatile int *l) {
-  while (atomic_cas(l, 0, 1) != 0)
-    ;
+  while (atomic_cas(l, 0, 1) != 0) {
+    /* Nothing to do here. */
+  }
   return 0;
 }
 #define lock_trylock(l) ((*(l)) ? 1 : atomic_cas(l, 0, 1))
diff --git a/src/memuse.c b/src/memuse.c
index 29844fda0e323a3496ee2c972e1adbcf2182d1e3..b5d125dbd5ea7eb71a51ae0ba1b72572e5b7dc3e 100644
--- a/src/memuse.c
+++ b/src/memuse.c
@@ -149,8 +149,9 @@ static void memuse_log_reallocate(size_t ind) {
         error("Failed to re-allocate memuse log.");
 
       /* Wait for all writes to the old buffer to complete. */
-      while (memuse_log_done < memuse_log_size)
-        ;
+      while (memuse_log_done < memuse_log_size) {
+        /* Nothing to do here */
+      }
 
       /* Copy to new buffer. */
       memcpy(new_log, memuse_log,
@@ -189,8 +190,9 @@ void memuse_log_allocation(const char *label, void *ptr, int allocated,
   if (ind == memuse_log_size) memuse_log_reallocate(ind);
 
   /* Other threads wait for space. */
-  while (ind > memuse_log_size)
-    ;
+  while (ind > memuse_log_size) {
+    /* Nothing to do here. */
+  }
 
   /* Guard against case when we have already overran the available new
    * space. */
diff --git a/src/memuse.h b/src/memuse.h
index b40869c87c1341265a07dae3e7aeea4b3da03c32..5883e68684fdb6f54925c65fb66174ce8f4f1f80 100644
--- a/src/memuse.h
+++ b/src/memuse.h
@@ -41,6 +41,25 @@ void memuse_log_allocation(const char *label, void *ptr, int allocated,
 #define memuse_log_allocation(label, ptr, allocated, size)
 #endif
 
+#ifdef HAVE_LSAN_IGNORE_OBJECT
+#include <sanitizer/lsan_interface.h>
+/**
+ * @brief if allocated memory is intended to leak, mark it to be
+ *        ignored in any leak reports.
+ *        Currently only works for the GCC/clang address sanitizer.
+ *
+ * @param memptr pointer to the memory that will leak.
+ */
+__attribute__((always_inline)) inline void swift_ignore_leak(
+    const void *memptr) {
+  __lsan_ignore_object(memptr);
+}
+#else
+
+/* No-op when not checking for leaks. */
+#define swift_ignore_leak(memptr)
+#endif
+
 /**
  * @brief allocate aligned memory. The use and results are the same as the
  *        posix_memalign function. This function should be used for any
diff --git a/src/mhd/None/mhd_io.h b/src/mhd/None/mhd_io.h
index 7c0a4a23594f0e7631f842c745248e632995fbfd..672e7f80f0bde284208f1185be31071ce0ec5ef0 100644
--- a/src/mhd/None/mhd_io.h
+++ b/src/mhd/None/mhd_io.h
@@ -19,6 +19,7 @@
 #ifndef SWIFT_NONE_MHD_IO_H
 #define SWIFT_NONE_MHD_IO_H
 
+#include "io_properties.h"
 #include "statistics.h"
 
 /**
@@ -53,6 +54,9 @@ INLINE static int mhd_write_particles(const struct part* parts,
  * @brief Writes the current model of MHD to the file
  * @param h_grpsph The HDF5 group in which to write
  */
-INLINE static void mhd_write_flavour(hid_t h_grpsph) {}
+INLINE static void mhd_write_flavour(hid_t h_grpsph) {
+
+  io_write_attribute_s(h_grpsph, "MHD Flavour", "No MHD");
+}
 
 #endif /* SWIFT_NONE_MHD_IO_H */
diff --git a/src/mpiuse.c b/src/mpiuse.c
index 7d00934226e61372f2b10db4214da2957e823709..96d634de3ec5b1877d6ebae14bc866d544fe9ea7 100644
--- a/src/mpiuse.c
+++ b/src/mpiuse.c
@@ -123,8 +123,9 @@ static void mpiuse_log_reallocate(size_t ind) {
       error("Failed to re-allocate MPI use log.");
 
     /* Wait for all writes to the old buffer to complete. */
-    while (mpiuse_log_done < mpiuse_log_size)
-      ;
+    while (mpiuse_log_done < mpiuse_log_size) {
+      /* Nothing to do here */
+    }
 
     /* Copy to new buffer. */
     memcpy(new_log, mpiuse_log,
@@ -159,8 +160,9 @@ void mpiuse_log_allocation(int type, int subtype, void *ptr, int activation,
   if (ind == mpiuse_log_size) mpiuse_log_reallocate(ind);
 
   /* Other threads wait for space. */
-  while (ind > mpiuse_log_size)
-    ;
+  while (ind > mpiuse_log_size) {
+    /* Nothing to do here */
+  }
 
   /* Record the log. */
   mpiuse_log[ind].step = engine_current_step;
diff --git a/src/multipole.h b/src/multipole.h
index 26c163a7c952361c63f923a60ca040647203a57e..1844d7bbfc631a67c70d48b0fcc15c254ac0ec2e 100644
--- a/src/multipole.h
+++ b/src/multipole.h
@@ -477,6 +477,17 @@ __attribute__((nonnull)) INLINE static int gravity_multipole_equal(
   const double v2 = ma->vel[0] * ma->vel[0] + ma->vel[1] * ma->vel[1] +
                     ma->vel[2] * ma->vel[2];
 
+#ifdef SWIFT_DEBUG_CHECKS
+  const int size = ma->num_gpart;
+
+  if (ma->num_gpart != mb->num_gpart) {
+    message("Number of particles does not match!");
+    return 0;
+  }
+#else
+  const int size = 1;
+#endif
+
   /* Check maximal softening */
   if (fabsf(ma->max_softening - mb->max_softening) /
           fabsf(ma->max_softening + mb->max_softening) >
@@ -494,21 +505,21 @@ __attribute__((nonnull)) INLINE static int gravity_multipole_equal(
   }
 
   /* Check bulk velocity (if non-zero and component > 1% of norm)*/
-  if (fabsf(ma->vel[0] + mb->vel[0]) > 1e-10 &&
+  if (fabsf(ma->vel[0] + mb->vel[0]) > 1e-10 * size &&
       (ma->vel[0] * ma->vel[0]) > 0.0001 * v2 &&
       fabsf(ma->vel[0] - mb->vel[0]) / fabsf(ma->vel[0] + mb->vel[0]) >
           tolerance) {
     message("v[0] different");
     return 0;
   }
-  if (fabsf(ma->vel[1] + mb->vel[1]) > 1e-10 &&
+  if (fabsf(ma->vel[1] + mb->vel[1]) > 1e-10 * size &&
       (ma->vel[1] * ma->vel[1]) > 0.0001 * v2 &&
       fabsf(ma->vel[1] - mb->vel[1]) / fabsf(ma->vel[1] + mb->vel[1]) >
           tolerance) {
     message("v[1] different");
     return 0;
   }
-  if (fabsf(ma->vel[2] + mb->vel[2]) > 1e-10 &&
+  if (fabsf(ma->vel[2] + mb->vel[2]) > 1e-10 * size &&
       (ma->vel[2] * ma->vel[2]) > 0.0001 * v2 &&
       fabsf(ma->vel[2] - mb->vel[2]) / fabsf(ma->vel[2] + mb->vel[2]) >
           tolerance) {
diff --git a/src/neutrino/Default/neutrino.c b/src/neutrino/Default/neutrino.c
index 5b6e35ca0069034618d22a60f90ddb703f9cab1d..35e6b7697c22daed65dec0a6a3279df1a2c110fb 100644
--- a/src/neutrino/Default/neutrino.c
+++ b/src/neutrino/Default/neutrino.c
@@ -215,7 +215,7 @@ void compute_neutrino_diagnostics(
                     ppi_sum, mass_sum, weight2_sum};
   double total_sums[7];
 
-  MPI_Reduce(&sums, &total_sums, 7, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
+  MPI_Reduce(sums, total_sums, 7, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
 
   double total_p = total_sums[0];
   double total_p2 = total_sums[1];
diff --git a/src/neutrino/Default/neutrino.h b/src/neutrino/Default/neutrino.h
index aaccc5a56b3f7cfd384813ff46d63ab05b276b4b..65770e99fe9cae30c53eb7885bb69edcf3fd7fcb 100644
--- a/src/neutrino/Default/neutrino.h
+++ b/src/neutrino/Default/neutrino.h
@@ -74,13 +74,14 @@ INLINE static double neutrino_mass_factor(
   const double eV_mass = eV / (c * c);  // 1 eV/c^2 in internal mass units
   const double prefactor = (1.5 * M_ZETA_3) / (M_PI * M_PI);
   const double T_nu = cosmo->T_nu_0;
+  const int N_nu = cosmo->N_nu;
 
   /* Compute the comoving number density per flavour */
   const double kThc = k_b * T_nu / (hbar * c);
   const double n = prefactor * kThc * kThc * kThc;
 
   /* Compute the conversion factor per flavour */
-  const double mass_factor = nr_nuparts / (n * volume);
+  const double mass_factor = nr_nuparts / (n * volume * N_nu);
 
   /* Convert to eV */
   const double mass_factor_eV = mass_factor / eV_mass;
diff --git a/src/neutrino/Default/neutrino_response.c b/src/neutrino/Default/neutrino_response.c
index 4d841a633c7efb598349652ee33bc05279e4c5ee..4ccec5a83f18b79ad7459296ce8a0847519549d2 100644
--- a/src/neutrino/Default/neutrino_response.c
+++ b/src/neutrino/Default/neutrino_response.c
@@ -497,7 +497,8 @@ void neutrino_response_apply_neutrino_response_mapper(void *map_data,
         if (u_k < 0 || u_a < 0 || u_k > 1 || u_a > 1 ||
             k_index > wavenumber_length || a_index > timestep_length)
           error("Interpolation out of bounds error: %g %g %g %g %llu %llu\n",
-                u_k, u_a, sqrt(k2), pt_ratio_interp, k_index, a_index);
+                u_k, u_a, sqrt(k2), pt_ratio_interp,
+                (unsigned long long)k_index, (unsigned long long)a_index);
 #endif
 
         /* Apply to the mesh */
diff --git a/src/parallel_io.c b/src/parallel_io.c
index bc81e1bbc433eb749d28858a290bbe12a2748b0f..cf67c9fe9d775f7b6678aa684dc86572792e7435 100644
--- a/src/parallel_io.c
+++ b/src/parallel_io.c
@@ -69,7 +69,10 @@
 #define HDF5_PARALLEL_IO_MAX_BYTES 2147000000LL
 
 /* Are we timing the i/o? */
-//#define IO_SPEED_MEASUREMENT
+// #define IO_SPEED_MEASUREMENT
+
+/* Max number of entries that can be written for a given particle type */
+static const int io_max_size_output_list = 100;
 
 /**
  * @brief Reads a chunk of data from an open HDF5 dataset
@@ -476,6 +479,9 @@ void prepare_array_parallel(
   io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
   io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
   io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -997,8 +1003,8 @@ void read_ic_parallel(char* fileName, const struct unit_system* internal_units,
       error("Error while opening particle group %s.", partTypeGroupName);
 
     int num_fields = 0;
-    struct io_props list[100];
-    bzero(list, 100 * sizeof(struct io_props));
+    struct io_props list[io_max_size_output_list];
+    bzero(list, io_max_size_output_list * sizeof(struct io_props));
     size_t Nparticles = 0;
 
     /* Read particle fields into the particle structure */
@@ -1148,6 +1154,7 @@ void read_ic_parallel(char* fileName, const struct unit_system* internal_units,
  * @param numFields The number of fields to write for each particle type.
  * @param internal_units The #unit_system used internally.
  * @param snapshot_units The #unit_system used in the snapshots.
+ * @param fof Is this a snapshot related to a stand-alone FOF call?
  * @param subsample_any Are any fields being subsampled?
  * @param subsample_fraction The subsampling fraction of each particle type.
  */
@@ -1158,7 +1165,7 @@ void prepare_file(struct engine* e, const char* fileName,
                   const int numFields[swift_type_count],
                   const char current_selection_name[FIELD_BUFFER_SIZE],
                   const struct unit_system* internal_units,
-                  const struct unit_system* snapshot_units,
+                  const struct unit_system* snapshot_units, const int fof,
                   const int subsample_any,
                   const float subsample_fraction[swift_type_count]) {
 
@@ -1184,8 +1191,14 @@ void prepare_file(struct engine* e, const char* fileName,
   /* Prepare the XMF file for the new entry */
   xmfFile = xmf_prepare_file(xmfFileName);
 
+  /* Set the minimal API version to avoid issues with advanced features */
+  hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+  herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                    HDF5_HIGHEST_FILE_FORMAT_VERSION);
+  if (err < 0) error("Error setting the hdf5 API version");
+
   /* Open HDF5 file with the chosen parameters */
-  hid_t h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+  hid_t h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
   if (h_file < 0) error("Error while opening file '%s'.", fileName);
 
   /* Write the part of the XMF file corresponding to this
@@ -1294,7 +1307,7 @@ void prepare_file(struct engine* e, const char* fileName,
   ic_info_write_hdf5(e->ics_metadata, h_file);
 
   /* Write all the meta-data */
-  io_write_meta_data(h_file, e, internal_units, snapshot_units);
+  io_write_meta_data(h_file, e, internal_units, snapshot_units, fof);
 
   /* Loop over all particle types */
   for (int ptype = 0; ptype < swift_type_count; ptype++) {
@@ -1331,8 +1344,8 @@ void prepare_file(struct engine* e, const char* fileName,
     io_write_attribute_ll(h_grp, "TotalNumberOfParticles", N_total[ptype]);
 
     int num_fields = 0;
-    struct io_props list[100];
-    bzero(list, 100 * sizeof(struct io_props));
+    struct io_props list[io_max_size_output_list];
+    bzero(list, io_max_size_output_list * sizeof(struct io_props));
 
     /* Write particle fields from the particle structure */
     switch (ptype) {
@@ -1373,6 +1386,15 @@ void prepare_file(struct engine* e, const char* fileName,
         error("Particle Type %d not yet supported. Aborting", ptype);
     }
 
+    /* Verify we are not going to crash when writing below */
+    if (num_fields >= io_max_size_output_list)
+      error("Too many fields to write for particle type %d", ptype);
+    for (int i = 0; i < num_fields; ++i) {
+      if (!list[i].is_used) error("List of field contains an empty entry!");
+      if (!list[i].dimension)
+        error("Dimension of field '%s' is <= 1!", list[i].name);
+    }
+
     /* Did the user specify a non-standard default for the entire particle
      * type? */
     const enum lossy_compression_schemes compression_level_current_default =
@@ -1414,6 +1436,7 @@ void prepare_file(struct engine* e, const char* fileName,
 
   /* Close the file for now */
   H5Fclose(h_file);
+  H5Pclose(h_props);
 }
 
 /**
@@ -1423,6 +1446,7 @@ void prepare_file(struct engine* e, const char* fileName,
  * @param e The engine containing all the system.
  * @param internal_units The #unit_system used internally
  * @param snapshot_units The #unit_system used in the snapshots
+ * @param fof Is this a snapshot related to a stand-alone FOF call?
  * @param mpi_rank The MPI rank of this node.
  * @param mpi_size The number of MPI ranks.
  * @param comm The MPI communicator.
@@ -1439,8 +1463,8 @@ void prepare_file(struct engine* e, const char* fileName,
 void write_output_parallel(struct engine* e,
                            const struct unit_system* internal_units,
                            const struct unit_system* snapshot_units,
-                           const int mpi_rank, const int mpi_size,
-                           MPI_Comm comm, MPI_Info info) {
+                           const int fof, const int mpi_rank,
+                           const int mpi_size, MPI_Comm comm, MPI_Info info) {
 
   const struct part* parts = e->s->parts;
   const struct xpart* xparts = e->s->xparts;
@@ -1596,7 +1620,7 @@ void write_output_parallel(struct engine* e,
   }
 
   /* Compute offset in the file and total number of particles */
-  size_t N[swift_type_count] = {
+  long long N[swift_type_count] = {
       Ngas_written,   Ndm_written,         Ndm_background, Nsinks_written,
       Nstars_written, Nblackholes_written, Ndm_neutrino};
   long long N_total[swift_type_count] = {0};
@@ -1624,7 +1648,7 @@ void write_output_parallel(struct engine* e,
   /* Rank 0 prepares the file */
   if (mpi_rank == 0)
     prepare_file(e, fileName, xmfFileName, N_total, to_write, numFields,
-                 current_selection_name, internal_units, snapshot_units,
+                 current_selection_name, internal_units, snapshot_units, fof,
                  subsample_any, subsample_fraction);
 
   MPI_Barrier(MPI_COMM_WORLD);
@@ -1668,6 +1692,11 @@ void write_output_parallel(struct engine* e,
   /* Prepare some file-access properties */
   hid_t plist_id = H5Pcreate(H5P_FILE_ACCESS);
 
+  /* Set the minimal API version to avoid issues with advanced features */
+  herr_t err = H5Pset_libver_bounds(plist_id, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                    HDF5_HIGHEST_FILE_FORMAT_VERSION);
+  if (err < 0) error("Error setting the hdf5 API version");
+
   /* Set some MPI-IO parameters */
   // MPI_Info_set(info, "IBM_largeblock_io", "true");
   MPI_Info_set(info, "romio_cb_write", "enable");
diff --git a/src/parallel_io.h b/src/parallel_io.h
index aaac00aa4c8b1153fe844e61b9146843834030cf..98be1278f5889a2b3f61459ba0ef5fe248a12860 100644
--- a/src/parallel_io.h
+++ b/src/parallel_io.h
@@ -52,8 +52,8 @@ void read_ic_parallel(char* fileName, const struct unit_system* internal_units,
 void write_output_parallel(struct engine* e,
                            const struct unit_system* internal_units,
                            const struct unit_system* snapshot_units,
-                           int mpi_rank, int mpi_size, MPI_Comm comm,
-                           MPI_Info info);
+                           const int fof, int mpi_rank, int mpi_size,
+                           MPI_Comm comm, MPI_Info info);
 #endif
 
 #endif /* SWIFT_PARALLEL_IO_H */
diff --git a/src/parser.h b/src/parser.h
index 072bbe03b719be025d2ddf04523caa8f13ef28af..e97188b48c91e9fda329cb861101829ca7c7c1d2 100644
--- a/src/parser.h
+++ b/src/parser.h
@@ -32,7 +32,7 @@
 
 /* Some constants. */
 #define PARSER_MAX_LINE_SIZE 256
-#define PARSER_MAX_NO_OF_PARAMS 600
+#define PARSER_MAX_NO_OF_PARAMS 700
 #define PARSER_MAX_NO_OF_SECTIONS 64
 
 /* A parameter in the input file */
diff --git a/src/part.c b/src/part.c
index aec016bea44387afa5b155e7f1330c23625b3892..f513153d61ba9b277a0b6b88ae9e4cf61c766e10 100644
--- a/src/part.c
+++ b/src/part.c
@@ -544,7 +544,7 @@ MPI_Datatype xpart_mpi_type;
 MPI_Datatype gpart_mpi_type;
 MPI_Datatype spart_mpi_type;
 MPI_Datatype bpart_mpi_type;
-MPI_Datatype lospart_mpi_type;
+MPI_Datatype sink_mpi_type;
 
 /**
  * @brief Registers MPI particle types.
@@ -582,6 +582,11 @@ void part_create_mpi_types(void) {
       MPI_Type_commit(&bpart_mpi_type) != MPI_SUCCESS) {
     error("Failed to create MPI type for bparts.");
   }
+  if (MPI_Type_contiguous(sizeof(struct sink) / sizeof(unsigned char), MPI_BYTE,
+                          &sink_mpi_type) != MPI_SUCCESS ||
+      MPI_Type_commit(&sink_mpi_type) != MPI_SUCCESS) {
+    error("Failed to create MPI type for sink.");
+  }
 }
 
 void part_free_mpi_types(void) {
@@ -591,6 +596,6 @@ void part_free_mpi_types(void) {
   MPI_Type_free(&gpart_mpi_type);
   MPI_Type_free(&spart_mpi_type);
   MPI_Type_free(&bpart_mpi_type);
-  MPI_Type_free(&lospart_mpi_type);
+  MPI_Type_free(&sink_mpi_type);
 }
 #endif
diff --git a/src/part.h b/src/part.h
index dce15ece9df269b51f5ceebefa32e3f9b52abc0b..c68c91cfa804e476cc41cd26db55d5e1397790e2 100644
--- a/src/part.h
+++ b/src/part.h
@@ -73,13 +73,18 @@ struct threadpool;
 #define hydro_need_extra_init_loop 0
 #define EXTRA_HYDRO_LOOP
 #define MPI_SYMMETRIC_FORCE_INTERACTION
-#elif defined(SHADOWFAX_SPH)
+#elif defined(SHADOWSWIFT)
 #include "./hydro/Shadowswift/hydro_part.h"
 #define hydro_need_extra_init_loop 0
 #define EXTRA_HYDRO_LOOP
 #elif defined(PLANETARY_SPH)
 #include "./hydro/Planetary/hydro_part.h"
 #define hydro_need_extra_init_loop 0
+#elif defined(REMIX_SPH)
+#include "./hydro/REMIX/hydro_part.h"
+#define hydro_need_extra_init_loop 0
+#define EXTRA_HYDRO_LOOP
+#define EXTRA_HYDRO_LOOP_TYPE2
 #elif defined(SPHENIX_SPH)
 #include "./hydro/SPHENIX/hydro_part.h"
 #define hydro_need_extra_init_loop 0
@@ -132,6 +137,8 @@ struct threadpool;
 /* Import the right sink particle definition */
 #if defined(SINK_NONE)
 #include "./sink/Default/sink_part.h"
+#elif defined(SINK_BASIC)
+#include "./sink/Basic/sink_part.h"
 #elif defined(SINK_GEAR)
 #include "./sink/GEAR/sink_part.h"
 #else
@@ -171,7 +178,7 @@ extern MPI_Datatype xpart_mpi_type;
 extern MPI_Datatype gpart_mpi_type;
 extern MPI_Datatype spart_mpi_type;
 extern MPI_Datatype bpart_mpi_type;
-extern MPI_Datatype lospart_mpi_type;
+extern MPI_Datatype sink_mpi_type;
 
 void part_create_mpi_types(void);
 void part_free_mpi_types(void);
diff --git a/src/particle_splitting.h b/src/particle_splitting.h
index 202f2e3b7195351145c12200ab1eb2401bb17245..a3f99a1d61ffb6fb16eb9ab9a61479b64480ef3e 100644
--- a/src/particle_splitting.h
+++ b/src/particle_splitting.h
@@ -36,7 +36,7 @@
  */
 __attribute__((always_inline)) INLINE static void
 particle_splitting_mark_part_as_not_split(
-    struct particle_splitting_data* restrict splitting_data, int id) {
+    struct particle_splitting_data* restrict splitting_data, long long id) {
 
   splitting_data->progenitor_id = id;
   splitting_data->split_tree = 0;
@@ -53,30 +53,61 @@ particle_splitting_mark_part_as_not_split(
  *           the splitting event.
  * @param sdj second particle_splitting_data* resulting from
  *           the splitting event.
+ * @param id_i ID of the first particle.
+ * @param id_j ID of the first particle.
+ * @param extra_split_logger File pointer (opened) where to log
+ *           the resetting of the split counters.
  */
 __attribute__((always_inline)) INLINE static void
 particle_splitting_update_binary_tree(
     struct particle_splitting_data* restrict sdi,
-    struct particle_splitting_data* restrict sdj) {
+    struct particle_splitting_data* restrict sdj, const long long id_i,
+    const long long id_j, FILE* extra_split_logger,
+    swift_lock_type* file_lock) {
+
+  /* Print warnings if we have split these particles more
+   * than the number of times the tree can accommodate.
+   * Warning is only printed once for each particle */
+  if (sdi->split_count > 0 &&
+      sdi->split_count % (8 * sizeof(sdi->split_tree)) == 0) {
+    message(
+        "Warning: Particle (%lld) with progenitor ID %lld with binary tree "
+        "%lld has been split over the maximum %zu times, making its binary "
+        "tree invalid.",
+        id_i, sdi->progenitor_id, sdi->split_tree, sizeof(sdi->split_tree));
+
+    /* Are we logging this? */
+    if (extra_split_logger != NULL) {
+
+      /* Log the old state before reseting. Use the lock to prevent multiple
+       * threads from writing at the same time. */
+      lock_lock(file_lock);
+      fprintf(extra_split_logger, "  %12d %20lld %20lld %20d %20lld\n",
+              engine_current_step, id_i, sdi->progenitor_id, sdi->split_count,
+              sdi->split_tree);
+      fflush(extra_split_logger);
+
+      /* Release the lock and continue in parallel */
+      if (lock_unlock(file_lock) != 0)
+        error("Impossible to unlock particle splitting");
+    }
+
+    /* Reset both counters and trees */
+    sdi->split_tree = 0LL;
+    sdj->split_tree = 0LL;
+
+    /* Set both particles as having particle i as their progenitor */
+    sdi->progenitor_id = id_i;
+    sdj->progenitor_id = id_i;
+  }
 
   /* Update the binary tree */
-  sdj->split_tree |= 1LL << sdj->split_count;
+  sdj->split_tree |= 1LL << sdj->split_count % (8 * sizeof(sdi->split_tree));
 
   /* Increase counters on both; sdi implicitly has a zero
    * in the relevant spot in its binary tree */
   sdj->split_count++;
   sdi->split_count++;
-
-  /* Print warnings if we have split these particles more
-   * than the number of times the tree can accommodate.
-   * Warning is only printed once for each particle */
-  if (sdi->split_count == 8 * sizeof(sdi->split_tree)) {
-    message(
-        "Warning: Particle with progenitor ID %lld with binary tree %lld has "
-        "been split over the maximum %zu times, making its binary tree "
-        "invalid.",
-        sdi->progenitor_id, sdi->split_tree, sizeof(sdi->split_tree));
-  }
 }
 
 /**
@@ -107,9 +138,7 @@ INLINE static int particle_splitting_write_particles(const struct part* parts,
       "SplitCounts", UINT8, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
       split_data.split_count,
       "Number of times this particle has been split. Note that both particles "
-      "that take part in the splitting have counter incremented, so the "
-      "number of splitting events in an entire simulation is half of the sum "
-      "of all of these numbers.");
+      "that take part in the splitting have their counter incremented.");
 
   list[2] = io_make_output_field(
       "SplitTrees", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
@@ -146,8 +175,7 @@ INLINE static int particle_splitting_write_sparticles(
       split_data.split_count,
       "Number of times the gas particle that turned into this star particle "
       "was split. Note that both particles that take part in the splitting "
-      "have this counter incremented, so the number of splitting events in an "
-      "entire simulation is half of the sum of all of these numbers.");
+      "have their counter incremented.");
 
   list[2] = io_make_output_field(
       "SplitTrees", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, sparts,
@@ -183,8 +211,7 @@ INLINE static int particle_splitting_write_bparticles(
       split_data.split_count,
       "Number of times the gas particle that became this BH seed "
       "was split. Note that both particles that take part in the splitting "
-      "have this counter incremented, so the number of splitting events in an "
-      "entire simulation is half of the sum of all of these numbers.");
+      "have their counter incremented.");
 
   list[2] = io_make_output_field(
       "SplitTrees", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
diff --git a/src/partition.c b/src/partition.c
index 2030451dadcf0ff686c0c9966ee8b5d362394059..f29a8036844a8041f9208a4987ef744847ef07b7 100644
--- a/src/partition.c
+++ b/src/partition.c
@@ -364,7 +364,7 @@ struct counts_mapper_data {
       if (cid < lcid) lcid = cid;                                              \
     }                                                                          \
     int nused = ucid - lcid + 1;                                               \
-    if ((lcounts = (double *)calloc(sizeof(double), nused)) == NULL)           \
+    if ((lcounts = (double *)calloc(nused, sizeof(double))) == NULL)           \
       error("Failed to allocate counts thread-specific buffer");               \
     for (int k = 0; k < num_elements; k++) {                                   \
       const int cid =                                                          \
@@ -1417,13 +1417,17 @@ void partition_gather_weights(void *map_data, int num_elements,
 
     /* Get the top-level cells involved. */
     struct cell *ci, *cj;
-    for (ci = t->ci; ci->parent != NULL; ci = ci->parent)
-      ;
-    if (t->cj != NULL)
-      for (cj = t->cj; cj->parent != NULL; cj = cj->parent)
-        ;
-    else
+    for (ci = t->ci; ci->parent != NULL; ci = ci->parent) {
+      /* Nothing to do here. */
+    }
+
+    if (t->cj != NULL) {
+      for (cj = t->cj; cj->parent != NULL; cj = cj->parent) {
+        /* Nothing to do here. */
+      }
+    } else {
       cj = NULL;
+    }
 
     /* Get the cell IDs. */
     int cid = ci - cells;
@@ -2063,7 +2067,7 @@ void partition_init(struct partition *partition,
 
 #ifdef WITH_MPI
 
-/* Defaults make use of METIS if available */
+  /* Defaults make use of METIS if available */
 #if defined(HAVE_METIS) || defined(HAVE_PARMETIS)
   const char *default_repart = "fullcosts";
   const char *default_part = "edgememory";
@@ -2079,6 +2083,10 @@ void partition_init(struct partition *partition,
   factor(partition->grid[0] * partition->grid[1], &partition->grid[1],
          &partition->grid[0]);
 
+  /* Initialise the repartition celllist. */
+  repartition->ncelllist = 0;
+  repartition->celllist = NULL;
+
   /* Now let's check what the user wants as an initial domain. */
   char part_type[20];
   parser_get_opt_param_string(params, "DomainDecomposition:initial_type",
@@ -2187,10 +2195,6 @@ void partition_init(struct partition *partition,
   repartition->itr =
       parser_get_opt_param_float(params, "DomainDecomposition:itr", 100.0f);
 
-  /* Clear the celllist for use. */
-  repartition->ncelllist = 0;
-  repartition->celllist = NULL;
-
   /* Do we have fixed costs available? These can be used to force
    * repartitioning at any time. Not required if not repartitioning.*/
   repartition->use_fixed_costs = parser_get_opt_param_int(
@@ -2219,6 +2223,24 @@ void partition_init(struct partition *partition,
 #endif
 }
 
+/**
+ * @brief Clean up any allocated resources.
+ *
+ * @param partition The #partition
+ * @param repartition The #repartition
+ */
+void partition_clean(struct partition *partition,
+                     struct repartition *repartition) {
+#ifdef WITH_MPI
+  /* Only the celllist is dynamic. */
+  if (repartition->celllist != NULL) free(repartition->celllist);
+
+  /* Zero structs for reuse. */
+  bzero(partition, sizeof(struct partition));
+  bzero(repartition, sizeof(struct repartition));
+#endif
+}
+
 #ifdef WITH_MPI
 /**
  * @brief Set the fixed costs for repartition using METIS.
@@ -2346,13 +2368,16 @@ static void check_weights(struct task *tasks, int nr_tasks,
 
     /* Get the top-level cells involved. */
     struct cell *ci, *cj;
-    for (ci = t->ci; ci->parent != NULL; ci = ci->parent)
-      ;
-    if (t->cj != NULL)
-      for (cj = t->cj; cj->parent != NULL; cj = cj->parent)
-        ;
-    else
+    for (ci = t->ci; ci->parent != NULL; ci = ci->parent) {
+      /* Nothing to do here */
+    }
+    if (t->cj != NULL) {
+      for (cj = t->cj; cj->parent != NULL; cj = cj->parent) {
+        /* Nothing to do here */
+      }
+    } else {
       cj = NULL;
+    }
 
     /* Get the cell IDs. */
     int cid = ci - cells;
diff --git a/src/partition.h b/src/partition.h
index 8f6dbbd148d510650578ff0a857f9fd29c3e3c25..b1c2cd2f2ae4d359d8fffc0b72c862de56e69e5e 100644
--- a/src/partition.h
+++ b/src/partition.h
@@ -83,6 +83,9 @@ void partition_init(struct partition *partition,
                     struct repartition *repartition,
                     struct swift_params *params, int nr_nodes);
 
+void partition_clean(struct partition *partition,
+                     struct repartition *repartition);
+
 /* Dump/restore. */
 void partition_store_celllist(struct space *s, struct repartition *reparttype);
 void partition_restore_celllist(struct space *s,
diff --git a/src/potential.h b/src/potential.h
index 9011eee11123f0d04883a69119ac6ee86aeb354d..311c3b3b4c824424939cfa218e96ea8e7ae81581 100644
--- a/src/potential.h
+++ b/src/potential.h
@@ -42,6 +42,8 @@
 #include "./potential/nfw/potential.h"
 #elif defined(EXTERNAL_POTENTIAL_NFW_MN)
 #include "./potential/nfw_mn/potential.h"
+#elif defined(EXTERNAL_POTENTIAL_MWPotential2014)
+#include "./potential/MWPotential2014/potential.h"
 #elif defined(EXTERNAL_POTENTIAL_DISC_PATCH)
 #include "./potential/disc_patch/potential.h"
 #elif defined(EXTERNAL_POTENTIAL_SINE_WAVE)
diff --git a/src/potential/MWPotential2014/potential.h b/src/potential/MWPotential2014/potential.h
new file mode 100644
index 0000000000000000000000000000000000000000..c077f9c509e035ede17cec3d424ade52bd1f4b27
--- /dev/null
+++ b/src/potential/MWPotential2014/potential.h
@@ -0,0 +1,685 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023  Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_POTENTIAL_MWPotential2014_H
+#define SWIFT_POTENTIAL_MWPotential2014_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Some standard headers. */
+#include <float.h>
+#include <math.h>
+
+/* Local includes. */
+#include "error.h"
+#include "gravity.h"
+#include "integer_power.h"
+#include "parser.h"
+#include "part.h"
+#include "physical_constants.h"
+#include "space.h"
+#include "units.h"
+
+#ifdef HAVE_LIBGSL
+#include <gsl/gsl_sf_gamma.h>
+#endif
+
+#define potential_MW2014_num_coefficients 17
+
+/**
+ * @brief External Potential Properties - MWPotential2014 composed by
+ * NFW + Miyamoto-Nagai + Power Spherical cut-off potentials
+ *
+ * halo --> rho_NFW(r) = rho_0 / ( (r/R_s)*(1+r/R_s)^2 )
+ * disk --> Phi_MN(R,z) = -G * Mdisk / (R^2 + (Rdisk +
+ * (z^2+Zdisk^2)^1/2)^2)^(1/2) bulge --> rho_PSC(r) =
+ * amplitude*(r_1/r)^alpha*exp(-(r/r_c)^2)
+ *
+ * We however parametrise this in terms of c and virial_mass, Mdisk, Rdisk
+ * and Zdisk. Also, each potential is given a contribution amplitude such that
+ * the resulting potential is:
+ *      Phi_tot = f_1 * Phi_NFW + f_2 * Phi_MN + f_3 * Phi_PSC,
+ * with f_1, f_2 and f_3 contained in the array f.
+ *
+ * This potential is inspired by the following article:
+ * galpy: A Python Library for Galactic Dynamics, Jo Bovy (2015),
+ * Astrophys. J. Supp., 216, 29 (arXiv/1412.3451).
+ */
+struct external_potential {
+
+  /*! Position of the centre of potential */
+  double x[3];
+
+  /*! The scale radius of the NFW potential */
+  double r_s;
+
+  /*! The pre-factor \f$ 4 \pi G \rho_0 \r_s^3 \f$ */
+  double pre_factor;
+
+  /*! Hubble parameter */
+  double H;
+
+  /*! The concentration parameter */
+  double c_200;
+
+  /*! The virial mass */
+  double M_200;
+
+  /*! The NFW density at rs */
+  double rho_0;
+
+  /*! Disk Size */
+  double Rdisk;
+
+  /*! Disk height */
+  double Zdisk;
+
+  /*! Disk Mass */
+  double Mdisk;
+
+  /*! Amplitude for the PSC potential */
+  double amplitude;
+
+  /*! Reference radius for amplitude */
+  double r_1;
+
+  /*! Inner power */
+  double alpha;
+
+  /*! Cut-off radius */
+  double r_c;
+
+  /*! Contribution of each potential : f[0]*NFW + f[1]*MN + f[2]*PSP */
+  double f[3];
+
+  /*! Prefactor \f$ 2 \pi amplitude r_1^\alpha r_c^(3-\alpha) \f$ */
+  double prefactor_psc_1;
+
+  /*! Prefactor \f$ 2 \pi amplitude r_1^\alpha r_c^(2-\alpha) \f$ */
+  double prefactor_psc_2;
+
+  /*! Are we using the dynamical friction ?*/
+  int with_dynamical_friction;
+
+  /*! Coulomb logarithm for the dynamical friction */
+  double df_lnLambda;
+
+  /*! Satellite mass for the dynamical friction in code unit */
+  double df_satellite_mass;
+
+  /*! Polynomial fit coefficients for the velocity dispersion model */
+  double df_polyfit_coeffs[potential_MW2014_num_coefficients];
+
+  /*! Minimum velocity dispersion for the velocity dispersion model */
+  double df_sigma_floor;
+
+  /*! Radius below which the dynamical friction vanishes */
+  double df_core_radius;
+
+  /*! Gamma function evaluation \f$ \Gamma((3-\alpha)/2 \f$ */
+  double gamma_psc;
+
+  /*! Time-step condition pre_factor, this factor is used to multiply times the
+   * orbital time, so in the case of 0.01 we take 1% of the orbital time as
+   * the time integration steps */
+  double timestep_mult;
+
+  /*! Time-step condition pre_factor, this factor is used to constraints
+   * the time-step so that the norm of v*dt is a fraction of the acceleration */
+  double df_timestep_mult;
+
+  /*! Minimum time step based on the orbital time at the softening times
+   * the timestep_mult */
+  double mintime;
+
+  /*! Common log term \f$ \ln(1+c_{200}) - \frac{c_{200}}{1 + c_{200}} \f$ */
+  double log_c200_term;
+
+  /*! Softening length */
+  double eps;
+};
+
+/**
+ * @brief Computes the time-step due to the acceleration from the NFW + MN + PSC
+ * potential as a fraction (timestep_mult) of the circular orbital time of that
+ * particle.
+ *
+ * @param time The current time.
+ * @param potential The #external_potential used in the run.
+ * @param phys_const The physical constants in internal units.
+ * @param g Pointer to the g-particle data.
+ */
+__attribute__((always_inline)) INLINE static float external_gravity_timestep(
+    double time, const struct external_potential* restrict potential,
+    const struct phys_const* restrict phys_const,
+    const struct gpart* restrict g) {
+
+#ifdef HAVE_LIBGSL
+
+  const float dx = g->x[0] - potential->x[0];
+  const float dy = g->x[1] - potential->x[1];
+  const float dz = g->x[2] - potential->x[2];
+
+  const float R2 = dx * dx + dy * dy;
+  const float r = sqrtf(R2 + dz * dz + potential->eps * potential->eps);
+
+  /* Vcirc for NFW */
+  const float M_NFW = potential->pre_factor * (logf(1.0f + r / potential->r_s) -
+                                               r / (r + potential->r_s));
+  const float Vcirc_NFW = sqrtf((phys_const->const_newton_G * M_NFW) / r);
+
+  /* Now for MN */
+  const float R = sqrtf(R2);
+  const float sqrt_term = sqrtf(dz * dz + potential->Zdisk * potential->Zdisk);
+  const float MN_denominator =
+      powf(R2 + powf(potential->Rdisk + sqrt_term, 2.0f), 1.5f);
+  const float dPhi_dR_MN = potential->Mdisk * R / MN_denominator;
+  const float dPhi_dz_MN = potential->Mdisk * dz *
+                           (potential->Rdisk + sqrt_term) /
+                           (sqrt_term * MN_denominator);
+  const float Vcirc_MN = sqrtf(phys_const->const_newton_G * R * dPhi_dR_MN +
+                               phys_const->const_newton_G * dz * dPhi_dz_MN);
+
+  /* Now for PSC */
+  const float r2 = r * r;
+  const float M_psc =
+      potential->prefactor_psc_1 *
+      (potential->gamma_psc -
+       gsl_sf_gamma_inc(1.5f - 0.5f * potential->alpha,
+                        r2 / (potential->r_c * potential->r_c)));
+  const float Vcirc_PSC = sqrtf(phys_const->const_newton_G * M_psc / r);
+
+  /* Total circular velocity */
+  const float Vcirc = sqrtf(potential->f[0] * Vcirc_NFW * Vcirc_NFW +
+                            potential->f[1] * Vcirc_MN * Vcirc_MN +
+                            potential->f[2] * Vcirc_PSC * Vcirc_PSC);
+
+  const float period = 2.0f * M_PI * r / Vcirc;
+
+  /* Time-step as a fraction of the circular period */
+  float time_step = potential->timestep_mult * period;
+
+  /* Add dynamical friction */
+
+  if (potential->with_dynamical_friction) {
+
+    const float vx = g->v_full[0];
+    const float vy = g->v_full[1];
+    const float vz = g->v_full[2];
+
+    float v = sqrtf(vx * vx + vy * vy + vz * vz);
+
+    const float ax = g->a_grav[0];
+    const float ay = g->a_grav[1];
+    const float az = g->a_grav[2];
+
+    float a = sqrtf(ax * ax + ay * ay + az * az);
+
+    time_step = min(time_step, potential->df_timestep_mult * v / a);
+  }
+
+  return max(time_step, potential->mintime);
+
+#else
+  error("Code not compiled with GSL. Can't compute MWPotential2014.");
+  return 0.0;
+#endif
+}
+
+/**
+ * @brief Computes the mass density of the MW2014 model.
+ *
+ * @param x The x coordinate.
+ * @param y The y coordinate.
+ * @param z The y coordinate.
+ * @param time The current time (unused here).
+ * @param potential The #external_potential used in the run.
+ * @param phys_const Physical constants in internal units.
+ */
+__attribute__((always_inline)) INLINE static float external_gravity_get_density(
+    float x, float y, float z, double time,
+    const struct external_potential* potential,
+    const struct phys_const* const phys_const) {
+
+  /* First for the NFW profile */
+  const float R2 = x * x + y * y;
+  const float r = sqrtf(R2 + z * z + potential->eps * potential->eps);
+
+  /* First for the NFW part */
+  const float rho_NFW =
+      potential->rho_0 / ((r / potential->r_s) * (1 + r / potential->r_s) *
+                          (1 + r / potential->r_s));
+
+  /* Second the MN disk */
+  const float zb = sqrtf(potential->Zdisk * potential->Zdisk + z * z);
+  const float azb2 = integer_pow(potential->Rdisk + zb, 2);
+  const float cte =
+      (potential->Zdisk * potential->Zdisk * potential->Mdisk) / (4 * M_PI);
+  const float rho_MN =
+      cte * (potential->Rdisk * R2 + (potential->Rdisk + 3 * zb) * azb2) /
+      (pow(R2 + azb2, 2.5) * zb * zb * zb);
+
+  /* Third the bulge */
+  const float rho_PSC = potential->amplitude *
+                        pow(potential->r_1 / r, potential->alpha) *
+                        exp(-integer_pow(r / potential->r_c, 2));
+
+  /* Total density */
+  const float density = potential->f[0] * rho_NFW + potential->f[1] * rho_MN +
+                        potential->f[2] * rho_PSC;
+
+  return density;
+}
+
+/**
+ * @brief Computes the gravitational acceleration from an NFW Halo potential +
+ * MN disk + PSC bulge.
+ *
+ * Note that the accelerations are multiplied by Newton's G constant
+ * later on.
+ *
+ * a_x = 4 pi \rho_0 r_s^3 ( 1/((r+rs)*r^2) - log(1+r/rs)/r^3) * x
+ *      - dphi_PSC/dr*x/r
+ * a_y = 4 pi \rho_0 r_s^3 ( 1/((r+rs)*r^2) - log(1+r/rs)/r^3) * y
+ *      - dphi_PSC/dr*y/r
+ * a_z = 4 pi \rho_0 r_s^3 ( 1/((r+rs)*r^2) - log(1+r/rs)/r^3) * z
+ *      - dphi_PSC/dr*z/r
+ *
+ *
+ * @param time The current time.
+ * @param potential The #external_potential used in the run.
+ * @param phys_const The physical constants in internal units.
+ * @param g Pointer to the g-particle data.
+ */
+__attribute__((always_inline)) INLINE static void external_gravity_acceleration(
+    double time, const struct external_potential* restrict potential,
+    const struct phys_const* restrict phys_const, struct gpart* restrict g) {
+
+#ifdef HAVE_LIBGSL
+
+  const float dx = g->x[0] - potential->x[0];
+  const float dy = g->x[1] - potential->x[1];
+  const float dz = g->x[2] - potential->x[2];
+
+  /* First for the NFW part */
+  const float R2 = dx * dx + dy * dy;
+  const float r = sqrtf(R2 + dz * dz + potential->eps * potential->eps);
+
+  const float r_inv = 1.0f / r;
+  const float M_NFW = potential->pre_factor * (logf(1.0f + r / potential->r_s) -
+                                               r / (r + potential->r_s));
+  const float dpot_dr_NFW = M_NFW * r_inv * r_inv;
+  const float pot_nfw =
+      -potential->pre_factor * logf(1.0f + r / potential->r_s) * r_inv;
+  g->a_grav[0] -= potential->f[0] * dpot_dr_NFW * dx * r_inv;
+  g->a_grav[1] -= potential->f[0] * dpot_dr_NFW * dy * r_inv;
+  g->a_grav[2] -= potential->f[0] * dpot_dr_NFW * dz * r_inv;
+  gravity_add_comoving_potential(g, potential->f[0] * pot_nfw);
+
+  /* Now the the MN disk */
+  const float f1 = sqrtf(potential->Zdisk * potential->Zdisk + dz * dz);
+  const float f2 = potential->Rdisk + f1;
+  const float f3 = powf(R2 + f2 * f2, -1.5f);
+  const float mn_term = potential->Rdisk + sqrtf(potential->Zdisk + dz * dz);
+  const float pot_mn = -potential->Mdisk / sqrtf(R2 + mn_term * mn_term);
+
+  g->a_grav[0] -= potential->f[1] * potential->Mdisk * f3 * dx;
+  g->a_grav[1] -= potential->f[1] * potential->Mdisk * f3 * dy;
+  g->a_grav[2] -= potential->f[1] * potential->Mdisk * f3 * (f2 / f1) * dz;
+  gravity_add_comoving_potential(g, potential->f[1] * pot_mn);
+
+  /* Now the the PSC bulge */
+  const float r2 = r * r;
+  const float M_psc =
+      potential->prefactor_psc_1 *
+      (potential->gamma_psc -
+       gsl_sf_gamma_inc(1.5f - 0.5f * potential->alpha,
+                        r2 / (potential->r_c * potential->r_c)));
+  const float dpot_dr = M_psc / r2;
+  const float pot_psc =
+      -M_psc / r - potential->prefactor_psc_2 *
+                       gsl_sf_gamma_inc(1.0f - 0.5f * potential->alpha,
+                                        r2 / (potential->r_c * potential->r_c));
+
+  g->a_grav[0] -= potential->f[2] * dpot_dr * dx * r_inv;
+  g->a_grav[1] -= potential->f[2] * dpot_dr * dy * r_inv;
+  g->a_grav[2] -= potential->f[2] * dpot_dr * dz * r_inv;
+  gravity_add_comoving_potential(g, potential->f[2] * pot_psc);
+
+  /* Add dynamical friction */
+
+  if (potential->with_dynamical_friction) {
+
+    const float sqrtpi = sqrtf(M_PI);
+
+    const float vx = g->v_full[0];
+    const float vy = g->v_full[1];
+    const float vz = g->v_full[2];
+
+    const float v = sqrtf(vx * vx + vy * vy + vz * vz);
+
+    /* Compute the velocity dispertion as a function of the radius r, using
+     * using a high order polynomial interpolation.
+     */
+    double sigma = 0;
+    for (int i = 0; i < potential_MW2014_num_coefficients; i++)
+      sigma +=
+          potential
+              ->df_polyfit_coeffs[potential_MW2014_num_coefficients - 1 - i] *
+          integer_pow(r, i);
+
+    /* Prevent the velocity dispersion to be zero */
+    sigma = fmax(potential->df_sigma_floor, sigma);
+
+    /* Compute the chi parameter */
+    double X = v / (sqrt(2) * sigma);
+    double amp1 = erf(X) - ((2 * X / sqrtpi) * exp(-X * X));
+
+    /* Kill the dynamical friction at the center */
+    amp1 *= max(0, erf((r - potential->df_core_radius) /
+                       potential->df_core_radius / 2.0));
+
+    /* Compute the density */
+    float density =
+        external_gravity_get_density(dx, dy, dz, time, potential, phys_const);
+
+    /* Final factor (Binney & Tremaine 2008, eq. 8.7) */
+    float dyn_fric_timescale_inv =
+        -4 * M_PI * integer_pow(phys_const->const_newton_G, 2) /
+        integer_pow(v, 3) * density * potential->df_lnLambda * amp1 *
+        potential->df_satellite_mass;
+
+    /* Sanity check */
+    if (dyn_fric_timescale_inv > 0)
+      error("dyn_fric_timescale_inv is larger than zero (%g %g %g\n) !",
+            dyn_fric_timescale_inv, erf((r - 10) / 20), r);
+
+    /* Acceleration is per unit of G */
+    dyn_fric_timescale_inv /= phys_const->const_newton_G;
+
+    g->a_grav[0] += dyn_fric_timescale_inv * vx;
+    g->a_grav[1] += dyn_fric_timescale_inv * vy;
+    g->a_grav[2] += dyn_fric_timescale_inv * vz;
+  }
+
+#else
+  error("Code not compiled with GSL. Can't compute MWPotential2014.");
+#endif
+}
+
+/**
+ * @brief Computes the gravitational potential energy of a particle in an
+ * NFW potential + MN potential.
+ *
+ * phi = f[0] * (-4 * pi * G * rho_0 * r_s^3 * ln(1+r/r_s)) - f[1] * (G * Mdisk
+ * / sqrt(R^2 + (Rdisk + sqrt(z^2 + Zdisk^2))^2)) + f[2] * [- G / r * (2 * pi *
+ * amplitude * r_1^alpha r_c^(3 - alpha) * gamma_inf((3 - alpha)/2, r^2 / r_c^2)
+ * ) - 2 * pi * G * amplitude * r_1^alpha * r_c^(2 - alpha)
+ * Gamma_sup((2-alpha)/2, r^2 / r_c^2 ) ]
+ *
+ * @param time The current time (unused here).
+ * @param potential The #external_potential used in the run.
+ * @param phys_const Physical constants in internal units.
+ * @param g Pointer to the particle data.
+ */
+__attribute__((always_inline)) INLINE static float
+external_gravity_get_potential_energy(
+    double time, const struct external_potential* potential,
+    const struct phys_const* const phys_const, const struct gpart* g) {
+
+#ifdef HAVE_LIBGSL
+
+  const float dx = g->x[0] - potential->x[0];
+  const float dy = g->x[1] - potential->x[1];
+  const float dz = g->x[2] - potential->x[2];
+
+  /* First for the NFW profile */
+  const float R2 = dx * dx + dy * dy;
+  const float r = sqrtf(R2 + dz * dz + potential->eps * potential->eps);
+  const float r_inv = 1.0f / r;
+  const float pot_nfw =
+      -potential->pre_factor * logf(1.0f + r / potential->r_s) * r_inv;
+
+  /* Now for the MN disk */
+  const float sqrt_term = sqrtf(dz * dz + potential->Zdisk * potential->Zdisk);
+  const float MN_denominator =
+      sqrtf(R2 + powf(potential->Rdisk + sqrt_term, 2.0f));
+  const float mn_pot = -potential->Mdisk / MN_denominator;
+
+  /* Now for PSC bulge */
+  const float r2 = r * r;
+  const float M_psc =
+      potential->prefactor_psc_1 *
+      (potential->gamma_psc -
+       gsl_sf_gamma_inc(1.5f - 0.5f * potential->alpha,
+                        r2 / (potential->r_c * potential->r_c)));
+  const float psc_pot =
+      -M_psc / r - potential->prefactor_psc_2 *
+                       gsl_sf_gamma_inc(1.0f - 0.5f * potential->alpha,
+                                        r2 / (potential->r_c * potential->r_c));
+
+  return phys_const->const_newton_G *
+         (potential->f[0] * pot_nfw + potential->f[1] * mn_pot +
+          potential->f[2] * psc_pot);
+
+#else
+  error("Code not compiled with GSL. Can't compute MWPotential2014.");
+  return 0.0;
+#endif
+}
+
+/**
+ * @brief Initialises the external potential properties in the internal system
+ * of units.
+ *
+ * @param parameter_file The parsed parameter file
+ * @param phys_const Physical constants in internal units
+ * @param us The current internal system of units
+ * @param potential The external potential properties to initialize
+ */
+static INLINE void potential_init_backend(
+    struct swift_params* parameter_file, const struct phys_const* phys_const,
+    const struct unit_system* us, const struct space* s,
+    struct external_potential* potential) {
+
+#ifdef HAVE_LIBGSL
+
+  /* Read in the position of the centre of potential */
+  parser_get_param_double_array(
+      parameter_file, "MWPotential2014Potential:position", 3, potential->x);
+
+  /* Is the position absolute or relative to the centre of the box? */
+  const int useabspos = parser_get_param_int(
+      parameter_file, "MWPotential2014Potential:useabspos");
+
+  /* Define the default value in the above system of units*/
+  const double c_200_default = 9.823403437774843;
+  const double M_200_Msun_default = 147.41031542774076e10; /* M_sun  */
+  const double H_default = 127.78254614201471e-2;          /* no unit  */
+  const double Mdisk_Msun_default = 6.8e10;                /* M_sun  */
+  const double Rdisk_kpc_default = 3.0;                    /* kpc  */
+  const double Zdisk_kpc_default = 0.280;                  /* kpc  */
+  const double amplitude_Msun_per_kpc3_default = 1e10;     /* M_sun/kpc^3  */
+  const double r_1_kpc_default = 1.0;                      /* kpc  */
+  const double alpha_default = 1.8;                        /* no unit  */
+  const double r_c_kpc_default = 1.9;                      /* kpc  */
+  potential->f[0] = 0.4367419745056084;                    /* no unit  */
+  potential->f[1] = 1.002641971008805;                     /* no unit */
+  potential->f[2] = 0.022264787598364262;                  /* no unit */
+
+  if (!useabspos) {
+    potential->x[0] += s->dim[0] / 2.;
+    potential->x[1] += s->dim[1] / 2.;
+    potential->x[2] += s->dim[2] / 2.;
+  }
+
+  const double df_polyfit_coeffs_default[potential_MW2014_num_coefficients] = {
+      -2.96536595e-31, 8.88944631e-28, -1.18280578e-24, 9.29479457e-22,
+      -4.82805265e-19, 1.75460211e-16, -4.59976540e-14, 8.83166045e-12,
+      -1.24747700e-09, 1.29060404e-07, -9.65315026e-06, 5.10187806e-04,
+      -1.83800281e-02, 4.26501444e-01, -5.78038064e+00, 3.57956721e+01,
+      1.85478908e+02};
+
+  /* Read the other parameters of the model */
+  potential->timestep_mult = parser_get_param_double(
+      parameter_file, "MWPotential2014Potential:timestep_mult");
+
+  /* Bug fix : Read the softening length from the params file */
+  potential->eps = parser_get_param_double(parameter_file,
+                                           "MWPotential2014Potential:epsilon");
+
+  potential->c_200 = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:concentration", c_200_default);
+  potential->M_200 = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:M_200_Msun",
+      M_200_Msun_default);
+  potential->H = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:H", H_default);
+  potential->Mdisk = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:Mdisk_kpc", Mdisk_Msun_default);
+  potential->Rdisk = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:Rdisk_kpc", Rdisk_kpc_default);
+  potential->Zdisk = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:Zdisk_kpc", Zdisk_kpc_default);
+  potential->amplitude = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:amplitude_Msun_per_kpc3",
+      amplitude_Msun_per_kpc3_default);
+  potential->r_1 = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:r_1_kpc", r_1_kpc_default);
+  potential->alpha = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:alpha", alpha_default);
+  potential->r_c = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:r_c_kpc", r_c_kpc_default);
+  parser_get_opt_param_double_array(
+      parameter_file, "MWPotential2014Potential:potential_factors", 3,
+      potential->f);
+  potential->with_dynamical_friction = parser_get_opt_param_int(
+      parameter_file, "MWPotential2014Potential:with_dynamical_friction", 0);
+  potential->df_lnLambda = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:df_lnLambda", 5.0);
+  potential->df_satellite_mass = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:df_satellite_mass_in_Msun",
+      1e10);
+  potential->df_timestep_mult = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:df_timestep_mult", 0.1);
+  potential->df_core_radius = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:df_core_radius_in_kpc", 10);
+  potential->df_sigma_floor = parser_get_opt_param_double(
+      parameter_file, "MWPotential2014Potential:df_sigma_floor_km_p_s", 10.0);
+
+  /* Read all the dynamical friction coefficients */
+  for (int i = 0; i < potential_MW2014_num_coefficients; i++) {
+    char param_name[128];
+    sprintf(param_name, "MWPotential2014Potential:df_polyfit_coeffs%2d", i);
+    potential->df_polyfit_coeffs[i] = parser_get_opt_param_double(
+        parameter_file, param_name, df_polyfit_coeffs_default[i]);
+  }
+
+  /* Convert to internal system of units by using the
+   * physical constants defined in this system */
+  const double kpc = 1000. * phys_const->const_parsec;
+  const double kms = 1e5 / units_cgs_conversion_factor(us, UNIT_CONV_VELOCITY);
+  potential->M_200 *= phys_const->const_solar_mass;
+  potential->H *= phys_const->const_reduced_hubble;
+  potential->Mdisk *= phys_const->const_solar_mass;
+  potential->Rdisk *= kpc;
+  potential->Zdisk *= kpc;
+  potential->r_1 *= kpc;
+  potential->r_c *= kpc;
+  potential->amplitude *= phys_const->const_solar_mass / (kpc * kpc * kpc);
+  potential->df_sigma_floor *= kms;
+  potential->df_satellite_mass *= phys_const->const_solar_mass;
+  potential->df_core_radius *= kpc;
+
+  /* units conversion for polyfit coefficients */
+  for (int i = 0; i < potential_MW2014_num_coefficients; i++)
+    potential->df_polyfit_coeffs[potential_MW2014_num_coefficients - 1 - i] /=
+        integer_pow(kpc, i) * kms;
+
+  /* Compute rho_c */
+  const double rho_c = 3.0 * potential->H * potential->H /
+                       (8.0 * M_PI * phys_const->const_newton_G);
+
+  /* Compute R_200 */
+  const double R_200 =
+      cbrtf(3.0 * potential->M_200 / (4. * M_PI * 200.0 * rho_c));
+
+  /* NFW scale-radius */
+  potential->r_s = R_200 / potential->c_200;
+  const double r_s3 = potential->r_s * potential->r_s * potential->r_s;
+
+  /* Log(c_200) term appearing in many expressions */
+  potential->log_c200_term =
+      log(1. + potential->c_200) - potential->c_200 / (1. + potential->c_200);
+
+  potential->rho_0 =
+      potential->M_200 / (4.f * M_PI * r_s3 * potential->log_c200_term);
+
+  /* Pre-factor for the accelerations (note G is multiplied in later on) */
+  potential->pre_factor = 4.0f * M_PI * potential->rho_0 * r_s3;
+
+  /* Prefactor for the mass of the PSC profile */
+  potential->prefactor_psc_1 = 2.0 * M_PI * potential->amplitude *
+                               pow(potential->r_1, potential->alpha) *
+                               pow(potential->r_c, 3.0 - potential->alpha);
+
+  /* Gamma function value for the mass of the PSC profile */
+  potential->gamma_psc = gsl_sf_gamma(1.5 - 0.5 * potential->alpha);
+
+  /* Prefactor for the potential of the PSC profile */
+  potential->prefactor_psc_2 = 2.0 * M_PI * potential->amplitude *
+                               pow(potential->r_1, potential->alpha) *
+                               pow(potential->r_c, 2.0 - potential->alpha);
+
+  /* Compute the orbital time at the softening radius */
+  const double sqrtgm = sqrt(phys_const->const_newton_G * potential->M_200);
+  const double epslnthing = log(1.0 + potential->eps / potential->r_s) -
+                            potential->eps / (potential->eps + potential->r_s);
+
+  potential->mintime = 2. * M_PI * potential->eps * sqrtf(potential->eps) *
+                       sqrtf(potential->log_c200_term / epslnthing) / sqrtgm *
+                       potential->timestep_mult;
+#else
+  error("Code not compiled with GSL. Can't compute MWPotential2014.");
+#endif
+}
+
+/**
+ * @brief Prints the properties of the external potential to stdout.
+ *
+ * @param  potential The external potential properties.
+ */
+static INLINE void potential_print_backend(
+    const struct external_potential* potential) {
+
+  message(
+      "External potential is 'MWPotential2014' "
+      "with properties (in internal units) are "
+      "(x,y,z) = "
+      "(%e, %e, %e), c_200 = %e, M_200 = %e, H = %e, M_disk = %e, R_disk = %e, "
+      "z_disk = %e, amplitude = %e, r_1 = %e, alpha = %e, r_c = %e, timestep "
+      "multiplier = %e mintime = %e",
+      potential->x[0], potential->x[1], potential->x[2], potential->c_200,
+      potential->M_200, potential->H, potential->Mdisk, potential->Rdisk,
+      potential->Zdisk, potential->amplitude, potential->r_1, potential->alpha,
+      potential->r_c, potential->timestep_mult, potential->mintime);
+}
+
+#endif /* SWIFT_POTENTIAL_MWPotential2014_H */
diff --git a/src/potential/isothermal/potential.h b/src/potential/isothermal/potential.h
index 408561116bcc75c9d947cbb4433c1f28d1ad578d..0e6bc3b813697c79bafc2e3a97004cb1e25183a2 100644
--- a/src/potential/isothermal/potential.h
+++ b/src/potential/isothermal/potential.h
@@ -97,7 +97,7 @@ __attribute__((always_inline)) INLINE static float external_gravity_timestep(
   return potential->timestep_mult * sqrtf(a_2 / dota_2);
 }
 
-/**
+/**c
  * @brief Computes the gravitational acceleration from an isothermal potential.
  *
  * Note that the accelerations are multiplied by Newton's G constant
@@ -138,7 +138,7 @@ __attribute__((always_inline)) INLINE static void external_gravity_acceleration(
  * @brief Computes the gravitational potential energy of a particle in an
  * isothermal potential.
  *
- * phi = -0.5 * vrot^2 * ln(r^2 + epsilon^2)
+ * phi = 0.5 * vrot^2 * ln(r^2 + epsilon^2)
  *
  * @param time The current time (unused here).
  * @param potential The #external_potential used in the run.
@@ -154,7 +154,7 @@ external_gravity_get_potential_energy(
   const float dy = g->x[1] - potential->x[1];
   const float dz = g->x[2] - potential->x[2];
 
-  return potential->vrot * potential->vrot *
+  return 0.5f * potential->vrot * potential->vrot *
          logf(dx * dx + dy * dy + dz * dz + potential->epsilon2);
 }
 
diff --git a/src/power_spectrum.c b/src/power_spectrum.c
index 08189a25250179174668f5fe08cda4ea2764904d..f6693c7c5a767e1711ec950b048a57ec873c9b57 100644
--- a/src/power_spectrum.c
+++ b/src/power_spectrum.c
@@ -53,6 +53,37 @@
 
 #ifdef HAVE_FFTW
 
+/* The way to calculate these shifts is to consider a 3D cube of (kx,ky,kz)
+ * cells and check which cells fall inside a spherical shell with boundaries
+ * (i+0.5,i+1.5), then calculate the average k=sqrt(kx^2+ky^2+kz^2). So for i=0
+ * you'd find 6 cells k=1 and 12 cells k=sqrt(2), so the weighted k becomes
+ * (6 * 1 + 12 * sqrt(2)) / 18 = 1.2761424 – etc.
+ * Note that beyond the 7th term, the correction is < 1%. */
+#define number_of_corrected_bins 128
+static const float correction_shift_k_values[number_of_corrected_bins] = {
+    1.2761424f, 1.1154015f, 1.0447197f, 1.0151449f, 1.0195166f, 1.0203214f,
+    1.0102490f, 1.0031348f, 1.0063766f, 1.0093355f, 1.0055681f, 1.0024279f,
+    1.0034435f, 1.0038386f, 1.0011069f, 1.0002888f, 1.0018693f, 1.0029172f,
+    1.0019128f, 1.0009282f, 1.0015312f, 1.0016361f, 1.0009436f, 1.0003777f,
+    1.0005931f, 1.0010948f, 1.0010581f, 1.0009779f, 1.0010282f, 1.0008224f,
+    1.0006637f, 1.0004002f, 1.0002419f, 1.0005172f, 1.0005523f, 1.0004342f,
+    1.0005183f, 1.0005357f, 1.0003162f, 1.0001836f, 1.0003737f, 1.0004792f,
+    1.0004169f, 1.0003660f, 1.0004468f, 1.0004218f, 1.0001436f, 1.0000479f,
+    1.0002012f, 1.0003710f, 1.0003234f, 1.0002661f, 1.0003446f, 1.0003313f,
+    1.0001844f, 1.0000630f, 1.0001714f, 1.0002382f, 1.0001507f, 1.0001663f,
+    1.0002199f, 1.0002403f, 1.0000911f, 0.9999714f, 1.0001136f, 1.0001907f,
+    1.0001917f, 1.0001684f, 1.0001875f, 1.0002158f, 1.0000941f, 1.0000646f,
+    1.0000930f, 1.0001497f, 1.0001589f, 1.0001215f, 1.0001563f, 1.0001254f,
+    1.0000557f, 1.0000220f, 1.0000517f, 1.0001039f, 1.0001185f, 1.0000778f,
+    1.0000848f, 1.0001415f, 1.0001108f, 1.0000709f, 1.0000724f, 1.0001201f,
+    1.0001480f, 1.0001204f, 1.0001185f, 1.0000844f, 1.0000224f, 0.9999752f,
+    0.9999997f, 1.0000969f, 1.0001076f, 1.0000756f, 1.0000700f, 1.0000854f,
+    1.0001067f, 1.0000390f, 1.0000443f, 1.0000863f, 1.0000585f, 1.0000352f,
+    1.0000677f, 1.0001081f, 1.0000537f, 1.0000199f, 1.0000308f, 1.0000585f,
+    1.0000479f, 1.0000304f, 1.0000751f, 1.0000710f, 1.0000152f, 1.0000083f,
+    1.0000342f, 1.0000530f, 1.0000543f, 1.0000442f, 1.0000680f, 1.0000753f,
+    1.0000369f, 1.0000117f};
+
 /**
  * @brief Return the #power_type corresponding to a given string.
  */
@@ -773,8 +804,8 @@ void pow_from_grid_mapper(void* map_data, const int num, void* extra) {
                          (powgridft[index][0] * powgridft2[index][0] +
                           powgridft[index][1] * powgridft2[index][1]));
       } /* Loop over z */
-    }   /* Loop over y */
-  }     /* Loop over z */
+    } /* Loop over y */
+  } /* Loop over z */
 }
 
 /**
@@ -1154,7 +1185,9 @@ void power_spectrum(const enum power_type type1, const enum power_type type2,
       else
         sprintf(powunits, "Mpc^3 eV cm^(-3)");
 
-      fprintf(outputfile, "# Folding %d, all lengths/volumes are comoving\n",
+      fprintf(outputfile,
+              "# Folding %d, all lengths/volumes are comoving. k-bin centres "
+              "are not corrected for the weights of the modes.\n",
               i);
       fprintf(outputfile, "# Shotnoise [%s]\n", powunits);
       fprintf(outputfile, "%g\n", shot);
@@ -1208,8 +1241,17 @@ void power_spectrum(const enum power_type type1, const enum power_type type2,
     power_init_output_file(outputfile, type1, type2, us, phys_const);
 
     for (int j = 0; j < numtot; ++j) {
+
+      float k = kcomb[j];
+
+      /* Shall we correct the position of the k-space bin
+       * to account for the different weights of the modes entering the bin? */
+      if (pow_data->shift_centre_small_k_bins && j < number_of_corrected_bins) {
+        k *= correction_shift_k_values[j];
+      }
+
       fprintf(outputfile, "%15.8f %15.8e %15.8e %15.8e\n", s->e->cosmology->z,
-              kcomb[j], (pcomb[j] - shot), shot);
+              k, (pcomb[j] - shot), shot);
     }
     fclose(outputfile);
   }
@@ -1273,6 +1315,9 @@ void power_init(struct power_spectrum_data* p, struct swift_params* params,
         "WARNING: fold factor is recommended not to exceed 6 for a "
         "mass assignment order of 3 (TSC) or below.");
 
+  p->shift_centre_small_k_bins = parser_get_opt_param_int(
+      params, "PowerSpectrum:shift_centre_small_k_bins", 1);
+
   /* Make sensible choices for the k-cuts */
   const int kcutn = (p->windoworder >= 3) ? 90 : 70;
   const int kcutleft = (int)(p->Ngrid / 256.0 * kcutn);
diff --git a/src/power_spectrum.h b/src/power_spectrum.h
index 97b7feefde6225e590a05f0331b607838a5a24bb..7b2cb73020221b67ae71a5a746fc4d422fc7145d 100644
--- a/src/power_spectrum.h
+++ b/src/power_spectrum.h
@@ -71,6 +71,9 @@ struct power_spectrum_data {
   /*! The order of the mass assignment window */
   int windoworder;
 
+  /* Shall we correct the position of the k-space bin? */
+  int shift_centre_small_k_bins;
+
   /*! Array of component types to correlate on the "left" side */
   enum power_type* types1;
 
diff --git a/src/proxy.c b/src/proxy.c
index 608ee89ea69125667630464a7f03a550291e7659..77703e248d57ebabc132054598dc41bd2b75a710 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -50,6 +50,48 @@
 MPI_Datatype pcell_mpi_type;
 #endif
 
+struct tag_mapper_data {
+  int *tags_out, *tags_in;
+  int *offset_out, *offset_in;
+  struct cell *space_cells;
+};
+
+#ifdef WITH_MPI
+
+void proxy_tags_exchange_pack_mapper(void *map_data, int num_elements,
+                                     void *extra_data) {
+
+  struct cell *cells = (struct cell *)map_data;
+  struct tag_mapper_data *data = (struct tag_mapper_data *)extra_data;
+  int *restrict tags_out = data->tags_out;
+  const int *restrict offset_out = data->offset_out;
+  struct cell *space_cells = data->space_cells;
+  const size_t delta = cells - space_cells;
+
+  for (int k = 0; k < num_elements; k++) {
+    if (cells[k].mpi.sendto) {
+      cell_pack_tags(&cells[k], &tags_out[offset_out[k + delta]]);
+    }
+  }
+}
+
+void proxy_tags_exchange_unpack_mapper(void *map_data, int num_elements,
+                                       void *extra_data) {
+
+  int *restrict cids_in = (int *)map_data;
+  struct tag_mapper_data *data = (struct tag_mapper_data *)extra_data;
+  const int *restrict offset_in = data->offset_in;
+  int *restrict tags_in = data->tags_in;
+  struct cell *space_cells = data->space_cells;
+
+  for (int k = 0; k < num_elements; k++) {
+    const int cid = cids_in[k];
+    cell_unpack_tags(&tags_in[offset_in[cid]], &space_cells[cid]);
+  }
+}
+
+#endif
+
 /**
  * @brief Exchange tags between nodes.
  *
@@ -65,7 +107,7 @@ void proxy_tags_exchange(struct proxy *proxies, int num_proxies,
 
 #ifdef WITH_MPI
 
-  ticks tic2 = getticks();
+  /* ticks tic2 = getticks(); */
 
   /* Run through the cells and get the size of the tags that will be sent off.
    */
@@ -103,16 +145,21 @@ void proxy_tags_exchange(struct proxy *proxies, int num_proxies,
                      sizeof(int) * count_out) != 0)
     error("Failed to allocate tags buffers.");
 
+  struct tag_mapper_data extra_data;
+  extra_data.tags_out = tags_out;
+  extra_data.offset_out = offset_out;
+  extra_data.space_cells = s->cells_top;
+
   /* Pack the local tags. */
-  for (int k = 0; k < s->nr_cells; k++) {
-    if (s->cells_top[k].mpi.sendto) {
-      cell_pack_tags(&s->cells_top[k], &tags_out[offset_out[k]]);
-    }
-  }
+  threadpool_map(&s->e->threadpool, proxy_tags_exchange_pack_mapper,
+                 s->cells_top, s->nr_cells, sizeof(struct cell),
+                 threadpool_auto_chunk_size, &extra_data);
 
-  if (s->e->verbose)
-    message("Cell pack tags took %.3f %s.",
-            clocks_from_ticks(getticks() - tic2), clocks_getunit());
+  /* if (s->e->verbose) */
+  /*   message("Cell pack tags took %.3f %s.", */
+  /*           clocks_from_ticks(getticks() - tic2), clocks_getunit()); */
+
+  /* tic2 = getticks(); */
 
   /* Allocate the incoming and outgoing request handles. */
   int num_reqs_out = 0;
@@ -153,9 +200,160 @@ void proxy_tags_exchange(struct proxy *proxies, int num_proxies,
     }
   }
 
+  /* if (s->e->verbose) */
+  /*   message("Emitting Send/Recv for tags took %.3f %s.", */
+  /*           clocks_from_ticks(getticks() - tic2), clocks_getunit()); */
+
+  /* tic2 = getticks(); */
+
+  /* Wait for all the sends to have completed. */
+  if (MPI_Waitall(num_reqs_in, reqs_in, MPI_STATUSES_IGNORE) != MPI_SUCCESS)
+    error("MPI_Waitall on sends failed.");
+
+  /* if (s->e->verbose) */
+  /*   message("WaitAll on tags took %.3f %s.", */
+  /*           clocks_from_ticks(getticks() - tic2), clocks_getunit()); */
+
+  /* tic2 = getticks(); */
+
+  /* Unpack the tags we received */
+  extra_data.tags_in = tags_in;
+  extra_data.offset_in = offset_in;
+  extra_data.space_cells = s->cells_top;
+  threadpool_map(&s->e->threadpool, proxy_tags_exchange_unpack_mapper, cids_in,
+                 num_reqs_in, sizeof(int), threadpool_auto_chunk_size,
+                 &extra_data);
+
+  /* if (s->e->verbose) */
+  /*   message("Cell unpack tags took %.3f %s.", */
+  /*           clocks_from_ticks(getticks() - tic2), clocks_getunit()); */
+
+  /* Wait for all the sends to have completed. */
+  if (MPI_Waitall(num_reqs_out, reqs_out, MPI_STATUSES_IGNORE) != MPI_SUCCESS)
+    error("MPI_Waitall on sends failed.");
+
+  /* Clean up. */
+  swift_free("tags_in", tags_in);
+  swift_free("tags_out", tags_out);
+  swift_free("tags_offsets_in", offset_in);
+  swift_free("tags_offsets_out", offset_out);
+  free(reqs_in);
+  free(cids_in);
+
+#else
+  error("SWIFT was not compiled with MPI support.");
+#endif
+}
+
+/**
+ * @brief Exchange extra information about the grid construction between nodes.
+ *
+ * Note that this function assumes that the cell structures have already
+ * been exchanged, e.g. via #proxy_cells_exchange.
+ *
+ * @param proxies The list of #proxy that will send/recv tags
+ * @param num_proxies The number of proxies.
+ * @param s The space into which the tags will be unpacked.
+ */
+void proxy_grid_extra_exchange(struct proxy *proxies, int num_proxies,
+                               struct space *s) {
+#ifdef WITH_MPI
+
+  ticks tic2 = getticks();
+
+  /* Run through the cells and get the size of the info that will be sent off.
+   */
+  int count_out = 0;
+  int *offset_out =
+      (int *)swift_malloc("info_offsets_out", s->nr_cells * sizeof(int));
+  if (offset_out == NULL) error("Error allocating memory for info offsets");
+
+  for (int k = 0; k < s->nr_cells; k++) {
+    offset_out[k] = count_out;
+    if (s->cells_top[k].mpi.sendto) {
+      count_out += s->cells_top[k].mpi.pcell_size;
+    }
+  }
+
+  /* Run through the proxies and get the count of incoming info. */
+  int count_in = 0;
+  int *offset_in =
+      (int *)swift_malloc("info_offsets_in", s->nr_cells * sizeof(int));
+  if (offset_in == NULL) error("Error allocating memory for info offsets");
+
+  for (int k = 0; k < num_proxies; k++) {
+    for (int j = 0; j < proxies[k].nr_cells_in; j++) {
+      offset_in[proxies[k].cells_in[j] - s->cells_top] = count_in;
+      count_in += proxies[k].cells_in[j]->mpi.pcell_size;
+    }
+  }
+
+  /* Allocate the tags. */
+  enum grid_construction_level *extra_info_in = NULL;
+  enum grid_construction_level *extra_info_out = NULL;
+  if (swift_memalign("extra_info_in", (void **)&extra_info_in,
+                     SWIFT_CACHE_ALIGNMENT,
+                     sizeof(enum grid_construction_level) * count_in) != 0 ||
+      swift_memalign("extra_info_out", (void **)&extra_info_out,
+                     SWIFT_CACHE_ALIGNMENT,
+                     sizeof(enum grid_construction_level) * count_out) != 0)
+    error("Failed to allocate extra info buffers.");
+
+  /* Pack the local grid info. */
+  for (int k = 0; k < s->nr_cells; k++) {
+    if (s->cells_top[k].mpi.sendto) {
+      cell_pack_grid_extra(&s->cells_top[k], &extra_info_out[offset_out[k]]);
+    }
+  }
+
+  if (s->e->verbose)
+    message("Cell pack grid extra took %.3f %s.",
+            clocks_from_ticks(getticks() - tic2), clocks_getunit());
+
+  /* Allocate the incoming and outgoing request handles. */
+  int num_reqs_out = 0;
+  int num_reqs_in = 0;
+  for (int k = 0; k < num_proxies; k++) {
+    num_reqs_in += proxies[k].nr_cells_in;
+    num_reqs_out += proxies[k].nr_cells_out;
+  }
+  MPI_Request *reqs_in = NULL;
+  int *cids_in = NULL;
+  if ((reqs_in = (MPI_Request *)malloc(sizeof(MPI_Request) *
+                                       (num_reqs_in + num_reqs_out))) == NULL ||
+      (cids_in = (int *)malloc(sizeof(int) * (num_reqs_in + num_reqs_out))) ==
+          NULL)
+    error("Failed to allocate MPI_Request arrays.");
+  MPI_Request *reqs_out = &reqs_in[num_reqs_in];
+  int *cids_out = &cids_in[num_reqs_in];
+
+  /* Emit the sends and recvs. */
+  for (int send_rid = 0, recv_rid = 0, k = 0; k < num_proxies; k++) {
+    for (int j = 0; j < proxies[k].nr_cells_in; j++) {
+      const int cid = proxies[k].cells_in[j] - s->cells_top;
+      cids_in[recv_rid] = cid;
+      int err =
+          MPI_Irecv(&extra_info_in[offset_in[cid]],
+                    proxies[k].cells_in[j]->mpi.pcell_size, MPI_INT,
+                    proxies[k].nodeID, cid, MPI_COMM_WORLD, &reqs_in[recv_rid]);
+      if (err != MPI_SUCCESS) mpi_error(err, "Failed to irecv grid info.");
+      recv_rid += 1;
+    }
+    for (int j = 0; j < proxies[k].nr_cells_out; j++) {
+      const int cid = proxies[k].cells_out[j] - s->cells_top;
+      cids_out[send_rid] = cid;
+      int err = MPI_Isend(&extra_info_out[offset_out[cid]],
+                          proxies[k].cells_out[j]->mpi.pcell_size, MPI_INT,
+                          proxies[k].nodeID, cid, MPI_COMM_WORLD,
+                          &reqs_out[send_rid]);
+      if (err != MPI_SUCCESS) mpi_error(err, "Failed to isend grid info.");
+      send_rid += 1;
+    }
+  }
+
   tic2 = getticks();
 
-  /* Wait for each recv and unpack the tags into the local cells. */
+  /* Wait for each recv and unpack the grid info into the local cells. */
   for (int k = 0; k < num_reqs_in; k++) {
     int pid = MPI_UNDEFINED;
     MPI_Status status;
@@ -163,11 +361,12 @@ void proxy_tags_exchange(struct proxy *proxies, int num_proxies,
         pid == MPI_UNDEFINED)
       error("MPI_Waitany failed.");
     const int cid = cids_in[pid];
-    cell_unpack_tags(&tags_in[offset_in[cid]], &s->cells_top[cid]);
+    cell_unpack_grid_extra(&extra_info_in[offset_in[cid]], &s->cells_top[cid],
+                           NULL);
   }
 
   if (s->e->verbose)
-    message("Cell unpack tags took %.3f %s.",
+    message("Cell unpack grid extra took %.3f %s.",
             clocks_from_ticks(getticks() - tic2), clocks_getunit());
 
   /* Wait for all the sends to have completed. */
@@ -175,10 +374,10 @@ void proxy_tags_exchange(struct proxy *proxies, int num_proxies,
     error("MPI_Waitall on sends failed.");
 
   /* Clean up. */
-  swift_free("tags_in", tags_in);
-  swift_free("tags_out", tags_out);
-  swift_free("tags_offsets_in", offset_in);
-  swift_free("tags_offsets_out", offset_out);
+  swift_free("extra_info_in", extra_info_in);
+  swift_free("extra_info_out", extra_info_out);
+  swift_free("info_offsets_in", offset_in);
+  swift_free("info_offsets_out", offset_out);
   free(reqs_in);
   free(cids_in);
 
@@ -564,12 +763,24 @@ void proxy_parts_exchange_first(struct proxy *p) {
   p->buff_out[1] = p->nr_gparts_out;
   p->buff_out[2] = p->nr_sparts_out;
   p->buff_out[3] = p->nr_bparts_out;
-  if (MPI_Isend(p->buff_out, 4, MPI_INT, p->nodeID,
-                p->mynodeID * proxy_tag_shift + proxy_tag_count, MPI_COMM_WORLD,
-                &p->req_parts_count_out) != MPI_SUCCESS)
+  p->buff_out[4] = p->nr_sinks_out;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  message("Number of particles out [%i , %i, %i, %i, %i]", p->nr_parts_out,
+          p->nr_gparts_out, p->nr_sparts_out, p->nr_bparts_out,
+          p->nr_sinks_out);
+#endif /* SWIFT_DEBUG_CHECKS */
+
+  if (MPI_Isend(p->buff_out, PROXY_EXCHANGE_NUMBER_PARTICLE_TYPES, MPI_INT,
+                p->nodeID, p->mynodeID * proxy_tag_shift + proxy_tag_count,
+                MPI_COMM_WORLD, &p->req_parts_count_out) != MPI_SUCCESS)
     error("Failed to isend nr of parts.");
-  /* message( "isent particle counts [%i, %i] from node %i to node %i." ,
-  p->buff_out[0], p->buff_out[1], p->mynodeID , p->nodeID ); fflush(stdout); */
+#ifdef SWIFT_DEBUG_CHECKS
+  message("isent particle counts [%i, %i, %i, %i, %i] from node %i to node %i.",
+          p->buff_out[0], p->buff_out[1], p->buff_out[2], p->buff_out[3],
+          p->buff_out[4], p->mynodeID, p->nodeID);
+  fflush(stdout);
+#endif /* SWIFT_DEBUG_CHECKS */
 
   /* Send the particle buffers. */
   if (p->nr_parts_out > 0) {
@@ -580,20 +791,28 @@ void proxy_parts_exchange_first(struct proxy *p) {
                   p->mynodeID * proxy_tag_shift + proxy_tag_xparts,
                   MPI_COMM_WORLD, &p->req_xparts_out) != MPI_SUCCESS)
       error("Failed to isend part data.");
-    // message( "isent particle data (%i) to node %i." , p->nr_parts_out ,
-    // p->nodeID ); fflush(stdout);
-    /*for (int k = 0; k < p->nr_parts_out; k++)
+#ifdef SWIFT_DEBUG_CHECKS
+    message("isent particle data (%i) to node %i.", p->nr_parts_out, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_parts_out; k++)
       message("sending particle %lli, x=[%.3e %.3e %.3e], h=%.3e, to node %i.",
               p->parts_out[k].id, p->parts_out[k].x[0], p->parts_out[k].x[1],
-              p->parts_out[k].x[2], p->parts_out[k].h, p->nodeID);*/
+              p->parts_out[k].x[2], p->parts_out[k].h, p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
   }
   if (p->nr_gparts_out > 0) {
     if (MPI_Isend(p->gparts_out, p->nr_gparts_out, gpart_mpi_type, p->nodeID,
                   p->mynodeID * proxy_tag_shift + proxy_tag_gparts,
                   MPI_COMM_WORLD, &p->req_gparts_out) != MPI_SUCCESS)
       error("Failed to isend gpart data.");
-    // message( "isent gpart data (%i) to node %i." , p->nr_gparts_out ,
-    // p->nodeID ); fflush(stdout);
+#ifdef SWIFT_DEBUG_CHECKS
+    message("isent gpart data (%i) to node %i.", p->nr_gparts_out, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_parts_out; k++)
+      message("sending gpart %lli, x=[%.3e %.3e %.3e], to node %i.",
+              p->gparts_out[k].id_or_neg_offset, p->gparts_out[k].x[0],
+              p->gparts_out[k].x[1], p->gparts_out[k].x[2], p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
   }
 
   if (p->nr_sparts_out > 0) {
@@ -601,23 +820,56 @@ void proxy_parts_exchange_first(struct proxy *p) {
                   p->mynodeID * proxy_tag_shift + proxy_tag_sparts,
                   MPI_COMM_WORLD, &p->req_sparts_out) != MPI_SUCCESS)
       error("Failed to isend spart data.");
-    // message( "isent spart data (%i) to node %i." , p->nr_sparts_out ,
-    // p->nodeID ); fflush(stdout);
+#ifdef SWIFT_DEBUG_CHECKS
+    message("isent spart data (%i) to node %i.", p->nr_sparts_out, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_sparts_out; k++)
+      message("sending spart %lli, x=[%.3e %.3e %.3e], h=%.3e, to node %i.",
+              p->sparts_out[k].id, p->sparts_out[k].x[0], p->sparts_out[k].x[1],
+              p->sparts_out[k].x[2], p->sparts_out[k].h, p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
   }
   if (p->nr_bparts_out > 0) {
     if (MPI_Isend(p->bparts_out, p->nr_bparts_out, bpart_mpi_type, p->nodeID,
                   p->mynodeID * proxy_tag_shift + proxy_tag_bparts,
                   MPI_COMM_WORLD, &p->req_bparts_out) != MPI_SUCCESS)
       error("Failed to isend bpart data.");
-    // message( "isent bpart data (%i) to node %i." , p->nr_bparts_out ,
-    // p->nodeID ); fflush(stdout);
+#ifdef SWIFT_DEBUG_CHECKS
+    message("isent bpart data (%i) to node %i.", p->nr_bparts_out, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_bparts_out; k++)
+      message("sending bpart %lli, x=[%.3e %.3e %.3e], h=%.3e, to node %i.",
+              p->bparts_out[k].id, p->bparts_out[k].x[0], p->bparts_out[k].x[1],
+              p->bparts_out[k].x[2], p->bparts_out[k].h, p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
+  }
+  if (p->nr_sinks_out > 0) {
+    if (MPI_Isend(p->sinks_out, p->nr_sinks_out, sink_mpi_type, p->nodeID,
+                  p->mynodeID * proxy_tag_shift + proxy_tag_sinks,
+                  MPI_COMM_WORLD, &p->req_sinks_out) != MPI_SUCCESS)
+      error("Failed to isend sink data.");
+#ifdef SWIFT_DEBUG_CHECKS
+    message("isent sink data (%i) to node %i.", p->nr_sinks_out, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_sinks_out; k++)
+      message("sending sinks %lli, x=[%.3e %.3e %.3e], h=%.3e, to node %i.",
+              p->sinks_out[k].id, p->sinks_out[k].x[0], p->sinks_out[k].x[1],
+              p->sinks_out[k].x[2], p->sinks_out[k].h, p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
   }
 
   /* Receive the number of particles. */
-  if (MPI_Irecv(p->buff_in, 4, MPI_INT, p->nodeID,
-                p->nodeID * proxy_tag_shift + proxy_tag_count, MPI_COMM_WORLD,
-                &p->req_parts_count_in) != MPI_SUCCESS)
+  if (MPI_Irecv(p->buff_in, PROXY_EXCHANGE_NUMBER_PARTICLE_TYPES, MPI_INT,
+                p->nodeID, p->nodeID * proxy_tag_shift + proxy_tag_count,
+                MPI_COMM_WORLD, &p->req_parts_count_in) != MPI_SUCCESS)
     error("Failed to irecv nr of parts.");
+#ifdef SWIFT_DEBUG_CHECKS
+  message(
+      "irecv particle counts [%i, %i, %i, %i, %i] from node %i, I am node %i.",
+      p->buff_in[0], p->buff_in[1], p->buff_in[2], p->buff_in[3], p->buff_in[4],
+      p->nodeID, p->mynodeID);
+  fflush(stdout);
+#endif /* SWIFT_DEBUG_CHECKS */
 
 #else
   error("SWIFT was not compiled with MPI support.");
@@ -633,6 +885,7 @@ void proxy_parts_exchange_second(struct proxy *p) {
   p->nr_gparts_in = p->buff_in[1];
   p->nr_sparts_in = p->buff_in[2];
   p->nr_bparts_in = p->buff_in[3];
+  p->nr_sinks_in = p->buff_in[4];
 
   /* Is there enough space in the buffers? */
   if (p->nr_parts_in > p->size_parts_in) {
@@ -674,6 +927,15 @@ void proxy_parts_exchange_second(struct proxy *p) {
              "bparts_in", sizeof(struct bpart) * p->size_bparts_in)) == NULL)
       error("Failed to re-allocate bparts_in buffers.");
   }
+  if (p->nr_sinks_in > p->size_sinks_in) {
+    do {
+      p->size_sinks_in *= proxy_buffgrow;
+    } while (p->nr_sinks_in > p->size_sinks_in);
+    swift_free("sinks_in", p->sinks_in);
+    if ((p->sinks_in = (struct sink *)swift_malloc(
+             "sinks_in", sizeof(struct sink) * p->size_sinks_in)) == NULL)
+      error("Failed to re-allocate sinks_in buffers.");
+  }
 
   /* Receive the particle buffers. */
   if (p->nr_parts_in > 0) {
@@ -684,32 +946,73 @@ void proxy_parts_exchange_second(struct proxy *p) {
                   p->nodeID * proxy_tag_shift + proxy_tag_xparts,
                   MPI_COMM_WORLD, &p->req_xparts_in) != MPI_SUCCESS)
       error("Failed to irecv part data.");
-    // message( "irecv particle data (%i) from node %i." , p->nr_parts_in ,
-    // p->nodeID ); fflush(stdout);
+#ifdef SWIFT_DEBUG_CHECKS
+    message("irecv particle data (%i) from node %i.", p->nr_parts_in,
+            p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_parts_in; k++)
+      message("receiving parts %lli, x=[%.3e %.3e %.3e], h=%.3e, from node %i.",
+              p->parts_in[k].id, p->parts_in[k].x[0], p->parts_in[k].x[1],
+              p->parts_in[k].x[2], p->parts_in[k].h, p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
   }
   if (p->nr_gparts_in > 0) {
     if (MPI_Irecv(p->gparts_in, p->nr_gparts_in, gpart_mpi_type, p->nodeID,
                   p->nodeID * proxy_tag_shift + proxy_tag_gparts,
                   MPI_COMM_WORLD, &p->req_gparts_in) != MPI_SUCCESS)
       error("Failed to irecv gpart data.");
-    // message( "irecv gpart data (%i) from node %i." , p->nr_gparts_in ,
-    // p->nodeID ); fflush(stdout);
+#ifdef SWIFT_DEBUG_CHECKS
+    message("irecv gpart data (%i) from node %i.", p->nr_gparts_in, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_gparts_in; k++)
+      message("receiving gparts %lli, x=[%.3e %.3e %.3e], from node %i.",
+              p->gparts_in[k].id_or_neg_offset, p->gparts_in[k].x[0],
+              p->gparts_in[k].x[1], p->gparts_in[k].x[2], p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
   }
   if (p->nr_sparts_in > 0) {
     if (MPI_Irecv(p->sparts_in, p->nr_sparts_in, spart_mpi_type, p->nodeID,
                   p->nodeID * proxy_tag_shift + proxy_tag_sparts,
                   MPI_COMM_WORLD, &p->req_sparts_in) != MPI_SUCCESS)
       error("Failed to irecv spart data.");
-    // message( "irecv spart data (%i) from node %i." , p->nr_sparts_in ,
-    // p->nodeID ); fflush(stdout);
+#ifdef SWIFT_DEBUG_CHECKS
+    message("irecv spart data (%i) from node %i.", p->nr_sparts_in, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_sparts_in; k++)
+      message(
+          "receiving sparts %lli, x=[%.3e %.3e %.3e], h=%.3e, from node %i.",
+          p->sparts_in[k].id, p->sparts_in[k].x[0], p->sparts_in[k].x[1],
+          p->sparts_in[k].x[2], p->sparts_in[k].h, p->nodeID);
+#endif
   }
   if (p->nr_bparts_in > 0) {
     if (MPI_Irecv(p->bparts_in, p->nr_bparts_in, bpart_mpi_type, p->nodeID,
                   p->nodeID * proxy_tag_shift + proxy_tag_bparts,
                   MPI_COMM_WORLD, &p->req_bparts_in) != MPI_SUCCESS)
       error("Failed to irecv bpart data.");
-    // message( "irecv bpart data (%i) from node %i." , p->nr_bparts_in ,
-    // p->nodeID ); fflush(stdout);
+#ifdef SWIFT_DEBUG_CHECKS
+    message("irecv bpart data (%i) from node %i.", p->nr_bparts_in, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_bparts_in; k++)
+      message(
+          "receiving bparts %lli, x=[%.3e %.3e %.3e], h=%.3e, from node %i.",
+          p->bparts_in[k].id, p->bparts_in[k].x[0], p->bparts_in[k].x[1],
+          p->bparts_in[k].x[2], p->bparts_in[k].h, p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
+  }
+  if (p->nr_sinks_in > 0) {
+    if (MPI_Irecv(p->sinks_in, p->nr_sinks_in, sink_mpi_type, p->nodeID,
+                  p->nodeID * proxy_tag_shift + proxy_tag_sinks, MPI_COMM_WORLD,
+                  &p->req_sinks_in) != MPI_SUCCESS)
+      error("Failed to irecv sink data.");
+#ifdef SWIFT_DEBUG_CHECKS
+    message("irecv sink data (%i) from node %i.", p->nr_sinks_in, p->nodeID);
+    fflush(stdout);
+    for (int k = 0; k < p->nr_sinks_in; k++)
+      message("receiving sinks %lli, x=[%.3e %.3e %.3e], h=%.3e, from node %i.",
+              p->sinks_in[k].id, p->sinks_in[k].x[0], p->sinks_in[k].x[1],
+              p->sinks_in[k].x[2], p->sinks_in[k].h, p->nodeID);
+#endif /* SWIFT_DEBUG_CHECKS */
   }
 
 #else
@@ -846,6 +1149,36 @@ void proxy_bparts_load(struct proxy *p, const struct bpart *bparts, int N) {
   p->nr_bparts_out += N;
 }
 
+/**
+ * @brief Load sinks onto a proxy for exchange.
+ *
+ * @param p The #proxy.
+ * @param sinks Pointer to an array of #sink to send.
+ * @param N The number of sinks.
+ */
+void proxy_sinks_load(struct proxy *p, const struct sink *sinks, int N) {
+
+  /* Is there enough space in the buffer? */
+  if (p->nr_sinks_out + N > p->size_sinks_out) {
+    do {
+      p->size_sinks_out *= proxy_buffgrow;
+    } while (p->nr_sinks_out + N > p->size_sinks_out);
+    struct sink *tp;
+    if ((tp = (struct sink *)swift_malloc(
+             "sinks_out", sizeof(struct sink) * p->size_sinks_out)) == NULL)
+      error("Failed to re-allocate sinks_out buffers.");
+    memcpy(tp, p->sinks_out, sizeof(struct sink) * p->nr_sinks_out);
+    swift_free("sinks_out", p->sinks_out);
+    p->sinks_out = tp;
+  }
+
+  /* Copy the parts and xparts data to the buffer. */
+  memcpy(&p->sinks_out[p->nr_sinks_out], sinks, sizeof(struct sink) * N);
+
+  /* Increase the counters. */
+  p->nr_sinks_out += N;
+}
+
 /**
  * @brief Frees the memory allocated for the particle proxies and sets their
  * size back to the initial state.
@@ -921,6 +1254,21 @@ void proxy_free_particle_buffers(struct proxy *p) {
              "bparts_in", sizeof(struct bpart) * p->size_bparts_in)) == NULL)
       error("Failed to allocate bparts_in buffers.");
   }
+
+  if (p->size_sinks_out > proxy_buffinit) {
+    swift_free("sinks_out", p->sinks_out);
+    p->size_sinks_out = proxy_buffinit;
+    if ((p->sinks_out = (struct sink *)swift_malloc(
+             "sinks_out", sizeof(struct sink) * p->size_sinks_out)) == NULL)
+      error("Failed to allocate sinks_out buffers.");
+  }
+  if (p->size_sinks_in > proxy_buffinit) {
+    swift_free("sinks_in", p->sinks_in);
+    p->size_sinks_in = proxy_buffinit;
+    if ((p->sinks_in = (struct sink *)swift_malloc(
+             "sinks_in", sizeof(struct sink) * p->size_sinks_in)) == NULL)
+      error("Failed to allocate sinks_in buffers.");
+  }
 }
 
 /**
@@ -1025,6 +1373,22 @@ void proxy_init(struct proxy *p, int mynodeID, int nodeID) {
       error("Failed to allocate bparts_out buffers.");
   }
   p->nr_bparts_out = 0;
+
+  /* Allocate the sinks send and receive buffers, if needed. */
+  if (p->sinks_in == NULL) {
+    p->size_sinks_in = proxy_buffinit;
+    if ((p->sinks_in = (struct sink *)swift_malloc(
+             "sinks_in", sizeof(struct sink) * p->size_sinks_in)) == NULL)
+      error("Failed to allocate sinks_in buffers.");
+  }
+  p->nr_sinks_in = 0;
+  if (p->sinks_out == NULL) {
+    p->size_sinks_out = proxy_buffinit;
+    if ((p->sinks_out = (struct sink *)swift_malloc(
+             "sinks_out", sizeof(struct sink) * p->size_sinks_out)) == NULL)
+      error("Failed to allocate sinks_out buffers.");
+  }
+  p->nr_sinks_out = 0;
 }
 
 /**
@@ -1043,11 +1407,13 @@ void proxy_clean(struct proxy *p) {
   swift_free("gparts_out", p->gparts_out);
   swift_free("sparts_out", p->sparts_out);
   swift_free("bparts_out", p->bparts_out);
+  swift_free("sinks_out", p->sinks_out);
   swift_free("parts_in", p->parts_in);
   swift_free("xparts_in", p->xparts_in);
   swift_free("gparts_in", p->gparts_in);
   swift_free("sparts_in", p->sparts_in);
   swift_free("bparts_in", p->bparts_in);
+  swift_free("sinks_in", p->sinks_in);
 }
 
 /**
diff --git a/src/proxy.h b/src/proxy.h
index 3bb0431ddac66fa87dcab5980da5528a1699ca13..46a322e3a49ff53f796353e90aa84852f855acd0 100644
--- a/src/proxy.h
+++ b/src/proxy.h
@@ -28,15 +28,21 @@
 #define proxy_buffgrow 1.5
 #define proxy_buffinit 100
 
+/* Number of particle types to exchange with proxies in
+   proxy_parts_exchange_first(). We have parts, gparts, sparts, bparts and
+   sinks to exchange, hence 5 types. */
+#define PROXY_EXCHANGE_NUMBER_PARTICLE_TYPES 5
+
 /* Proxy tag arithmetic. */
-#define proxy_tag_shift 8
+#define proxy_tag_shift 9
 #define proxy_tag_count 0
 #define proxy_tag_parts 1
 #define proxy_tag_xparts 2
 #define proxy_tag_gparts 3
 #define proxy_tag_sparts 4
 #define proxy_tag_bparts 5
-#define proxy_tag_cells 6
+#define proxy_tag_sinks 6
+#define proxy_tag_cells 7
 
 /**
  * @brief The different reasons a cell can be in a proxy
@@ -71,6 +77,7 @@ struct proxy {
   struct gpart *gparts_in, *gparts_out;
   struct spart *sparts_in, *sparts_out;
   struct bpart *bparts_in, *bparts_out;
+  struct sink *sinks_in, *sinks_out;
   int size_parts_in, size_parts_out;
   int nr_parts_in, nr_parts_out;
   int size_gparts_in, size_gparts_out;
@@ -79,9 +86,12 @@ struct proxy {
   int nr_sparts_in, nr_sparts_out;
   int size_bparts_in, size_bparts_out;
   int nr_bparts_in, nr_bparts_out;
+  int size_sinks_in, size_sinks_out;
+  int nr_sinks_in, nr_sinks_out;
 
   /* Buffer to hold the incomming/outgoing particle counts. */
-  int buff_out[4], buff_in[4];
+  int buff_out[PROXY_EXCHANGE_NUMBER_PARTICLE_TYPES],
+      buff_in[PROXY_EXCHANGE_NUMBER_PARTICLE_TYPES];
 
 /* MPI request handles. */
 #ifdef WITH_MPI
@@ -91,6 +101,7 @@ struct proxy {
   MPI_Request req_gparts_out, req_gparts_in;
   MPI_Request req_sparts_out, req_sparts_in;
   MPI_Request req_bparts_out, req_bparts_in;
+  MPI_Request req_sinks_out, req_sinks_in;
   MPI_Request req_cells_count_out, req_cells_count_in;
   MPI_Request req_cells_out, req_cells_in;
 #endif
@@ -104,6 +115,7 @@ void proxy_parts_load(struct proxy *p, const struct part *parts,
 void proxy_gparts_load(struct proxy *p, const struct gpart *gparts, int N);
 void proxy_sparts_load(struct proxy *p, const struct spart *sparts, int N);
 void proxy_bparts_load(struct proxy *p, const struct bpart *bparts, int N);
+void proxy_sinks_load(struct proxy *p, const struct sink *sinks, int N);
 void proxy_free_particle_buffers(struct proxy *p);
 void proxy_parts_exchange_first(struct proxy *p);
 void proxy_parts_exchange_second(struct proxy *p);
@@ -113,6 +125,8 @@ void proxy_cells_exchange(struct proxy *proxies, int num_proxies,
                           struct space *s, int with_gravity);
 void proxy_tags_exchange(struct proxy *proxies, int num_proxies,
                          struct space *s);
+void proxy_grid_extra_exchange(struct proxy *proxies, int num_proxies,
+                               struct space *s);
 void proxy_create_mpi_type(void);
 void proxy_free_mpi_type(void);
 
diff --git a/src/random.h b/src/random.h
index 530233b68fb4b32e2961e6112c90f50c778f7e3b..bce43ccca2cf5f941ba1ce169cf774b7e713504e 100644
--- a/src/random.h
+++ b/src/random.h
@@ -62,6 +62,7 @@ enum random_number_type {
   random_number_BH_reposition = 59969537LL,
   random_number_BH_spin = 193877777LL,
   random_number_BH_kick = 303595777LL,
+  random_number_sink_swallow = 7337737777LL,
   random_number_snapshot_sampling = 6561001LL,
   random_number_stellar_winds = 5947309451LL,
   random_number_HII_regions = 8134165677LL,
@@ -300,7 +301,6 @@ INLINE static int random_poisson(const int64_t id, const double lambda,
  * opening_angle radians.
  *
  * @param id_bh The ID of the (BH) particle which is doing the jet feedback.
- * @param id_gas The ID of the gas particle being kicked by jet feedback.
  * @param ti_current The time (on the time-line) for which to generate the
  *                    random kick direction.
  * @param type The #random_number_type to generate.
@@ -308,10 +308,12 @@ INLINE static int random_poisson(const int64_t id, const double lambda,
  * @param a Reference direction that defines the cone.
  * @param rand_cone_direction Return value.
  */
-INLINE static void random_direction_in_cone(
-    const int64_t id_bh, const int64_t id_gas, const integertime_t ti_current,
-    const enum random_number_type type, const float opening_angle,
-    const float a[3], float rand_cone_direction[3]) {
+INLINE static void random_direction_in_cone(const int64_t id_bh,
+                                            const integertime_t ti_current,
+                                            const enum random_number_type type,
+                                            const float opening_angle,
+                                            const float a[3],
+                                            float rand_cone_direction[3]) {
 
   /* We want to draw a random unit vector from a cone around the unit
    * vector a = (a0, a1, a2). We do this in a frame x'y'z', where the z'
@@ -367,17 +369,15 @@ INLINE static void random_direction_in_cone(
   /* Draw a random cosine confined to the range [cos(opening_angle), 1] */
   const float rand_cos_theta =
       1.f - (1.f - cosf(opening_angle)) *
-                random_unit_interval(id_bh + id_gas, ti_current, type);
+                random_unit_interval(id_bh, ti_current, type);
 
   /* Get the corresponding sine */
   const float rand_sin_theta =
       sqrtf(max(0.f, (1.f - rand_cos_theta) * (1.f + rand_cos_theta)));
 
   /* Get a random equitorial angle from [0, 180] deg */
-  const float rand_phi =
-      ((float)(2. * M_PI)) *
-      random_unit_interval((id_bh + id_gas) * (id_bh + id_gas), ti_current,
-                           type);
+  const float rand_phi = ((float)(2. * M_PI)) *
+                         random_unit_interval(id_bh * id_bh, ti_current, type);
 
   float rand_cos_phi, rand_sin_phi;
   sincosf(rand_phi, &rand_sin_phi, &rand_cos_phi);
diff --git a/src/rays.h b/src/rays.h
index cf1179873e5702dbf28007557226d70dd8294bee..90e379208564089332d551b0c92439ae63653439 100644
--- a/src/rays.h
+++ b/src/rays.h
@@ -91,10 +91,10 @@ __attribute__((always_inline)) INLINE static void ray_extra_init(
  * between two points with angular coordinates (theta_1, phi_1) and (theta_2,
  * phi_2)
  *
- * @param theta_1 Polar angle of point 1; \theta \in [-\pi/2, \pi/2]
- * @param phi_1 Azimuthal angle of point 1; \phi \in [-\pi, \pi)
- * @param theta_2 Polar angle of point 2; \theta \in [-\pi/2, \pi/2]
- * @param phi_2 Azimuthal angle of point 2; \phi \in [-\pi, \pi)
+ * @param theta_1 Polar angle of point 1; \f$ \theta \in [-\pi/2, \pi/2] \f$
+ * @param phi_1 Azimuthal angle of point 1; \f$ \phi \in [-\pi, \pi) \f$
+ * @param theta_2 Polar angle of point 2; \f$ \theta \in [-\pi/2, \pi/2] \f$
+ * @param phi_2 Azimuthal angle of point 2; \f$ \phi \in [-\pi, \pi) \f$
  * @param r_sphere Radius of the sphere on which the arc length between the two
  * points is computed
  */
@@ -132,7 +132,7 @@ __attribute__((always_inline)) INLINE static float ray_arclength(
  * @param v Gas particle velocity
  */
 __attribute__((always_inline)) INLINE static void ray_minimise_arclength(
-    const float *dx, const float r, struct ray_data *ray,
+    const float dx[3], const float r, struct ray_data *ray,
     const ray_feedback_type ray_type, const long long gas_part_id,
     const double rand_theta_gen, const double rand_phi_gen, const float m,
     struct ray_data_extra *ray_ext, const float *v) {
@@ -201,8 +201,8 @@ __attribute__((always_inline)) INLINE static void ray_minimise_arclength(
  * @param cosmo The cosmological model
  * @param current_mass Current mass of the gas particle
  * @param v_star Velocity of the stellar particle
- * @param rand_theta_gen Random number to generate \theta_ray
- * @param rand_phi_gen Random number to generate \phi_ray
+ * @param rand_theta_gen Random number to generate \f$ \theta_{ray} \f$
+ * @param rand_phi_gen Random number to generate \f$  \phi_{ray} \f$
  * @param mass_true Unaffected mass of the true particle
  * @param mass_mirror Unaffected mass of the mirror particle
  */
diff --git a/src/restart.c b/src/restart.c
index ed9d346457c60bbec81e85d66e800018d16b0e7f..be23734f9b66d97fd0857d46729085289bd8b2bf 100644
--- a/src/restart.c
+++ b/src/restart.c
@@ -29,6 +29,7 @@
 #include "engine.h"
 #include "error.h"
 #include "restart.h"
+#include "swift_lustre_api.h"
 #include "version.h"
 
 #include <errno.h>
@@ -133,24 +134,54 @@ void restart_write(struct engine *e, const char *filename) {
   /* Save a backup the existing restart file, if requested. */
   if (e->restart_save) restart_save_previous(filename);
 
-  /* Use a single Lustre stripe with a rank-based OST offset? */
-  if (e->restart_lustre_OST_count != 0) {
-
-    /* Use a random offset to avoid placing things in the same OSTs. We do
-     * this to keep the use of OSTs balanced, much like using -1 for the
-     * stripe. */
-    int offset = rand() % e->restart_lustre_OST_count;
 #ifdef WITH_MPI
-    MPI_Bcast(&offset, 1, MPI_INT, 0, MPI_COMM_WORLD);
-#endif
-    char string[1200];
-    sprintf(string, "lfs setstripe -c 1 -i %d %s",
-            ((e->nodeID + offset) % e->restart_lustre_OST_count), filename);
-    const int result = system(string);
-    if (result != 0) {
-      message("lfs setstripe command returned error code %d", result);
+  /* Attempt to use lustre OSTs intelligently so we avoid issues with full
+   * OSTs, OSTs that are not writable and making sure we only write restart
+   * files using one stripe. Only relevant for MPI. */
+  if (e->restart_lustre_OST_checks != 0) {
+
+    /* Gather information about the current state of the OSTs. */
+    struct swift_ost_store ost_infos;
+
+    /* Select good OSTs sorted by free space. */
+    if (e->nodeID == 0) {
+      swift_ost_select(&ost_infos, filename, e->restart_lustre_OST_free,
+                       e->restart_lustre_OST_test, e->verbose);
+    }
+
+    /* Distribute the OST information. */
+    MPI_Bcast(&ost_infos, sizeof(struct swift_ost_store), MPI_BYTE, 0,
+              MPI_COMM_WORLD);
+
+    /* Need to make space for the OSTs and copy those locally. If the count is
+     * zero this is probably not a lustre mount. */
+    if (ost_infos.count > 0) {
+      if (e->nodeID != 0) swift_ost_store_alloc(&ost_infos, ost_infos.size);
+      MPI_Bcast(ost_infos.infos, sizeof(struct swift_ost_info) * ost_infos.size,
+                MPI_BYTE, 0, MPI_COMM_WORLD);
+
+      /* We now know how many OSTs are available, each rank should attempt to
+       * use a different one, but overtime we should try not to use the same
+       * ones. Culling will order things by free space so we should get some
+       * reordering of those if we do this process each time. */
+      int dummy = e->nodeID;
+      int offset = swift_ost_next(&ost_infos, &dummy, 1);
+
+      /* And create the file with a stripe of 1 on the OST. */
+      const int result = swift_create_striped_file(filename, offset, 1, &dummy);
+      if (result != 0) message("failed to set stripe of restart file");
+
+      /* Finished with this. */
+      swift_ost_store_free(&ost_infos);
+    } else if (e->nodeID == 0) {
+      swift_ost_store_free(&ost_infos);
+
+      /* Don't try this again until next launch. */
+      e->snapshot_lustre_OST_checks = 0;
+      message("Disabling further lustre OST checks");
     }
   }
+#endif /* Lustre OST checks. */
 
   FILE *stream = fopen(filename, "w");
   if (stream == NULL)
@@ -191,19 +222,21 @@ void restart_read(struct engine *e, const char *filename) {
   if (stream == NULL)
     error("Failed to open restart file: %s (%s)", filename, strerror(errno));
 
-  /* Get our version and signature back. These should match. */
-  char signature[strlen(SWIFT_RESTART_SIGNATURE) + 1];
-  int len = strlen(SWIFT_RESTART_SIGNATURE);
-  restart_read_blocks(signature, len, 1, stream, NULL, "SWIFT signature");
-  signature[len] = '\0';
-  if (strncmp(signature, SWIFT_RESTART_SIGNATURE, len) != 0)
+  /* Get our version and signature back. These should match.
+   * Use static int here to avoid compiler warnings about gnu-extensions
+   * of folding a variable length array to constant array. */
+  const int sig_len = strlen(SWIFT_RESTART_SIGNATURE);
+  char signature[sig_len + 1];
+  restart_read_blocks(signature, sig_len, 1, stream, NULL, "SWIFT signature");
+  signature[sig_len] = '\0';
+  if (strncmp(signature, SWIFT_RESTART_SIGNATURE, sig_len) != 0)
     error(
         "Do not recognise this as a SWIFT restart file, found '%s' "
         "expected '%s'",
         signature, SWIFT_RESTART_SIGNATURE);
 
   char version[FNAMELEN];
-  len = strlen(package_version());
+  int len = strlen(package_version());
   restart_read_blocks(version, len, 1, stream, NULL, "SWIFT version");
   version[len] = '\0';
 
diff --git a/src/rt.h b/src/rt.h
index 21482ce3618876c785e2a04926feab2dfb204151..43abd3d0b23d2eec305238ebd88215e3df9b8f8f 100644
--- a/src/rt.h
+++ b/src/rt.h
@@ -21,7 +21,8 @@
 
 /**
  * @file src/rt.h
- * @brief Branches between the different radiative transfer schemes.
+ * @brief Branches between the different radiative transfer schemes. Also
+ * contains some globally valid functions related to time bin data.
  */
 
 /* Config parameters. */
@@ -44,6 +45,19 @@
 #error "Invalid choice of radiation scheme"
 #endif
 
+/**
+ * @brief Initialise RT time step data. This struct should be hidden from users,
+ * so we do it for all schemes here.
+ *
+ * @param p The #part.
+ */
+__attribute__((always_inline)) INLINE static void rt_first_init_timestep_data(
+    struct part *restrict p) {
+
+  p->rt_time_data.min_ngb_time_bin = num_time_bins + 1;
+  p->rt_time_data.time_bin = 0;
+}
+
 /**
  * @brief Prepare the rt *time step* quantities for a *hydro force* calculation.
  *
diff --git a/src/rt/GEAR/rt.h b/src/rt/GEAR/rt.h
index 72a9e25b221c162063dbffbad0e8d84abe6ed37e..b27e92e738aa13a2b3d30ebf0e4616f8efe0b7fd 100644
--- a/src/rt/GEAR/rt.h
+++ b/src/rt/GEAR/rt.h
@@ -256,7 +256,7 @@ __attribute__((always_inline)) INLINE static void rt_split_part(struct part* p,
 __attribute__((always_inline)) INLINE static void rt_part_has_no_neighbours(
     struct part* p) {
   message("WARNING: found particle without neighbours");
-};
+}
 
 /**
  * @brief Exception handle a star part not having any neighbours in ghost task
@@ -272,7 +272,7 @@ __attribute__((always_inline)) INLINE static void rt_spart_has_no_neighbours(
     sp->rt_data.emission_this_step[g] = 0.f;
   }
   message("WARNING: found star without neighbours");
-};
+}
 
 /**
  * @brief Do checks/conversions on particles on startup.
@@ -402,8 +402,7 @@ __attribute__((always_inline)) INLINE static double rt_part_dt(
     const double time_base, const int with_cosmology,
     const struct cosmology* cosmo) {
   if (with_cosmology) {
-    error("GEAR RT with cosmology not implemented yet! :(");
-    return 0.f;
+    return cosmology_get_delta_time(cosmo, ti_beg, ti_end);
   } else {
     return (ti_end - ti_beg) * time_base;
   }
@@ -463,7 +462,7 @@ __attribute__((always_inline)) INLINE static void rt_end_gradient(
  * @param cosmo #cosmology data structure.
  */
 __attribute__((always_inline)) INLINE static void rt_finalise_transport(
-    struct part* restrict p, const double dt,
+    struct part* restrict p, struct rt_props* rtp, const double dt,
     const struct cosmology* restrict cosmo) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
@@ -481,14 +480,36 @@ __attribute__((always_inline)) INLINE static void rt_finalise_transport(
   struct rt_part_data* restrict rtd = &p->rt_data;
   const float Vinv = 1.f / p->geometry.volume;
 
+  /* Do not redshift if we have a constant spectrum (type == 0) */
+  const float redshift_factor =
+      (rtp->stellar_spectrum_type == 0) ? 0. : cosmo->H * dt;
+
   for (int g = 0; g < RT_NGROUPS; g++) {
     const float e_old = rtd->radiation[g].energy_density;
+
     /* Note: in this scheme, we're updating d/dt (U * V) + sum F * A * dt = 0.
      * So we'll need the division by the volume here. */
+
     rtd->radiation[g].energy_density += rtd->flux[g].energy * Vinv;
+    rtd->radiation[g].energy_density -=
+        rtd->radiation[g].energy_density *
+        redshift_factor;  // Energy lost due to redshift
+
     rtd->radiation[g].flux[0] += rtd->flux[g].flux[0] * Vinv;
+    rtd->radiation[g].flux[0] -=
+        rtd->radiation[g].flux[0] *
+        redshift_factor;  // Energy lost due to redshift
+
     rtd->radiation[g].flux[1] += rtd->flux[g].flux[1] * Vinv;
+    rtd->radiation[g].flux[1] -=
+        rtd->radiation[g].flux[1] *
+        redshift_factor;  // Energy lost due to redshift
+
     rtd->radiation[g].flux[2] += rtd->flux[g].flux[2] * Vinv;
+    rtd->radiation[g].flux[2] -=
+        rtd->radiation[g].flux[2] *
+        redshift_factor;  // Energy lost due to redshift
+
     rt_check_unphysical_state(&rtd->radiation[g].energy_density,
                               rtd->radiation[g].flux, e_old, /*callloc=*/4);
   }
@@ -559,6 +580,8 @@ __attribute__((always_inline)) INLINE static void rt_kick_extra(
   }
 #endif
 
+#ifdef GIZMO_MFV_SPH
+
   /* Note: We need to mimick here what Gizmo does for the mass fluxes.
    * The relevant time scale is the hydro time step for the mass fluxes,
    * not the RT times. We also need to prevent the kick to apply the mass
@@ -632,6 +655,8 @@ __attribute__((always_inline)) INLINE static void rt_kick_extra(
      * hydro_kick_extra calls */
   }
 
+#endif
+
   rt_check_unphysical_mass_fractions(p);
 }
 
diff --git a/src/rt/GEAR/rt_flux.h b/src/rt/GEAR/rt_flux.h
index f16ac13b0f28af05515eca2aeb68479e9edc58d9..642ba060629e47908691999e5bae04b4705350e0 100644
--- a/src/rt/GEAR/rt_flux.h
+++ b/src/rt/GEAR/rt_flux.h
@@ -102,11 +102,13 @@ __attribute__((always_inline)) INLINE static void rt_compute_flux(
  **/
 __attribute__((always_inline)) INLINE static void rt_part_reset_mass_fluxes(
     struct part* restrict p) {
+#ifdef GIZMO_MFV_SPH
   p->rt_data.mass_flux.HI = 0.f;
   p->rt_data.mass_flux.HII = 0.f;
   p->rt_data.mass_flux.HeI = 0.f;
   p->rt_data.mass_flux.HeII = 0.f;
   p->rt_data.mass_flux.HeIII = 0.f;
+#endif
 }
 
 #endif /* SWIFT_GEAR_RT_FLUX_H */
diff --git a/src/rt/GEAR/rt_getters.h b/src/rt/GEAR/rt_getters.h
index dc9e3e6b9cb4a5337e357aad05cb4da1976cb4bb..a11ee9cbbecc1d660160d9e2594caf8d9eea8b80 100644
--- a/src/rt/GEAR/rt_getters.h
+++ b/src/rt/GEAR/rt_getters.h
@@ -29,20 +29,35 @@
  */
 
 /**
- * @brief Get the radiation energy densities of a particle.
+ * @brief Get the comoving radiation energy densities of a particle.
  *
  * @param p Particle.
  * @param E (return) Pointer to the array in which the result needs to be stored
  */
 __attribute__((always_inline)) INLINE static void
-rt_part_get_radiation_energy_density(const struct part *restrict p,
-                                     float E[RT_NGROUPS]) {
+rt_part_get_comoving_radiation_energy_density(const struct part *restrict p,
+                                              float E[RT_NGROUPS]) {
 
   for (int g = 0; g < RT_NGROUPS; g++) {
     E[g] = p->rt_data.radiation[g].energy_density;
   }
 }
 
+/**
+ * @brief Get the physical radiation energy densities of a particle
+ *
+ * @param p Particle.
+ * @param E (return) Pointer to the array in which the result needs to be stored
+ */
+__attribute__((always_inline)) INLINE static void
+rt_part_get_physical_radiation_energy_density(const struct part *restrict p,
+                                              float E[RT_NGROUPS],
+                                              const struct cosmology *cosmo) {
+  for (int g = 0; g < RT_NGROUPS; g++) {
+    E[g] = cosmo->a3_inv * p->rt_data.radiation[g].energy_density;
+  }
+}
+
 /**
  * @brief Get a 4-element state vector U containing the radiation energy
  * density and fluxes for a specific photon group.
diff --git a/src/rt/GEAR/rt_grackle_utils.h b/src/rt/GEAR/rt_grackle_utils.h
index 599221737365832fc1f8df665fe67757006a93e3..d5b281eabf3095769526798dc3298a855717b5c3 100644
--- a/src/rt/GEAR/rt_grackle_utils.h
+++ b/src/rt/GEAR/rt_grackle_utils.h
@@ -35,6 +35,21 @@
  * @brief Utility and helper functions related to using grackle.
  */
 
+/**
+ * @brief Update grackle units during run
+ *
+ * @param grackle_units grackle units struct
+ * @param cosmo cosmology struct
+ *
+ * NOTE: In the current implementation, this function does nothing.
+ * However, there might be use-cases in the future (e.g. switching
+ * UV background on or off depending on redshift) that might be
+ * needed in the future, which can be implemented into this function.
+ */
+__attribute__((always_inline)) INLINE void update_grackle_units_cosmo(
+    code_units *grackle_units, const struct unit_system *us,
+    const struct cosmology *restrict cosmo) {}
+
 /**
  * @brief initialize grackle during rt_props_init
  *
@@ -50,7 +65,8 @@ __attribute__((always_inline)) INLINE static void rt_init_grackle(
     code_units *grackle_units, chemistry_data *grackle_chemistry_data,
     chemistry_data_storage *grackle_chemistry_rates,
     float hydrogen_mass_fraction, const int grackle_verb,
-    const int case_B_recombination, const struct unit_system *us) {
+    const int case_B_recombination, const struct unit_system *us,
+    const struct cosmology *restrict cosmo) {
 
   grackle_verbose = grackle_verb;
 
diff --git a/src/rt/GEAR/rt_gradients.h b/src/rt/GEAR/rt_gradients.h
index dc116c940240f68255dbb3523ddd20d1340498eb..6bef2520d79a1910896d373079a604ae5be39e2b 100644
--- a/src/rt/GEAR/rt_gradients.h
+++ b/src/rt/GEAR/rt_gradients.h
@@ -20,12 +20,7 @@
 #ifndef SWIFT_RT_GRADIENTS_GEAR_H
 #define SWIFT_RT_GRADIENTS_GEAR_H
 
-/* better safe than sorry */
-#ifndef GIZMO_MFV_SPH
-#error "Cannot compile GEAR-RT without gizmo-mfv hydro!"
-#endif
-
-#include "hydro.h" /* needed for hydro_part_geometry_well_behaved() */
+#include "fvpm_geometry.h"
 #include "rt_getters.h"
 /* #include "rt_slope_limiters_cell.h" [> skipped for now <] */
 #include "rt_slope_limiters_face.h"
@@ -106,7 +101,7 @@ __attribute__((always_inline)) INLINE static void rt_finalise_gradient_part(
   const float h_inv = 1.0f / h;
 
   float norm;
-  if (hydro_part_geometry_well_behaved(p)) {
+  if (fvpm_part_geometry_well_behaved(p)) {
     const float hinvdim = pow_dimension(h_inv);
     norm = hinvdim;
   } else {
@@ -143,8 +138,8 @@ __attribute__((always_inline)) INLINE static void rt_finalise_gradient_part(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void rt_gradients_collect(
-    float r2, const float dx[3], float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
   rt_debug_sequence_check(pi, 2, __func__);
@@ -183,7 +178,7 @@ __attribute__((always_inline)) INLINE static void rt_gradients_collect(
 
   /* Compute psi tilde */
   float psii_tilde[3];
-  if (hydro_part_geometry_well_behaved(pi)) {
+  if (fvpm_part_geometry_well_behaved(pi)) {
     psii_tilde[0] =
         wi * (Bi[0][0] * dx[0] + Bi[0][1] * dx[1] + Bi[0][2] * dx[2]);
     psii_tilde[1] =
@@ -198,7 +193,7 @@ __attribute__((always_inline)) INLINE static void rt_gradients_collect(
   }
 
   float psij_tilde[3];
-  if (hydro_part_geometry_well_behaved(pj)) {
+  if (fvpm_part_geometry_well_behaved(pj)) {
     psij_tilde[0] =
         wi * (Bj[0][0] * dx[0] + Bj[0][1] * dx[1] + Bj[0][2] * dx[2]);
     psij_tilde[1] =
@@ -282,8 +277,8 @@ __attribute__((always_inline)) INLINE static void rt_gradients_collect(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void rt_gradients_nonsym_collect(
-    float r2, const float dx[3], float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
   rt_debug_sequence_check(pi, 2, __func__);
@@ -311,7 +306,7 @@ __attribute__((always_inline)) INLINE static void rt_gradients_nonsym_collect(
 
   /* Compute psi tilde */
   float psii_tilde[3];
-  if (hydro_part_geometry_well_behaved(pi)) {
+  if (fvpm_part_geometry_well_behaved(pi)) {
     psii_tilde[0] =
         wi * (Bi[0][0] * dx[0] + Bi[0][1] * dx[1] + Bi[0][2] * dx[2]);
     psii_tilde[1] =
@@ -391,7 +386,7 @@ __attribute__((always_inline)) INLINE static float rt_gradients_extrapolate(
  */
 __attribute__((always_inline)) INLINE static void rt_gradients_predict(
     const struct part *restrict pi, const struct part *restrict pj, float Ui[4],
-    float Uj[4], int group, const float *dx, const float r,
+    float Uj[4], int group, const float dx[3], const float r,
     const float xij_i[3]) {
 
   rt_part_get_radiation_state_vector(pi, group, Ui);
diff --git a/src/rt/GEAR/rt_iact.h b/src/rt/GEAR/rt_iact.h
index 3ad71d3b66616369ee45851cca73b4cbb9fae0ed..b356dd4d3ba5fc8a90a5a78964ec6a72f3394416 100644
--- a/src/rt/GEAR/rt_iact.h
+++ b/src/rt/GEAR/rt_iact.h
@@ -19,6 +19,7 @@
 #ifndef SWIFT_RT_IACT_GEAR_H
 #define SWIFT_RT_IACT_GEAR_H
 
+#include "fvpm_geometry.h"
 #include "rt_debugging.h"
 #include "rt_flux.h"
 #include "rt_gradients.h"
@@ -44,7 +45,7 @@
  */
 
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
+runner_iact_nonsym_rt_injection_prep(const float r2, const float dx[3],
                                      const float hi, const float hj,
                                      struct spart *si, const struct part *pj,
                                      const struct cosmology *cosmo,
@@ -96,9 +97,9 @@ runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
  * @param rt_props Properties of the RT scheme.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
-    const float r2, float *dx, const float hi, const float hj,
-    struct spart *restrict si, struct part *restrict pj, float a, float H,
-    const struct rt_props *rt_props) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct spart *restrict si, struct part *restrict pj, const float a,
+    const float H, const struct rt_props *rt_props) {
 
   /* If the star doesn't have any neighbours, we
    * have nothing to do here. */
@@ -206,8 +207,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
  * @param mode 0 if non-symmetric interaction, 1 if symmetric
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_flux_common(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H, int mode) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H, int mode) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
   const char *func_name = (mode == 1) ? "sym flux iact" : "nonsym flux iact";
@@ -254,8 +256,8 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_flux_common(
   /* eqn. (7) */
   float Anorm2 = 0.0f;
   float A[3];
-  if (hydro_part_geometry_well_behaved(pi) &&
-      hydro_part_geometry_well_behaved(pj)) {
+  if (fvpm_part_geometry_well_behaved(pi) &&
+      fvpm_part_geometry_well_behaved(pj)) {
     /* in principle, we use Vi and Vj as weights for the left and right
      * contributions to the generalized surface vector.
      * However, if Vi and Vj are very different (because they have very
@@ -387,8 +389,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_flux_common(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
 
   runner_iact_rt_flux_common(r2, dx, hi, hj, pi, pj, a, H, 1);
 }
@@ -410,9 +413,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_transport(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_transport(const float r2, const float dx[3],
+                                const float hi, const float hj,
                                 struct part *restrict pi,
-                                struct part *restrict pj, float a, float H) {
+                                struct part *restrict pj, const float a,
+                                const float H) {
 
   runner_iact_rt_flux_common(r2, dx, hi, hj, pi, pj, a, H, 0);
 }
@@ -434,8 +439,9 @@ runner_iact_nonsym_rt_transport(float r2, const float *dx, float hi, float hj,
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
 
   rt_gradients_collect(r2, dx, hi, hj, pi, pj);
 }
@@ -458,9 +464,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_gradient(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_gradient(const float r2, const float dx[3],
+                               const float hi, const float hj,
                                struct part *restrict pi,
-                               struct part *restrict pj, float a, float H) {
+                               struct part *restrict pj, const float a,
+                               const float H) {
 
   rt_gradients_nonsym_collect(r2, dx, hi, hj, pi, pj);
 }
diff --git a/src/rt/GEAR/rt_io.h b/src/rt/GEAR/rt_io.h
index 49d1096b61d29549612e345655aa4483de81b4a5..5aefc3ec997740eccb7ed433832966b6ce2c76b2 100644
--- a/src/rt/GEAR/rt_io.h
+++ b/src/rt/GEAR/rt_io.h
@@ -161,13 +161,16 @@ INLINE static int rt_write_particles(const struct part* parts,
 
   int num_elements = 3;
 
-  list[0] = io_make_output_field_convert_part(
+  list[0] = io_make_physical_output_field_convert_part(
       "PhotonEnergies", FLOAT, RT_NGROUPS, UNIT_CONV_ENERGY, 0, parts,
-      /*xparts=*/NULL, rt_convert_radiation_energies,
+      /*xparts=*/NULL,
+      /*convertible to comoving=*/1, rt_convert_radiation_energies,
       "Photon Energies (all groups)");
-  list[1] = io_make_output_field_convert_part(
+
+  list[1] = io_make_physical_output_field_convert_part(
       "PhotonFluxes", FLOAT, 3 * RT_NGROUPS, UNIT_CONV_RADIATION_FLUX, 0, parts,
-      /*xparts=*/NULL, rt_convert_radiation_fluxes,
+      /*xparts=*/NULL,
+      /*convertible to comoving=*/1, rt_convert_radiation_fluxes,
       "Photon Fluxes (all groups; x, y, and z coordinates)");
   list[2] = io_make_output_field_convert_part(
       "IonMassFractions", FLOAT, 5, UNIT_CONV_NO_UNITS, 0, parts,
@@ -312,6 +315,8 @@ INLINE static void rt_write_flavour(hid_t h_grp, hid_t h_grp_columns,
   io_write_attribute_f(dset, "h-scale exponent", 0.f);
   io_write_attribute_f(dset, "a-scale exponent", 0.f);
   io_write_attribute_s(dset, "Expression for physical CGS units", buffer);
+  io_write_attribute_b(dset, "Value stored as physical", 1);
+  io_write_attribute_b(dset, "Property can be converted to comoving", 0);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -413,6 +418,8 @@ INLINE static void rt_write_flavour(hid_t h_grp, hid_t h_grp_columns,
   io_write_attribute_f(dset_cred, "a-scale exponent", 0.f);
   io_write_attribute_s(dset_cred, "Expression for physical CGS units",
                        buffer_cred);
+  io_write_attribute_b(dset_cred, "Value stored as physical", 1);
+  io_write_attribute_b(dset_cred, "Property can be converted to comoving", 0);
 
   /* Write the actual number this conversion factor corresponds to */
   /* TODO Mladen: check cosmology. reduced_speed_of_light is physical only for
diff --git a/src/rt/GEAR/rt_properties.h b/src/rt/GEAR/rt_properties.h
index 49b0c9c60d50067f7927faa22bcc6df5f549db2d..6f9e95201bc8e145e11afc4c9f4a437814b6f205 100644
--- a/src/rt/GEAR/rt_properties.h
+++ b/src/rt/GEAR/rt_properties.h
@@ -452,7 +452,7 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
       params, "GEARRT:case_B_recombination", /*default=*/1);
   rt_init_grackle(&rtp->grackle_units, &rtp->grackle_chemistry_data,
                   &rtp->grackle_chemistry_rates, rtp->hydrogen_mass_fraction,
-                  rtp->grackle_verbose, rtp->case_B_recombination, us);
+                  rtp->grackle_verbose, rtp->case_B_recombination, us, cosmo);
 
   /* Pre-compute interaction rates/cross sections */
   /* -------------------------------------------- */
@@ -465,6 +465,12 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
   /* --------- */
 }
 
+__attribute__((always_inline)) INLINE static void rt_props_update(
+    struct rt_props* rtp, const struct unit_system* us,
+    struct cosmology* cosmo) {
+  update_grackle_units_cosmo(&(rtp->grackle_units), us, cosmo);
+}
+
 /**
  * @brief Write an RT properties struct to the given FILE as a
  * stream of bytes.
@@ -491,10 +497,11 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump(
  * @param stream the file stream
  * @param phys_const The physical constants in the internal unit system.
  * @param us The internal unit system.
+ * @param cosmo the #cosmology
  */
 __attribute__((always_inline)) INLINE static void rt_struct_restore(
     struct rt_props* props, FILE* stream, const struct phys_const* phys_const,
-    const struct unit_system* us) {
+    const struct unit_system* us, const struct cosmology* restrict cosmo) {
 
   restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL,
                       "RT properties struct");
@@ -502,7 +509,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_restore(
   rt_init_grackle(&props->grackle_units, &props->grackle_chemistry_data,
                   &props->grackle_chemistry_rates,
                   props->hydrogen_mass_fraction, props->grackle_verbose,
-                  props->case_B_recombination, us);
+                  props->case_B_recombination, us, cosmo);
 
   props->energy_weighted_cross_sections = NULL;
   props->number_weighted_cross_sections = NULL;
diff --git a/src/rt/GEAR/rt_struct.h b/src/rt/GEAR/rt_struct.h
index 63cfebbb6a2e3337e317edea206d7dd183f7ac6e..caadfc0b304f755326158023bc6e0f3723a220be 100644
--- a/src/rt/GEAR/rt_struct.h
+++ b/src/rt/GEAR/rt_struct.h
@@ -73,6 +73,7 @@ struct rt_part_data {
     float number_density_electrons; /* number density of electrons */
   } tchem;
 
+#ifdef GIZMO_MFV_SPH
   /* Keep track of the actual mass fluxes of the gas species */
   struct {
     float HI;    /* mass fraction taken by HI */
@@ -81,6 +82,7 @@ struct rt_part_data {
     float HeII;  /* mass fraction taken by HeII */
     float HeIII; /* mass fraction taken by HeIII */
   } mass_flux;
+#endif
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
   /* debugging data to store during entire run */
diff --git a/src/rt/GEAR/rt_thermochemistry.h b/src/rt/GEAR/rt_thermochemistry.h
index 273a240e3d7ed0ec89f6e6928224ff5d25451409..dc2155caca16b31f99215583d49977a7c548c1d0 100644
--- a/src/rt/GEAR/rt_thermochemistry.h
+++ b/src/rt/GEAR/rt_thermochemistry.h
@@ -100,7 +100,7 @@ __attribute__((always_inline)) INLINE static void rt_tchem_first_init_part(
  * @param phys_const The physical constants in internal units.
  * @param us The internal system of units.
  * @param dt The time-step of this particle.
- * @depth recursion depth
+ * @param depth recursion depth
  */
 INLINE static void rt_do_thermochemistry(
     struct part* restrict p, struct xpart* restrict xp,
@@ -122,7 +122,6 @@ INLINE static void rt_do_thermochemistry(
   grackle_field_data particle_grackle_data;
 
   gr_float density = hydro_get_physical_density(p, cosmo);
-
   /* In rare cases, unphysical solutions can arise with negative densities
    * which won't be fixed in the hydro part until further down the dependency
    * graph. Also, we can have vacuum, in which case we have nothing to do here.
@@ -130,15 +129,20 @@ INLINE static void rt_do_thermochemistry(
   if (density <= 0.) return;
 
   const float u_minimal = hydro_props->minimal_internal_energy;
-  gr_float internal_energy =
-      max(hydro_get_physical_internal_energy(p, xp, cosmo), u_minimal);
+
+  /* Physical internal energy */
+  gr_float internal_energy_phys =
+      hydro_get_physical_internal_energy(p, xp, cosmo);
+  gr_float internal_energy = max(internal_energy_phys, u_minimal);
+
   const float u_old = internal_energy;
 
   gr_float species_densities[6];
   rt_tchem_get_species_densities(p, density, species_densities);
 
   float radiation_energy_density[RT_NGROUPS];
-  rt_part_get_radiation_energy_density(p, radiation_energy_density);
+  rt_part_get_physical_radiation_energy_density(p, radiation_energy_density,
+                                                cosmo);
 
   gr_float iact_rates[5];
   rt_get_interaction_rates_for_grackle(
@@ -152,9 +156,6 @@ INLINE static void rt_do_thermochemistry(
                                  iact_rates);
 
   /* solve chemistry */
-  /* Note: `grackle_rates` is a global variable defined by grackle itself.
-   * Using a manually allocd and initialized variable here fails with MPI
-   * for some reason. */
   if (local_solve_chemistry(
           &rt_props->grackle_chemistry_data, &rt_props->grackle_chemistry_rates,
           &rt_props->grackle_units, &particle_grackle_data, dt) == 0)
@@ -163,8 +164,9 @@ INLINE static void rt_do_thermochemistry(
   /* copy updated grackle data to particle */
   /* update particle internal energy. Grackle had access by reference
    * to internal_energy */
-  internal_energy = particle_grackle_data.internal_energy[0];
-  const float u_new = max(internal_energy, u_minimal);
+  internal_energy_phys = particle_grackle_data.internal_energy[0];
+
+  const float u_new = max(internal_energy_phys, u_minimal);
 
   /* Re-do thermochemistry? */
   if ((rt_props->max_tchem_recursion > depth) &&
@@ -180,7 +182,11 @@ INLINE static void rt_do_thermochemistry(
   }
 
   /* If we're good, update the particle data from grackle results */
-  hydro_set_internal_energy(p, u_new);
+#ifdef GIZMO_MFV_SPH
+  hydro_set_physical_internal_energy(p, xp, cosmo, u_new);
+#else
+  hydro_set_physical_internal_energy_TESTING_SPH_RT(p, cosmo, u_new);
+#endif
 
   /* Update mass fractions */
   const gr_float one_over_rho = 1. / density;
@@ -232,7 +238,6 @@ INLINE static void rt_do_thermochemistry(
                               p->rt_data.radiation[g].flux, E_old,
                               /*callloc=*/2);
   }
-
   /* Clean up after yourself. */
   rt_clean_grackle_fields(&particle_grackle_data);
 }
@@ -262,14 +267,18 @@ __attribute__((always_inline)) INLINE static float rt_tchem_get_tchem_time(
 
   gr_float density = hydro_get_physical_density(p, cosmo);
   const float u_minimal = hydro_props->minimal_internal_energy;
-  gr_float internal_energy =
-      max(hydro_get_physical_internal_energy(p, xp, cosmo), u_minimal);
+
+  /* Physical internal energy */
+  gr_float internal_energy_phys =
+      hydro_get_physical_internal_energy(p, xp, cosmo);
+  gr_float internal_energy = max(internal_energy_phys, u_minimal);
 
   gr_float species_densities[6];
   rt_tchem_get_species_densities(p, density, species_densities);
 
   float radiation_energy_density[RT_NGROUPS];
-  rt_part_get_radiation_energy_density(p, radiation_energy_density);
+  rt_part_get_physical_radiation_energy_density(p, radiation_energy_density,
+                                                cosmo);
 
   gr_float iact_rates[5];
   rt_get_interaction_rates_for_grackle(
@@ -282,15 +291,11 @@ __attribute__((always_inline)) INLINE static float rt_tchem_get_tchem_time(
                                  iact_rates);
 
   /* Compute 'cooling' time */
-  /* Note: grackle_rates is a global variable defined by grackle itself.
-   * Using a manually allocd and initialized variable here fails with MPI
-   * for some reason. */
   gr_float tchem_time;
   if (local_calculate_cooling_time(
           &rt_props->grackle_chemistry_data, &rt_props->grackle_chemistry_rates,
           &rt_props->grackle_units, &particle_grackle_data, &tchem_time) == 0)
     error("Error in calculate_cooling_time.");
-
   /* Clean up after yourself. */
   rt_clean_grackle_fields(&particle_grackle_data);
 
diff --git a/src/rt/GEAR/rt_thermochemistry_utils.h b/src/rt/GEAR/rt_thermochemistry_utils.h
index 580813471307f99884e2c9a03fbdf9d6bf177935..5417594052048119691d7b5e9fa8ccc8f8ed5084 100644
--- a/src/rt/GEAR/rt_thermochemistry_utils.h
+++ b/src/rt/GEAR/rt_thermochemistry_utils.h
@@ -195,6 +195,8 @@ rt_tchem_get_gas_temperature(const struct part* restrict p,
 __attribute__((always_inline)) INLINE static void
 rt_tchem_set_particle_quantities_for_test(struct part* restrict p) {
 
+#ifdef GIZMO_MFV_SPH
+
   /* Set the values that you actually want. Needs to be in internal units.*/
   /* 1 hydrogen_atom_mass / cm^3 / (1.98848e18 g/IMU * 3.0857e15cm/ILU^3) */
   /* float density = 2.471e+04; */
@@ -218,6 +220,12 @@ rt_tchem_set_particle_quantities_for_test(struct part* restrict p) {
   /* This assumes zero velocity */
   p->conserved.energy = p->conserved.mass * internal_energy;
   hydro_set_internal_energy(p, internal_energy);
+
+#else
+
+  error("This isn't implemented for SPH yet.");
+
+#endif
 }
 
 /**
diff --git a/src/rt/GEAR/rt_unphysical.h b/src/rt/GEAR/rt_unphysical.h
index 39bb0086d4b77697bb5dd7959e4c7d9f2652231e..ba44a9cc0764b9c1c72a71e8399ad549798ac2ea 100644
--- a/src/rt/GEAR/rt_unphysical.h
+++ b/src/rt/GEAR/rt_unphysical.h
@@ -72,11 +72,12 @@ __attribute__((always_inline)) INLINE static void rt_check_unphysical_state(
   }
 
   /* Check for too high fluxes */
-  const float flux2 = flux[0] * flux[0] + flux[1] * flux[1] + flux[2] * flux[2];
-  const float flux_norm = sqrtf(flux2);
-  const float flux_max = rt_params.reduced_speed_of_light * *energy_density;
+  const double flux2 =
+      flux[0] * flux[0] + flux[1] * flux[1] + flux[2] * flux[2];
+  const double flux_norm = sqrt(flux2);
+  const double flux_max = rt_params.reduced_speed_of_light * *energy_density;
   if (flux_norm > flux_max) {
-    const float correct = flux_max / flux_norm;
+    const double correct = flux_max / flux_norm;
     flux[0] *= correct;
     flux[1] *= correct;
     flux[2] *= correct;
@@ -184,7 +185,7 @@ rt_check_unphysical_mass_fractions(struct part* restrict p) {
    * inactive particles however remains zero until the particle is active
    * again. See issue #833. */
 
-  if (p->conserved.mass <= 0.f || p->rho <= 0.f) {
+  if (hydro_get_mass(p) <= 0.f || p->rho <= 0.f) {
     /* Deal with unphysical situations and vacuum. */
     p->rt_data.tchem.mass_fraction_HI = RT_GEAR_TINY_MASS_FRACTION;
     p->rt_data.tchem.mass_fraction_HII = RT_GEAR_TINY_MASS_FRACTION;
diff --git a/src/rt/SPHM1RT/rt.h b/src/rt/SPHM1RT/rt.h
index 64ad8eaa263d20c609352ec48ca293f2e3be3bf1..810dc0a759e0265c12e0c472f816fe7bd46c14d1 100644
--- a/src/rt/SPHM1RT/rt.h
+++ b/src/rt/SPHM1RT/rt.h
@@ -103,7 +103,7 @@ __attribute__((always_inline)) INLINE static void rt_reset_part_each_subcycle(
     rt_check_unphysical_state(&rpd->conserved[g].urad, rpd->conserved[g].frad,
                               urad_old, cred);
   }
-};
+}
 
 /**
  * @brief First initialisation of the RT hydro particle data.
@@ -196,7 +196,7 @@ __attribute__((always_inline)) INLINE static void rt_split_part(struct part* p,
 __attribute__((always_inline)) INLINE static void rt_part_has_no_neighbours(
     struct part* p) {
   message("WARNING: found particle without neighbours");
-};
+}
 
 /**
  * @brief Exception handle a star part not having any neighbours in ghost task
@@ -206,7 +206,7 @@ __attribute__((always_inline)) INLINE static void rt_part_has_no_neighbours(
 __attribute__((always_inline)) INLINE static void rt_spart_has_no_neighbours(
     struct spart* sp) {
   message("WARNING: found star without neighbours");
-};
+}
 
 /**
  * @brief Do checks/conversions on particles on startup.
@@ -451,7 +451,7 @@ __attribute__((always_inline)) INLINE static void rt_end_gradient(
  * @param cosmo #cosmology data structure.
  */
 __attribute__((always_inline)) INLINE static void rt_finalise_transport(
-    struct part* restrict p, const double dt,
+    struct part* restrict p, struct rt_props* rtp, const double dt,
     const struct cosmology* restrict cosmo) {
   struct rt_part_data* rpd = &p->rt_data;
 
diff --git a/src/rt/SPHM1RT/rt_iact.h b/src/rt/SPHM1RT/rt_iact.h
index 23242980cc7cf653c225f36108ac86606d3b7977..5b8ddd84999351111faebddde9d00d04a95d24e0 100644
--- a/src/rt/SPHM1RT/rt_iact.h
+++ b/src/rt/SPHM1RT/rt_iact.h
@@ -43,7 +43,7 @@
  */
 
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
+runner_iact_nonsym_rt_injection_prep(const float r2, const float dx[3],
                                      const float hi, const float hj,
                                      struct spart *si, const struct part *pj,
                                      const struct cosmology *cosmo,
@@ -100,9 +100,9 @@ runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
  * @param rt_props Properties of the RT scheme.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
-    const float r2, float *dx, const float hi, const float hj,
-    struct spart *restrict si, struct part *restrict pj, float a, float H,
-    const struct rt_props *rt_props) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct spart *restrict si, struct part *restrict pj, const float a,
+    const float H, const struct rt_props *rt_props) {
 
   /* If the star doesn't have any neighbours, we
    * have nothing to do here. */
@@ -189,10 +189,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
  *
  */
 __attribute__((always_inline)) INLINE static void
-radiation_gradient_loop_function(float r2, const float *dx, float hi, float hj,
+radiation_gradient_loop_function(const float r2, const float dx[3],
+                                 const float hi, const float hj,
                                  struct part *restrict pi,
-                                 struct part *restrict pj, float a, float H,
-                                 int mode) {
+                                 struct part *restrict pj, const float a,
+                                 const float H, int mode) {
 
   struct rt_part_data *rpi = &pi->rt_data;
   struct rt_part_data *rpj = &pj->rt_data;
@@ -311,8 +312,9 @@ radiation_gradient_loop_function(float r2, const float *dx, float hi, float hj,
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
   radiation_gradient_loop_function(r2, dx, hi, hj, pi, pj, a, H, 1);
 }
 
@@ -331,9 +333,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_gradient(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_gradient(const float r2, const float dx[3],
+                               const float hi, const float hj,
                                struct part *restrict pi,
-                               struct part *restrict pj, float a, float H) {
+                               struct part *restrict pj, const float a,
+                               const float H) {
   radiation_gradient_loop_function(r2, dx, hi, hj, pi, pj, a, H, 0);
 }
 
@@ -353,8 +357,9 @@ runner_iact_nonsym_rt_gradient(float r2, const float *dx, float hi, float hj,
  *
  */
 __attribute__((always_inline)) INLINE static void radiation_force_loop_function(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H, int mode) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H, int mode) {
 
   struct rt_part_data *rpi = &pi->rt_data;
   struct rt_part_data *rpj = &pj->rt_data;
@@ -736,8 +741,9 @@ __attribute__((always_inline)) INLINE static void radiation_force_loop_function(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
 
   radiation_force_loop_function(r2, dx, hi, hj, pi, pj, a, H, 1);
 }
@@ -757,9 +763,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_transport(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_transport(const float r2, const float dx[3],
+                                const float hi, const float hj,
                                 struct part *restrict pi,
-                                struct part *restrict pj, float a, float H) {
+                                struct part *restrict pj, const float a,
+                                const float H) {
 
   radiation_force_loop_function(r2, dx, hi, hj, pi, pj, a, H, 0);
 }
diff --git a/src/rt/SPHM1RT/rt_io.h b/src/rt/SPHM1RT/rt_io.h
index c1cf3c41f521cfd29b6e8bc70c8167251aea0f1d..e19d062cd82836210c9fe20e4ac3c310396edeb5 100644
--- a/src/rt/SPHM1RT/rt_io.h
+++ b/src/rt/SPHM1RT/rt_io.h
@@ -126,10 +126,8 @@ INLINE static void rt_convert_conserved_photon_fluxes(
 INLINE static int rt_write_particles(const struct part* parts,
                                      struct io_props* list) {
 
-  /* Note that in the output, we write radiation energy and flux
-   * then we convert these quantities from radiation energy per mass and flux
-   * per mass
-   * */
+  /* Note that in the output, we write radiation energy and flux then we convert
+   * these quantities from radiation energy per mass and flux per mass */
   int num_elements = 4;
 
   list[0] = io_make_output_field_convert_part(
@@ -233,6 +231,8 @@ INLINE static void rt_write_flavour(hid_t h_grp, hid_t h_grp_columns,
   io_write_attribute_f(dset, "h-scale exponent", 0.f);
   io_write_attribute_f(dset, "a-scale exponent", 0.f);
   io_write_attribute_s(dset, "Expression for physical CGS units", buffer);
+  io_write_attribute_b(dset, "Value stored as physical", 1);
+  io_write_attribute_b(dset, "Property can be converted to comoving", 0);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -334,6 +334,8 @@ INLINE static void rt_write_flavour(hid_t h_grp, hid_t h_grp_columns,
   io_write_attribute_f(dset_cred, "a-scale exponent", 0.f);
   io_write_attribute_s(dset_cred, "Expression for physical CGS units",
                        buffer_cred);
+  io_write_attribute_b(dset_cred, "Value stored as physical", 1);
+  io_write_attribute_b(dset_cred, "Property can be converted to comoving", 0);
 
   /* Write the actual number this conversion factor corresponds to */
   /* TODO Mladen: check cosmology. reduced_speed_of_light is physical only for
diff --git a/src/rt/SPHM1RT/rt_properties.h b/src/rt/SPHM1RT/rt_properties.h
index f2eb53d2030fb103296dd691fa3e207ac9fd2423..38dbde050a35a00a33eae3ef1f59637ca549ce49 100644
--- a/src/rt/SPHM1RT/rt_properties.h
+++ b/src/rt/SPHM1RT/rt_properties.h
@@ -402,6 +402,9 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
     message("Radiative transfer initialized");
   }
 }
+__attribute__((always_inline)) INLINE static void rt_props_update(
+    struct rt_props* rtp, const struct unit_system* us,
+    struct cosmology* cosmo) {}
 
 /**
  * @brief Write an RT properties struct to the given FILE as a
@@ -428,7 +431,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump(
  */
 __attribute__((always_inline)) INLINE static void rt_struct_restore(
     struct rt_props* props, FILE* stream, const struct phys_const* phys_const,
-    const struct unit_system* us) {
+    const struct unit_system* us, const struct cosmology* restrict cosmo) {
 
   restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL,
                       "RT properties struct");
diff --git a/src/rt/debug/rt.h b/src/rt/debug/rt.h
index 3742ec1129dc348dfe27160e28c06e3d84236ebf..ddac996526ae07ed4c04b9e9394f137c3a390962 100644
--- a/src/rt/debug/rt.h
+++ b/src/rt/debug/rt.h
@@ -172,7 +172,7 @@ __attribute__((always_inline)) INLINE static void rt_split_part(struct part* p,
 __attribute__((always_inline)) INLINE static void rt_part_has_no_neighbours(
     struct part* p) {
   message("WARNING: found particle without neighbours");
-};
+}
 
 /**
  * @brief Exception handle a star part not having any neighbours in ghost task
@@ -180,7 +180,7 @@ __attribute__((always_inline)) INLINE static void rt_part_has_no_neighbours(
  * @param sp The #spart.
  */
 __attribute__((always_inline)) INLINE static void rt_spart_has_no_neighbours(
-    struct spart* sp){};
+    struct spart* sp) {}
 
 /**
  * @brief Do checks/conversions on particles on startup.
@@ -300,9 +300,8 @@ __attribute__((always_inline)) INLINE static void rt_end_gradient(
  * @param cosmo #cosmology data structure.
  */
 __attribute__((always_inline)) INLINE static void rt_finalise_transport(
-    struct part* restrict p, const double dt,
+    struct part* restrict p, struct rt_props* rtp, const double dt,
     const struct cosmology* restrict cosmo) {
-
   rt_debug_sequence_check(p, 3, __func__);
 
   if (p->rt_data.debug_calls_iact_transport_interaction == 0)
diff --git a/src/rt/debug/rt_gradients.h b/src/rt/debug/rt_gradients.h
index 3d2dfc8b0cd238e100872df5d1827e928cd3034c..bf80fbfc378d0c92a0d02667995984dc2ee10d0e 100644
--- a/src/rt/debug/rt_gradients.h
+++ b/src/rt/debug/rt_gradients.h
@@ -37,8 +37,8 @@
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void rt_gradients_collect(
-    float r2, const float dx[3], float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj) {
 
   rt_debug_sequence_check(pi, 2, __func__);
   rt_debug_sequence_check(pj, 2, __func__);
@@ -58,8 +58,8 @@ __attribute__((always_inline)) INLINE static void rt_gradients_collect(
  * @param pj Particle j.
  */
 __attribute__((always_inline)) INLINE static void rt_gradients_nonsym_collect(
-    float r2, const float dx[3], float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj) {
 
   rt_debug_sequence_check(pi, 2, __func__);
   pi->rt_data.debug_calls_iact_gradient_interaction += 1;
diff --git a/src/rt/debug/rt_iact.h b/src/rt/debug/rt_iact.h
index 2eb590dd43281affb0c3aece562a7d5f71948376..29538711ef033baa8eae5e3f75fe2302a7df5549 100644
--- a/src/rt/debug/rt_iact.h
+++ b/src/rt/debug/rt_iact.h
@@ -43,7 +43,7 @@
  */
 
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
+runner_iact_nonsym_rt_injection_prep(const float r2, const float dx[3],
                                      const float hi, const float hj,
                                      struct spart *si, const struct part *pj,
                                      const struct cosmology *cosmo,
@@ -66,9 +66,9 @@ runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
  * @param rt_props Properties of the RT scheme.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
-    const float r2, float *dx, const float hi, const float hj,
-    struct spart *restrict si, struct part *restrict pj, float a, float H,
-    const struct rt_props *rt_props) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct spart *restrict si, struct part *restrict pj, const float a,
+    const float H, const struct rt_props *rt_props) {
 
   /* If the star doesn't have any neighbours, we
    * have nothing to do here. */
@@ -107,8 +107,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
  * @param mode 0 if non-symmetric interaction, 1 if symmetric
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_flux_common(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H, int mode) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H, int mode) {
 
   const char *func_name = (mode == 1) ? "sym flux iact" : "nonsym flux iact";
   rt_debug_sequence_check(pi, 3, func_name);
@@ -136,8 +137,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_flux_common(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
 
   runner_iact_rt_flux_common(r2, dx, hi, hj, pi, pj, a, H, 1);
 }
@@ -159,9 +161,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_transport(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_transport(const float r2, const float dx[3],
+                                const float hi, const float hj,
                                 struct part *restrict pi,
-                                struct part *restrict pj, float a, float H) {
+                                struct part *restrict pj, const float a,
+                                const float H) {
 
   runner_iact_rt_flux_common(r2, dx, hi, hj, pi, pj, a, H, 0);
 }
@@ -183,8 +187,9 @@ runner_iact_nonsym_rt_transport(float r2, const float *dx, float hi, float hj,
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
 
   rt_gradients_collect(r2, dx, hi, hj, pi, pj);
 }
@@ -207,9 +212,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_gradient(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_gradient(const float r2, const float dx[3],
+                               const float hi, const float hj,
                                struct part *restrict pi,
-                               struct part *restrict pj, float a, float H) {
+                               struct part *restrict pj, const float a,
+                               const float H) {
 
   rt_gradients_nonsym_collect(r2, dx, hi, hj, pi, pj);
 }
diff --git a/src/rt/debug/rt_properties.h b/src/rt/debug/rt_properties.h
index 9fba0a49b274eb4499c31a6072032d92a15f45c8..d4a31a2c1b3c91537b3538f120bc1373ce41a0a8 100644
--- a/src/rt/debug/rt_properties.h
+++ b/src/rt/debug/rt_properties.h
@@ -92,6 +92,10 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
       parser_get_param_int(params, "TimeIntegration:max_nr_rt_subcycles");
 }
 
+__attribute__((always_inline)) INLINE static void rt_props_update(
+    struct rt_props* rtp, const struct unit_system* us,
+    struct cosmology* cosmo) {}
+
 /**
  * @brief Write an RT properties struct to the given FILE as a
  * stream of bytes.
@@ -117,7 +121,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump(
  */
 __attribute__((always_inline)) INLINE static void rt_struct_restore(
     struct rt_props* props, FILE* stream, const struct phys_const* phys_const,
-    const struct unit_system* us) {
+    const struct unit_system* us, const struct cosmology* restrict cosmo) {
 
   restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL,
                       "RT properties struct");
diff --git a/src/rt/none/rt.h b/src/rt/none/rt.h
index 9edc956e1885080ac98f526b1f6f315c8267694a..d3c1f7c5d454655af2688a9c42230305817e0dba 100644
--- a/src/rt/none/rt.h
+++ b/src/rt/none/rt.h
@@ -136,7 +136,7 @@ __attribute__((always_inline)) INLINE static void rt_split_part(struct part* p,
  * @param p The #part.
  */
 __attribute__((always_inline)) INLINE static void rt_part_has_no_neighbours(
-    struct part* p){};
+    struct part* p) {}
 
 /**
  * @brief Exception handle a star part not having any neighbours in ghost task
@@ -144,7 +144,7 @@ __attribute__((always_inline)) INLINE static void rt_part_has_no_neighbours(
  * @param sp The #spart.
  */
 __attribute__((always_inline)) INLINE static void rt_spart_has_no_neighbours(
-    struct spart* sp){};
+    struct spart* sp) {}
 
 /**
  * @brief Do checks/conversions on particles on startup.
@@ -248,9 +248,8 @@ __attribute__((always_inline)) INLINE static void rt_end_gradient(
  * @param cosmo #cosmology data structure.
  */
 __attribute__((always_inline)) INLINE static void rt_finalise_transport(
-    struct part* restrict p, const double dt,
+    struct part* restrict p, struct rt_props* rtp, const double dt,
     const struct cosmology* restrict cosmo) {}
-
 /**
  * @brief Do the thermochemistry on a particle.
  *
diff --git a/src/rt/none/rt_iact.h b/src/rt/none/rt_iact.h
index f8a3d85447ae7b463e9f7382d797af1037bf2cb5..cee3bbc145c5f627bdfb7f7690dfc5e7127540dc 100644
--- a/src/rt/none/rt_iact.h
+++ b/src/rt/none/rt_iact.h
@@ -40,7 +40,7 @@
  */
 
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
+runner_iact_nonsym_rt_injection_prep(const float r2, const float dx[3],
                                      const float hi, const float hj,
                                      struct spart *si, const struct part *pj,
                                      const struct cosmology *cosmo,
@@ -60,9 +60,9 @@ runner_iact_nonsym_rt_injection_prep(const float r2, const float *dx,
  * @param rt_props Properties of the RT scheme.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
-    const float r2, float *dx, const float hi, const float hj,
-    struct spart *restrict si, struct part *restrict pj, float a, float H,
-    const struct rt_props *rt_props) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct spart *restrict si, struct part *restrict pj, const float a,
+    const float H, const struct rt_props *rt_props) {}
 
 /**
  * @brief Flux calculation between particle i and particle j
@@ -78,8 +78,9 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {}
 
 /**
  * @brief Flux calculation between particle i and particle j: non-symmetric
@@ -96,9 +97,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_transport(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_transport(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_transport(const float r2, const float dx[3],
+                                const float hi, const float hj,
                                 struct part *restrict pi,
-                                struct part *restrict pj, float a, float H) {}
+                                struct part *restrict pj, const float a,
+                                const float H) {}
 
 /**
  * @brief Calculate the gradient interaction between particle i and particle j
@@ -114,8 +117,9 @@ runner_iact_nonsym_rt_transport(float r2, const float *dx, float hi, float hj,
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {}
 
 /**
  * @brief Calculate the gradient interaction between particle i and particle j:
@@ -132,8 +136,10 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_gradient(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_rt_gradient(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_rt_gradient(const float r2, const float dx[3],
+                               const float hi, const float hj,
                                struct part *restrict pi,
-                               struct part *restrict pj, float a, float H) {}
+                               struct part *restrict pj, const float a,
+                               const float H) {}
 
 #endif /* SWIFT_RT_IACT_NONE_H */
diff --git a/src/rt/none/rt_properties.h b/src/rt/none/rt_properties.h
index 16fd5fa5bf64987b7203e5437fa2da1ed1844aee..07e3cef313c2176b1d81f4a089472bd0b7c07f84 100644
--- a/src/rt/none/rt_properties.h
+++ b/src/rt/none/rt_properties.h
@@ -59,6 +59,10 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
     const struct unit_system* us, struct swift_params* params,
     struct cosmology* cosmo) {}
 
+__attribute__((always_inline)) INLINE static void rt_props_update(
+    struct rt_props* rtp, const struct unit_system* us,
+    struct cosmology* cosmo) {}
+
 /**
  * @brief Write an RT properties struct to the given FILE as a
  * stream of bytes.
@@ -84,7 +88,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump(
  */
 __attribute__((always_inline)) INLINE static void rt_struct_restore(
     struct rt_props* props, FILE* stream, const struct phys_const* phys_const,
-    const struct unit_system* us) {
+    const struct unit_system* us, const struct cosmology* restrict cosmo) {
 
   restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL,
                       "RT properties struct");
diff --git a/src/runner.h b/src/runner.h
index 4ccd3cec4ac7e6ef1edffb0e81ec16da54d15145..33c51c23618caafba53fca76b0ecd0380f02d1ad 100644
--- a/src/runner.h
+++ b/src/runner.h
@@ -98,9 +98,11 @@ void runner_do_black_holes_density_ghost(struct runner *r, struct cell *c,
                                          int timer);
 void runner_do_black_holes_swallow_ghost(struct runner *r, struct cell *c,
                                          int timer);
+void runner_do_sinks_density_ghost(struct runner *r, struct cell *c, int timer);
 void runner_do_init_grav(struct runner *r, struct cell *c, int timer);
 void runner_do_hydro_sort(struct runner *r, struct cell *c, int flag,
-                          int cleanup, int rt_requests_sort, int clock);
+                          const int cleanup, const int lock,
+                          const int rt_requests_sort, const int clock);
 void runner_do_stars_sort(struct runner *r, struct cell *c, int flag,
                           int cleanup, int clock);
 void runner_do_all_hydro_sort(struct runner *r, struct cell *c);
@@ -124,9 +126,12 @@ void runner_do_grav_mesh(struct runner *r, struct cell *c, int timer);
 void runner_do_grav_external(struct runner *r, struct cell *c, int timer);
 void runner_do_grav_fft(struct runner *r, int timer);
 void runner_do_csds(struct runner *r, struct cell *c, int timer);
-void runner_do_fof_self(struct runner *r, struct cell *c, int timer);
-void runner_do_fof_pair(struct runner *r, struct cell *ci, struct cell *cj,
-                        int timer);
+void runner_do_fof_search_self(struct runner *r, struct cell *c, int timer);
+void runner_do_fof_search_pair(struct runner *r, struct cell *ci,
+                               struct cell *cj, int timer);
+void runner_do_fof_attach_self(struct runner *r, struct cell *c, int timer);
+void runner_do_fof_attach_pair(struct runner *r, struct cell *ci,
+                               struct cell *cj, int timer);
 void runner_do_rt_ghost1(struct runner *r, struct cell *c, int timer);
 void runner_do_rt_ghost2(struct runner *r, struct cell *c, int timer);
 void runner_do_rt_tchem(struct runner *r, struct cell *c, int timer);
@@ -139,6 +144,17 @@ void runner_do_bh_swallow_pair(struct runner *r, struct cell *ci,
 void runner_do_star_formation(struct runner *r, struct cell *c, int timer);
 void runner_do_star_formation_sink(struct runner *r, struct cell *c, int timer);
 void runner_do_sink_formation(struct runner *r, struct cell *c);
+void runner_do_prepare_part_sink_formation(struct runner *r, struct cell *c,
+                                           struct part *restrict p,
+                                           struct xpart *restrict xp);
+void runner_do_sinks_gas_swallow_self(struct runner *r, struct cell *c,
+                                      int timer);
+void runner_do_sinks_gas_swallow_pair(struct runner *r, struct cell *ci,
+                                      struct cell *cj, int timer);
+void runner_do_sinks_sink_swallow_self(struct runner *r, struct cell *c,
+                                       int timer);
+void runner_do_sinks_sink_swallow_pair(struct runner *r, struct cell *ci,
+                                       struct cell *cj, int timer);
 void runner_do_stars_resort(struct runner *r, struct cell *c, const int timer);
 
 void runner_do_recv_gpart(struct runner *r, struct cell *c, int timer);
diff --git a/src/runner_black_holes.c b/src/runner_black_holes.c
index aebef16591aa6feaff1d12c26f0733e4a7cad0d4..ca5dc32461cbec5ebc758e2b492a153783a04714 100644
--- a/src/runner_black_holes.c
+++ b/src/runner_black_holes.c
@@ -211,7 +211,7 @@ void runner_do_gas_swallow(struct runner *r, struct cell *c, int timer) {
               break;
             }
           } /* Loop over foreign BHs */
-        }   /* Is the cell local? */
+        } /* Is the cell local? */
 #endif
 
         /* If we have a local particle, we must have found the BH in one
@@ -221,8 +221,8 @@ void runner_do_gas_swallow(struct runner *r, struct cell *c, int timer) {
                 p->id, swallow_id);
         }
       } /* Part was flagged for swallowing */
-    }   /* Loop over the parts */
-  }     /* Cell is not split */
+    } /* Loop over the parts */
+  } /* Cell is not split */
 }
 
 /**
@@ -449,7 +449,7 @@ void runner_do_bh_swallow(struct runner *r, struct cell *c, int timer) {
               break;
             }
           } /* Loop over foreign BHs */
-        }   /* Is the cell local? */
+        } /* Is the cell local? */
 #endif
 
         /* If we have a local particle, we must have found the BH in one
@@ -460,8 +460,8 @@ void runner_do_bh_swallow(struct runner *r, struct cell *c, int timer) {
         }
 
       } /* Part was flagged for swallowing */
-    }   /* Loop over the parts */
-  }     /* Cell is not split */
+    } /* Loop over the parts */
+  } /* Cell is not split */
 }
 
 /**
diff --git a/src/runner_doiact_functions_black_holes.h b/src/runner_doiact_functions_black_holes.h
index 55e63e928b2a8f849061933700e2c3c17d4c5fcc..cbfa9c78edabd7d4014fcbd59a710410ac522284 100644
--- a/src/runner_doiact_functions_black_holes.h
+++ b/src/runner_doiact_functions_black_holes.h
@@ -116,8 +116,8 @@ void DOSELF1_BH(struct runner *r, struct cell *c, int timer) {
           }
         }
       } /* loop over the parts in ci. */
-    }   /* loop over the bparts in ci. */
-  }     /* Do we have gas particles in the cell? */
+    } /* loop over the bparts in ci. */
+  } /* Do we have gas particles in the cell? */
 
   /* When doing BH swallowing, we need a quick loop also over the BH
    * neighbours */
@@ -177,7 +177,7 @@ void DOSELF1_BH(struct runner *r, struct cell *c, int timer) {
         }
       }
     } /* loop over the bparts in ci. */
-  }   /* loop over the bparts in ci. */
+  } /* loop over the bparts in ci. */
 
 #endif /* (FUNCTION_TASK_LOOP == TASK_LOOP_SWALLOW) */
 
@@ -286,8 +286,8 @@ void DO_NONSYM_PAIR1_BH_NAIVE(struct runner *r, struct cell *restrict ci,
           }
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the bparts in ci. */
-  }     /* Do we have gas particles in the cell? */
+    } /* loop over the bparts in ci. */
+  } /* Do we have gas particles in the cell? */
 
   /* When doing BH swallowing, we need a quick loop also over the BH
    * neighbours */
@@ -347,7 +347,7 @@ void DO_NONSYM_PAIR1_BH_NAIVE(struct runner *r, struct cell *restrict ci,
         }
       }
     } /* loop over the bparts in cj. */
-  }   /* loop over the bparts in ci. */
+  } /* loop over the bparts in ci. */
 
 #endif /* (FUNCTION_TASK_LOOP == TASK_LOOP_SWALLOW) */
 }
@@ -469,7 +469,7 @@ void DOPAIR1_SUBSET_BH_NAIVE(struct runner *r, struct cell *restrict ci,
         }
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 }
 
 /**
@@ -557,7 +557,7 @@ void DOSELF1_SUBSET_BH(struct runner *r, struct cell *restrict ci,
         }
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 }
 
 /**
@@ -667,7 +667,7 @@ void DOSUB_SUBSET_BH(struct runner *r, struct cell *ci, struct bpart *bparts,
 
       /* Get the type of pair and flip ci/cj if needed. */
       double shift[3] = {0.0, 0.0, 0.0};
-      const int sid = space_getsid(s, &ci, &cj, shift);
+      const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
 
       struct cell_split_pair *csp = &cell_split_pairs[sid];
       for (int k = 0; k < csp->count; k++) {
@@ -813,7 +813,7 @@ void DOSUB_PAIR1_BH(struct runner *r, struct cell *ci, struct cell *cj,
 
   /* Get the type of pair and flip ci/cj if needed. */
   double shift[3];
-  const int sid = space_getsid(s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
 
   /* Recurse? */
   if (cell_can_recurse_in_pair_black_holes_task(ci, cj) &&
diff --git a/src/runner_doiact_functions_hydro.h b/src/runner_doiact_functions_hydro.h
index 189ac30181b5532c927f87bd1502516f0b59bc5a..102fb042fc2bb7e6c568f2e511610b55e5d84b34 100644
--- a/src/runner_doiact_functions_hydro.h
+++ b/src/runner_doiact_functions_hydro.h
@@ -34,9 +34,12 @@
  * @param r The #runner.
  * @param ci The first #cell.
  * @param cj The second #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
-                   struct cell *restrict cj) {
+void DOPAIR1_NAIVE(struct runner *r, const struct cell *restrict ci,
+                   const struct cell *restrict cj, const int limit_min_h,
+                   const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -51,15 +54,29 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
   /* Anything to do here? */
   if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
 
+  /* Cosmological terms */
+  const float a = cosmo->a;
+  const float H = cosmo->H;
+  GET_MU0();
+
   const int count_i = ci->hydro.count;
   const int count_j = cj->hydro.count;
   struct part *restrict parts_i = ci->hydro.parts;
   struct part *restrict parts_j = cj->hydro.parts;
 
-  /* Cosmological terms and physical constants */
-  const float a = cosmo->a;
-  const float H = cosmo->H;
-  GET_MU0();
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci->dmin != cj->dmin) error("Cells of different size!");
+#endif
+
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+#endif
 
   /* Get the relative distance between the pairs, wrapping. */
   double shift[3] = {0.0, 0.0, 0.0};
@@ -80,6 +97,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
     if (part_is_inhibited(pi, e)) continue;
 
     const int pi_active = PART_IS_ACTIVE(pi, e);
+    const char depth_i = pi->depth_h;
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - (cj->loc[0] + shift[0])),
@@ -95,9 +113,10 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
       /* Skip inhibited particles. */
       if (part_is_inhibited(pj, e)) continue;
 
+      const int pj_active = PART_IS_ACTIVE(pj, e);
+      const char depth_j = pj->depth_h;
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
-      const int pj_active = PART_IS_ACTIVE(pj, e);
 
       /* Compute the pairwise distance. */
       const float pjx[3] = {(float)(pj->x[0] - cj->loc[0]),
@@ -114,8 +133,17 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
         error("Particle pj not drifted to current time");
 #endif
 
+      const int doi = pi_active && (r2 < hig2) && (depth_i >= min_depth) &&
+                      (depth_i <= max_depth);
+      const int doj = pj_active && (r2 < hjg2) && (depth_j >= min_depth) &&
+                      (depth_j <= max_depth);
+
       /* Hit or miss? */
-      if (r2 < hig2 && pi_active) {
+      if (doi) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
 
         IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
         IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
@@ -123,8 +151,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
         runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -133,7 +160,11 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
                                      t_current, cosmo, with_cosmology);
 #endif
       }
-      if (r2 < hjg2 && pj_active) {
+      if (doj) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
 
         dx[0] = -dx[0];
         dx[1] = -dx[1];
@@ -145,8 +176,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
         runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -156,7 +186,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(TIMER_DOPAIR);
 }
@@ -169,9 +199,12 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
  * @param r The #runner.
  * @param ci The first #cell.
  * @param cj The second #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
-                   struct cell *restrict cj) {
+void DOPAIR2_NAIVE(struct runner *r, const struct cell *restrict ci,
+                   const struct cell *restrict cj, const int limit_min_h,
+                   const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -186,15 +219,25 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
   /* Anything to do here? */
   if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
 
+  /* Cosmological terms */
+  const float a = cosmo->a;
+  const float H = cosmo->H;
+  GET_MU0();
+
   const int count_i = ci->hydro.count;
   const int count_j = cj->hydro.count;
   struct part *restrict parts_i = ci->hydro.parts;
   struct part *restrict parts_j = cj->hydro.parts;
 
-  /* Cosmological terms and physical constants */
-  const float a = cosmo->a;
-  const float H = cosmo->H;
-  GET_MU0();
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+#endif
 
   /* Get the relative distance between the pairs, wrapping. */
   double shift[3] = {0.0, 0.0, 0.0};
@@ -215,6 +258,7 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
     if (part_is_inhibited(pi, e)) continue;
 
     const int pi_active = PART_IS_ACTIVE(pi, e);
+    const char depth_i = pi->depth_h;
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - (cj->loc[0] + shift[0])),
@@ -231,6 +275,7 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
       if (part_is_inhibited(pj, e)) continue;
 
       const int pj_active = PART_IS_ACTIVE(pj, e);
+      const char depth_j = pj->depth_h;
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
 
@@ -249,66 +294,60 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
         error("Particle pj not drifted to current time");
 #endif
 
+      const int doi = pi_active && (depth_i >= min_depth) &&
+                      (depth_i <= max_depth) && ((r2 < hig2) || (r2 < hjg2));
+      const int doj = pj_active && (depth_j >= min_depth) &&
+                      (depth_j <= max_depth) && ((r2 < hjg2) || (r2 < hig2));
+
       /* Hit or miss? */
-      if (r2 < hig2 || r2 < hjg2) {
+      if (doi) {
 
-        if (pi_active && pj_active) {
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
 
-          IACT(r2, dx, hi, hj, pi, pj, a, H);
-          IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
+        IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
+        IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-          runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-          runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
-                                t_current, cosmo, with_cosmology);
+        runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
+                                     t_current, cosmo, with_cosmology);
 #endif
-        } else if (pi_active) {
+      }
+      if (doj) {
 
-          IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
-          IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
-#if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-          runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                  e->sink_properties);
-#endif
-#if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-          runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
-                                       t_current, cosmo, with_cosmology);
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
 #endif
-        } else if (pj_active) {
 
-          dx[0] = -dx[0];
-          dx[1] = -dx[1];
-          dx[2] = -dx[2];
+        dx[0] = -dx[0];
+        dx[1] = -dx[1];
+        dx[2] = -dx[2];
 
-          IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
-          IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
+        IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
+        IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-          runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                  e->sink_properties);
+        runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-          runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_rt_timebin(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_diffusion(r2, dx, hj, hi, pj, pi, a, H, time_base,
-                                       t_current, cosmo, with_cosmology);
+        runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_rt_timebin(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_diffusion(r2, dx, hj, hi, pj, pi, a, H, time_base,
+                                     t_current, cosmo, with_cosmology);
 #endif
-        }
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(TIMER_DOPAIR);
 }
@@ -320,8 +359,11 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
  *
  * @param r The #runner.
  * @param c The #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
+void DOSELF1_NAIVE(struct runner *r, const struct cell *c,
+                   const int limit_min_h, const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -342,7 +384,17 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
   GET_MU0();
 
   const int count = c->hydro.count;
-  struct part *restrict parts = c->hydro.parts;
+  struct part *parts = c->hydro.parts;
+
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? c->depth : 0;
+  const char max_depth = limit_min_h ? c->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? c->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? c->h_max_allowed : FLT_MAX;
+#endif
 
   /* Loop over the parts in ci. */
   for (int pid = 0; pid < count; pid++) {
@@ -354,6 +406,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
     if (part_is_inhibited(pi, e)) continue;
 
     const int pi_active = PART_IS_ACTIVE(pi, e);
+    const char depth_i = pi->depth_h;
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - c->loc[0]),
@@ -372,6 +425,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
       const int pj_active = PART_IS_ACTIVE(pj, e);
+      const char depth_j = pj->depth_h;
 
       /* Compute the pairwise distance. */
       const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
@@ -380,8 +434,10 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
       float dx[3] = {pix[0] - pjx[0], pix[1] - pjx[1], pix[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-      const int doi = pi_active && (r2 < hig2);
-      const int doj = pj_active && (r2 < hjg2);
+      const int doi = pi_active && (r2 < hig2) && (depth_i >= min_depth) &&
+                      (depth_i <= max_depth);
+      const int doj = pj_active && (r2 < hjg2) && (depth_j >= min_depth) &&
+                      (depth_j <= max_depth);
 
 #if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
@@ -394,12 +450,18 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
       /* Hit or miss? */
       if (doi && doj) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT(r2, dx, hi, hj, pi, pj, a, H);
         IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
         runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -409,14 +471,17 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
 #endif
       } else if (doi) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
         IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
         runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -426,6 +491,10 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
 #endif
       } else if (doj) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
+
         dx[0] = -dx[0];
         dx[1] = -dx[1];
         dx[2] = -dx[2];
@@ -436,8 +505,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
         runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -447,7 +515,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(TIMER_DOSELF);
 }
@@ -459,8 +527,11 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
  *
  * @param r The #runner.
  * @param c The #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
+void DOSELF2_NAIVE(struct runner *r, const struct cell *c,
+                   const int limit_min_h, const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -481,7 +552,17 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
   GET_MU0();
 
   const int count = c->hydro.count;
-  struct part *restrict parts = c->hydro.parts;
+  struct part *parts = c->hydro.parts;
+
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? c->depth : 0;
+  const char max_depth = limit_min_h ? c->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? c->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? c->h_max_allowed : FLT_MAX;
+#endif
 
   /* Loop over the parts in ci. */
   for (int pid = 0; pid < count; pid++) {
@@ -493,6 +574,7 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
     if (part_is_inhibited(pi, e)) continue;
 
     const int pi_active = PART_IS_ACTIVE(pi, e);
+    const char depth_i = pi->depth_h;
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - c->loc[0]),
@@ -508,9 +590,10 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
       /* Skip inhibited particles. */
       if (part_is_inhibited(pj, e)) continue;
 
+      const int pj_active = PART_IS_ACTIVE(pj, e);
+      const char depth_j = pj->depth_h;
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
-      const int pj_active = PART_IS_ACTIVE(pj, e);
 
       /* Compute the pairwise distance. */
       const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
@@ -519,8 +602,10 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
       float dx[3] = {pix[0] - pjx[0], pix[1] - pjx[1], pix[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-      const int doi = pi_active && ((r2 < hig2) || (r2 < hjg2));
-      const int doj = pj_active && ((r2 < hig2) || (r2 < hjg2));
+      const int doi = pi_active && (depth_i >= min_depth) &&
+                      (depth_i <= max_depth) && ((r2 < hig2) || (r2 < hjg2));
+      const int doj = pj_active && (depth_j >= min_depth) &&
+                      (depth_j <= max_depth) && ((r2 < hig2) || (r2 < hjg2));
 
 #if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
@@ -533,12 +618,18 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
       /* Hit or miss? */
       if (doi && doj) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT(r2, dx, hi, hj, pi, pj, a, H);
         IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
         runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -548,14 +639,17 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
 #endif
       } else if (doi) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
         IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
         runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -565,6 +659,10 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
 #endif
       } else if (doj) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
+
         dx[0] = -dx[0];
         dx[1] = -dx[1];
         dx[2] = -dx[2];
@@ -575,8 +673,7 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
         runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -586,7 +683,7 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(TIMER_DOSELF);
 }
@@ -605,10 +702,10 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
  * @param cj The second #cell.
  * @param shift The shift vector to apply to the particles in ci.
  */
-void DOPAIR_SUBSET_NAIVE(struct runner *r, struct cell *restrict ci,
-                         struct part *restrict parts_i, int *restrict ind,
-                         int count, struct cell *restrict cj,
-                         const double *shift) {
+void DOPAIR_SUBSET_NAIVE(struct runner *r, const struct cell *restrict ci,
+                         struct part *restrict parts_i, const int *ind,
+                         const int count, const struct cell *restrict cj,
+                         const double shift[3]) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -677,8 +774,7 @@ void DOPAIR_SUBSET_NAIVE(struct runner *r, struct cell *restrict ci,
         runner_iact_nonsym_chemistry(r2, dx, hi, pj->h, pi, pj, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hi, pj->h, pi, pj, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hi, pj->h, pi, pj, a, H);
-        runner_iact_nonsym_sink(r2, dx, hi, pj->h, pi, pj, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hi, pj->h, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hi, pj->h, pi, pj, a, H);
@@ -688,7 +784,7 @@ void DOPAIR_SUBSET_NAIVE(struct runner *r, struct cell *restrict ci,
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(timer_dopair_subset_naive);
 }
@@ -707,10 +803,10 @@ void DOPAIR_SUBSET_NAIVE(struct runner *r, struct cell *restrict ci,
  * @param flipped Flag to check whether the cells have been flipped or not.
  * @param shift The shift vector to apply to the particles in ci.
  */
-void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
-                   struct part *restrict parts_i, int *restrict ind, int count,
-                   struct cell *restrict cj, const int sid, const int flipped,
-                   const double *shift) {
+void DOPAIR_SUBSET(struct runner *r, const struct cell *restrict ci,
+                   struct part *restrict parts_i, const int *ind,
+                   const int count, const struct cell *restrict cj,
+                   const int sid, const int flipped, const double shift[3]) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -786,8 +882,7 @@ void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
           runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -797,7 +892,7 @@ void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
 #endif
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the parts in ci. */
+    } /* loop over the parts in ci. */
   }
 
   /* Parts are on the right. */
@@ -852,8 +947,7 @@ void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
           runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -863,7 +957,7 @@ void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
 #endif
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the parts in ci. */
+    } /* loop over the parts in ci. */
   }
 
   TIMER_TOC(timer_dopair_subset);
@@ -881,9 +975,9 @@ void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
  * @param count The number of particles in @c ind.
  * @param cj The second #cell.
  */
-void DOPAIR_SUBSET_BRANCH(struct runner *r, struct cell *restrict ci,
-                          struct part *restrict parts_i, int *restrict ind,
-                          int count, struct cell *restrict cj) {
+void DOPAIR_SUBSET_BRANCH(struct runner *r, const struct cell *restrict ci,
+                          struct part *restrict parts_i, const int *ind,
+                          const int count, struct cell *restrict cj) {
 
   const struct engine *e = r->e;
 
@@ -910,15 +1004,18 @@ void DOPAIR_SUBSET_BRANCH(struct runner *r, struct cell *restrict ci,
   const int flipped = runner_flip[sid];
   sid = sortlistID[sid];
 
+  /* Let's first lock the cell */
+  lock_lock(&cj->hydro.extra_sort_lock);
+
   /* Is it sorted, if not we use the naive interactions. */
   const int is_sorted =
       (cj->hydro.sorted & (1 << sid)) &&
       (cj->hydro.dx_max_sort_old <= space_maxreldx * cj->dmin);
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
-  int force_naive = 1;
+  const int force_naive = 1;
 #else
-  int force_naive = 0;
+  const int force_naive = 0;
 #endif
 
   if (force_naive || !is_sorted) {
@@ -934,20 +1031,25 @@ void DOPAIR_SUBSET_BRANCH(struct runner *r, struct cell *restrict ci,
     DOPAIR_SUBSET(r, ci, parts_i, ind, count, cj, sid, flipped, shift);
 #endif
   }
+
+  /* Now we can unlock */
+  if (lock_unlock(&cj->hydro.extra_sort_lock) != 0)
+    error("Impossible to unlock cell!");
 }
 
 /**
- * @brief Compute the interactions between a cell pair, but only for the
- *      given indices in ci.
+ * @brief Compute the interactions between a cell, but only for the
+ *      given indices in c.
  *
  * @param r The #runner.
- * @param ci The first #cell.
+ * @param i The #cell.
  * @param parts The #part to interact.
  * @param ind The list of indices of particles in @c ci to interact with.
  * @param count The number of particles in @c ind.
  */
-void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
-                   struct part *restrict parts, int *restrict ind, int count) {
+void DOSELF_SUBSET(struct runner *r, const struct cell *c,
+                   struct part *restrict parts, const int *ind,
+                   const int count) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -964,16 +1066,16 @@ void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
   const float H = cosmo->H;
   GET_MU0();
 
-  const int count_i = ci->hydro.count;
-  struct part *restrict parts_j = ci->hydro.parts;
+  const int count_cell = c->hydro.count;
+  struct part *restrict parts_j = c->hydro.parts;
   /* Loop over the parts in ci. */
   for (int pid = 0; pid < count; pid++) {
 
     /* Get a hold of the ith part in ci. */
     struct part *pi = &parts[ind[pid]];
-    const float pix[3] = {(float)(pi->x[0] - ci->loc[0]),
-                          (float)(pi->x[1] - ci->loc[1]),
-                          (float)(pi->x[2] - ci->loc[2])};
+    const float pix[3] = {(float)(pi->x[0] - c->loc[0]),
+                          (float)(pi->x[1] - c->loc[1]),
+                          (float)(pi->x[2] - c->loc[2])};
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
 
@@ -982,7 +1084,7 @@ void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
 #endif
 
     /* Loop over the parts in cj. */
-    for (int pjd = 0; pjd < count_i; pjd++) {
+    for (int pjd = 0; pjd < count_cell; pjd++) {
 
       /* Get a pointer to the jth particle. */
       struct part *restrict pj = &parts_j[pjd];
@@ -996,9 +1098,9 @@ void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
       const float hj = pj->h;
 
       /* Compute the pairwise distance. */
-      const float pjx[3] = {(float)(pj->x[0] - ci->loc[0]),
-                            (float)(pj->x[1] - ci->loc[1]),
-                            (float)(pj->x[2] - ci->loc[2])};
+      const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
+                            (float)(pj->x[1] - c->loc[1]),
+                            (float)(pj->x[2] - c->loc[2])};
       float dx[3] = {pix[0] - pjx[0], pix[1] - pjx[1], pix[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
@@ -1019,8 +1121,7 @@ void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
         runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
         runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -1030,7 +1131,7 @@ void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(timer_doself_subset);
 }
@@ -1045,9 +1146,9 @@ void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
  * @param ind The list of indices of particles in @c ci to interact with.
  * @param count The number of particles in @c ind.
  */
-void DOSELF_SUBSET_BRANCH(struct runner *r, struct cell *restrict ci,
-                          struct part *restrict parts, int *restrict ind,
-                          int count) {
+void DOSELF_SUBSET_BRANCH(struct runner *r, const struct cell *ci,
+                          struct part *restrict parts, const int *ind,
+                          const int count) {
 
 #if defined(WITH_VECTORIZATION) && defined(GADGET2_SPH)
   runner_doself_subset_density_vec(r, ci, parts, ind, count);
@@ -1062,11 +1163,14 @@ void DOSELF_SUBSET_BRANCH(struct runner *r, struct cell *restrict ci,
  * @param r The #runner.
  * @param ci The first #cell.
  * @param cj The second #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  * @param sid The direction of the pair.
  * @param shift The shift vector to apply to the particles in ci.
  */
-void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
-             const double *shift) {
+void DOPAIR1(struct runner *r, const struct cell *restrict ci,
+             const struct cell *restrict cj, const int limit_min_h,
+             const int limit_max_h, const int sid, const double shift[3]) {
 
   const struct engine *restrict e = r->e;
   const struct cosmology *restrict cosmo = e->cosmology;
@@ -1078,7 +1182,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
   TIMER_TIC;
 
-  /* Get the cutoff shift. */
+  /* Get the cut_off shift. */
   double rshift = 0.0;
   for (int k = 0; k < 3; k++) rshift += shift[k] * runner_shift[sid][k];
 
@@ -1100,9 +1204,20 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
       2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
 #endif /* SWIFT_DEBUG_CHECKS */
 
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+#endif
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+
   /* Get some other useful values. */
-  const double hi_max = ci->hydro.h_max * kernel_gamma - rshift;
-  const double hj_max = cj->hydro.h_max * kernel_gamma;
+  const double hi_max =
+      min(h_max, ci->hydro.h_max_active) * kernel_gamma - rshift;
+  const double hj_max = min(h_max, cj->hydro.h_max_active) * kernel_gamma;
   const int count_i = ci->hydro.count;
   const int count_j = cj->hydro.count;
   struct part *restrict parts_i = ci->hydro.parts;
@@ -1118,17 +1233,28 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
   if (CELL_IS_ACTIVE(ci, e)) {
 
-    /* Loop over the parts in ci. */
+    /* Loop over the *active* parts in ci that are within range (on the axis)
+       of any particle in cj. */
     for (int pid = count_i - 1;
          pid >= 0 && sort_i[pid].d + hi_max + dx_max > dj_min; pid--) {
 
       /* Get a hold of the ith part in ci. */
       struct part *restrict pi = &parts_i[sort_i[pid].i];
+      const char depth_i = pi->depth_h;
       const float hi = pi->h;
 
       /* Skip inactive particles */
       if (!PART_IS_ACTIVE(pi, e)) continue;
 
+#ifdef SWIFT_DEBUG_CHECKS
+      if (hi > ci->hydro.h_max_active)
+        error("Particle has h larger than h_max_active");
+#endif
+
+      /* Skip particles not in the range of h we care about */
+      if (depth_i < min_depth) continue;
+      if (depth_i > max_depth) continue;
+
       /* Is there anything we need to interact with ? */
       const double di = sort_i[pid].d + hi * kernel_gamma + dx_max - rshift;
       if (di < dj_min) continue;
@@ -1154,7 +1280,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         const float pjz = pj->x[2] - cj->loc[2];
 
         /* Compute the pairwise distance. */
-        float dx[3] = {pix - pjx, piy - pjy, piz - pjz};
+        const float dx[3] = {pix - pjx, piy - pjy, piz - pjz};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -1196,14 +1322,18 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         /* Hit or miss? */
         if (r2 < hig2) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
           IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
           runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -1213,22 +1343,33 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 #endif
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the parts in ci. */
-  }     /* Cell ci is active */
+    } /* loop over the parts in ci. */
+  } /* Cell ci is active */
 
   if (CELL_IS_ACTIVE(cj, e)) {
 
-    /* Loop over the parts in cj. */
+    /* Loop over the *active* parts in cj that are within range (on the axis)
+       of any particle in ci. */
     for (int pjd = 0; pjd < count_j && sort_j[pjd].d - hj_max - dx_max < di_max;
          pjd++) {
 
       /* Get a hold of the jth part in cj. */
       struct part *pj = &parts_j[sort_j[pjd].i];
+      const char depth_j = pj->depth_h;
       const float hj = pj->h;
 
       /* Skip inactive particles */
       if (!PART_IS_ACTIVE(pj, e)) continue;
 
+#ifdef SWIFT_DEBUG_CHECKS
+      if (hj > cj->hydro.h_max_active)
+        error("Particle has h larger than h_max_active");
+#endif
+
+      /* Skip particles not in the range of h we care about */
+      if (depth_j < min_depth) continue;
+      if (depth_j > max_depth) continue;
+
       /* Is there anything we need to interact with ? */
       const double dj = sort_j[pjd].d - hj * kernel_gamma - dx_max + rshift;
       if (dj - rshift > di_max) continue;
@@ -1254,7 +1395,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         const float piz = pi->x[2] - (cj->loc[2] + shift[2]);
 
         /* Compute the pairwise distance. */
-        float dx[3] = {pjx - pix, pjy - piy, pjz - piz};
+        const float dx[3] = {pjx - pix, pjy - piy, pjz - piz};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -1296,14 +1437,18 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         /* Hit or miss? */
         if (r2 < hjg2) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
           IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
           runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -1313,8 +1458,8 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 #endif
         }
       } /* loop over the parts in ci. */
-    }   /* loop over the parts in cj. */
-  }     /* Cell cj is active */
+    } /* loop over the parts in cj. */
+  } /* Cell cj is active */
 
   TIMER_TOC(TIMER_DOPAIR);
 }
@@ -1326,11 +1471,13 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
  * @param r #runner
  * @param ci #cell ci
  * @param cj #cell cj
- *
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
+void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj,
+                    const int limit_min_h, const int limit_max_h) {
 
-  const struct engine *restrict e = r->e;
+  const struct engine *e = r->e;
 
   /* Anything to do here? */
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
@@ -1342,73 +1489,30 @@ void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
   if (!CELL_ARE_PART_DRIFTED(ci, e) || !CELL_ARE_PART_DRIFTED(cj, e))
     error("Interacting undrifted cells.");
 
-  /* Get the sort ID. */
+  /* Get the sort ID.
+   * Note: this may swap the ci and cj pointers!! */
   double shift[3] = {0.0, 0.0, 0.0};
-  const int sid = space_getsid(e->s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
 
   /* Have the cells been sorted? */
   if (!(ci->hydro.sorted & (1 << sid)) ||
       ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin)
-    error("Interacting unsorted cells.");
+    error("Interacting unsorted cells (ci).");
+
   if (!(cj->hydro.sorted & (1 << sid)) ||
       cj->hydro.dx_max_sort_old > space_maxreldx * cj->dmin)
-    error("Interacting unsorted cells.");
-
-#ifdef SWIFT_DEBUG_CHECKS
-  /* Pick-out the sorted lists. */
-  const struct sort_entry *restrict sort_i = cell_get_hydro_sorts(ci, sid);
-  const struct sort_entry *restrict sort_j = cell_get_hydro_sorts(cj, sid);
-
-  /* Check that the dx_max_sort values in the cell are indeed an upper
-     bound on particle movement. */
-  for (int pid = 0; pid < ci->hydro.count; pid++) {
-    const struct part *p = &ci->hydro.parts[sort_i[pid].i];
-    if (part_is_inhibited(p, e)) continue;
-
-    const float d = p->x[0] * runner_shift[sid][0] +
-                    p->x[1] * runner_shift[sid][1] +
-                    p->x[2] * runner_shift[sid][2];
-    if (fabsf(d - sort_i[pid].d) - ci->hydro.dx_max_sort >
-            1.0e-4 * max(fabsf(d), ci->hydro.dx_max_sort_old) &&
-        fabsf(d - sort_i[pid].d) - ci->hydro.dx_max_sort >
-            ci->width[0] * 1.0e-10)
-      error(
-          "particle shift diff exceeds dx_max_sort in cell ci. ci->nodeID=%d "
-          "cj->nodeID=%d d=%e sort_i[pid].d=%e ci->hydro.dx_max_sort=%e "
-          "ci->hydro.dx_max_sort_old=%e",
-          ci->nodeID, cj->nodeID, d, sort_i[pid].d, ci->hydro.dx_max_sort,
-          ci->hydro.dx_max_sort_old);
-  }
-  for (int pjd = 0; pjd < cj->hydro.count; pjd++) {
-    const struct part *p = &cj->hydro.parts[sort_j[pjd].i];
-    if (part_is_inhibited(p, e)) continue;
-
-    const float d = p->x[0] * runner_shift[sid][0] +
-                    p->x[1] * runner_shift[sid][1] +
-                    p->x[2] * runner_shift[sid][2];
-    if ((fabsf(d - sort_j[pjd].d) - cj->hydro.dx_max_sort) >
-            1.0e-4 * max(fabsf(d), cj->hydro.dx_max_sort_old) &&
-        (fabsf(d - sort_j[pjd].d) - cj->hydro.dx_max_sort) >
-            cj->width[0] * 1.0e-10)
-      error(
-          "particle shift diff exceeds dx_max_sort in cell cj. cj->nodeID=%d "
-          "ci->nodeID=%d d=%e sort_j[pjd].d=%e cj->hydro.dx_max_sort=%e "
-          "cj->hydro.dx_max_sort_old=%e",
-          cj->nodeID, ci->nodeID, d, sort_j[pjd].d, cj->hydro.dx_max_sort,
-          cj->hydro.dx_max_sort_old);
-  }
-#endif /* SWIFT_DEBUG_CHECKS */
+    error("Interacting unsorted cells (cj).");
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
-  DOPAIR1_NAIVE(r, ci, cj);
+  DOPAIR1_NAIVE(r, ci, cj, limit_min_h, limit_max_h);
 #elif defined(WITH_VECTORIZATION) && defined(GADGET2_SPH) && \
     (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
   if (!sort_is_corner(sid))
     runner_dopair1_density_vec(r, ci, cj, sid, shift);
   else
-    DOPAIR1(r, ci, cj, sid, shift);
+    DOPAIR1(r, ci, cj, limit_min_h, limit_max_h, sid, shift);
 #else
-  DOPAIR1(r, ci, cj, sid, shift);
+  DOPAIR1(r, ci, cj, limit_min_h, limit_max_h, sid, shift);
 #endif
 }
 
@@ -1418,14 +1522,18 @@ void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
  * @param r The #runner.
  * @param ci The first #cell.
  * @param cj The second #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  * @param sid The direction of the pair
  * @param shift The shift vector to apply to the particles in ci.
  */
-void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
-             const double *shift) {
+void DOPAIR2(struct runner *r, const struct cell *restrict ci,
+             const struct cell *restrict cj, const int limit_min_h,
+             const int limit_max_h, const int sid, const double shift[3]) {
 
   const struct engine *restrict e = r->e;
   const struct cosmology *restrict cosmo = e->cosmology;
+
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
   const double time_base = e->time_base;
   const integertime_t t_current = e->ti_current;
@@ -1434,7 +1542,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
   TIMER_TIC;
 
-  /* Get the cutoff shift. */
+  /* Get the cut_off shift. */
   double rshift = 0.0;
   for (int k = 0; k < 3; k++) rshift += shift[k] * runner_shift[sid][k];
 
@@ -1456,7 +1564,19 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
       2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
 #endif /* SWIFT_DEBUG_CHECKS */
 
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+#endif
+
   /* Get some other useful values. */
+  const int local_i = ci->nodeID == e->nodeID;
+  const int local_j = cj->nodeID == e->nodeID;
   const double hi_max = ci->hydro.h_max;
   const double hj_max = cj->hydro.h_max;
   const int count_i = ci->hydro.count;
@@ -1497,7 +1617,11 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
     /* Collect the active particles in ci */
     for (int k = 0; k < count_i; k++) {
-      if (PART_IS_ACTIVE(&parts_i[sort_i[k].i], e)) {
+      const struct part *p = &parts_i[sort_i[k].i];
+      const char depth = p->depth_h;
+
+      if (PART_IS_ACTIVE(p, e) && (depth >= min_depth) &&
+          (depth <= max_depth)) {
         sort_active_i[count_active_i] = sort_i[k];
         count_active_i++;
       }
@@ -1516,7 +1640,11 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
     /* Collect the active particles in cj */
     for (int k = 0; k < count_j; k++) {
-      if (PART_IS_ACTIVE(&parts_j[sort_j[k].i], e)) {
+      const struct part *p = &parts_j[sort_j[k].i];
+      const char depth = p->depth_h;
+
+      if (PART_IS_ACTIVE(p, e) && (depth >= min_depth) &&
+          (depth <= max_depth)) {
         sort_active_j[count_active_j] = sort_j[k];
         count_active_j++;
       }
@@ -1532,6 +1660,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
     /* Get a hold of the ith part in ci. */
     struct part *pi = &parts_i[sort_i[pid].i];
+    const char depth_i = pi->depth_h;
 
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
@@ -1548,9 +1677,12 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
     const float piy = pi->x[1] - shift_i[1];
     const float piz = pi->x[2] - shift_i[2];
 
+    const int update_i = PART_IS_ACTIVE(pi, e) && local_i &&
+                         (depth_i >= min_depth) && (depth_i <= max_depth);
+
     /* Do we need to only check active parts in cj
        (i.e. pi does not need updating) ? */
-    if (!PART_IS_ACTIVE(pi, e)) {
+    if (!update_i) {
 
       /* Loop over the *active* parts in cj within range of pi */
       for (int pjd = 0; pjd < count_active_j && sort_active_j[pjd].d < di;
@@ -1617,14 +1749,19 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         /* Hit or miss?
            (note that we will do the other condition in the reverse loop) */
         if (r2 < hig2) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
           IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
           runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -1643,6 +1780,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
         /* Recover pj */
         struct part *pj = &parts_j[sort_j[pjd].i];
+        const char depth_j = pj->depth_h;
 
         /* Skip inhibited particles. */
         if (part_is_inhibited(pj, e)) continue;
@@ -1693,19 +1831,33 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         if (pj->ti_drift != e->ti_current)
           error("Particle pj not drifted to current time");
 #endif
+
 #endif
+
         /* Hit or miss?
            (note that we will do the other condition in the reverse loop) */
         if (r2 < hig2) {
 
+          const int doj = PART_IS_ACTIVE(pj, e) && local_j &&
+                          (depth_j >= min_depth) && (depth_j <= max_depth);
+
           /* Does pj need to be updated too? */
-          if (PART_IS_ACTIVE(pj, e)) {
+          if (doj) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+            if (hi < h_min || hi >= h_max)
+              error("Inappropriate h for this level!");
+            if (hj < h_min || hj >= h_max)
+              error("Inappropriate h for this level!");
+#endif
+
             IACT(r2, dx, hi, hj, pi, pj, a, H);
             IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
             runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
             runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
             runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+            runner_iact_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
             runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -1714,14 +1866,18 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
                                   t_current, cosmo, with_cosmology);
 #endif
           } else {
+
+#ifdef SWIFT_DEBUG_CHECKS
+            if (hi < h_min || hi >= h_max)
+              error("Inappropriate h for this level!");
+#endif
             IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
             IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
             runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
             runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
             runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                    e->sink_properties);
+            runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
             runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -1733,8 +1889,8 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
           }
         }
       } /* loop over the parts in cj. */
-    }   /* Is pi active? */
-  }     /* Loop over all ci */
+    } /* Is pi active? */
+  } /* Loop over all ci */
 
   /* Loop over *all* the parts in cj starting from the centre until
      we are out of range of anything in ci (using the maximal hj). */
@@ -1745,6 +1901,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
     /* Get a hold of the jth part in cj. */
     struct part *pj = &parts_j[sort_j[pjd].i];
+    const char depth_j = pj->depth_h;
 
     /* Skip inhibited particles. */
     if (part_is_inhibited(pj, e)) continue;
@@ -1761,9 +1918,12 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
     const float pjy = pj->x[1] - shift_j[1];
     const float pjz = pj->x[2] - shift_j[2];
 
+    const int update_j = PART_IS_ACTIVE(pj, e) && local_j &&
+                         (depth_j >= min_depth) && (depth_j <= max_depth);
+
     /* Do we need to only check active parts in ci
        (i.e. pj does not need updating) ? */
-    if (!PART_IS_ACTIVE(pj, e)) {
+    if (!update_j) {
 
       /* Loop over the *active* parts in ci. */
       for (int pid = count_active_i - 1;
@@ -1830,14 +1990,19 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         /* Hit or miss?
            (note that we must avoid the r2 < hig2 cases we already processed) */
         if (r2 < hjg2 && r2 >= hig2) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
           IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
           runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
@@ -1857,6 +2022,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
         /* Recover pi */
         struct part *pi = &parts_i[sort_i[pid].i];
+        const char depth_i = pi->depth_h;
 
         /* Skip inhibited particles. */
         if (part_is_inhibited(pi, e)) continue;
@@ -1913,14 +2079,26 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
            (note that we must avoid the r2 < hig2 cases we already processed) */
         if (r2 < hjg2 && r2 >= hig2) {
 
+          const int doi = PART_IS_ACTIVE(pi, e) && local_i &&
+                          (depth_i >= min_depth) && (depth_i <= max_depth);
+
           /* Does pi need to be updated too? */
-          if (PART_IS_ACTIVE(pi, e)) {
+          if (doi) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+            if (hi < h_min || hi >= h_max)
+              error("Inappropriate h for this level!");
+            if (hj < h_min || hj >= h_max)
+              error("Inappropriate h for this level!");
+#endif
+
             IACT(r2, dx, hj, hi, pj, pi, a, H);
             IACT_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
             runner_iact_chemistry(r2, dx, hj, hi, pj, pi, a, H);
             runner_iact_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
             runner_iact_star_formation(r2, dx, hj, hi, pj, pi, a, H);
+            runner_iact_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
             runner_iact_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -1929,14 +2107,19 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
                                   t_current, cosmo, with_cosmology);
 #endif
           } else {
+
+#ifdef SWIFT_DEBUG_CHECKS
+            if (hj < h_min || hj >= h_max)
+              error("Inappropriate h for this level!");
+#endif
+
             IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
             IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
             runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
             runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
             runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-            runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                    e->sink_properties);
+            runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
             runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -1948,8 +2131,8 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
           }
         }
       } /* loop over the parts in ci. */
-    }   /* Is pj active? */
-  }     /* Loop over all cj */
+    } /* Is pj active? */
+  } /* Loop over all cj */
 
   /* Clean-up if necessary */  // MATTHIEU: temporary disable this optimization
   if (CELL_IS_ACTIVE(ci, e))   // && !cell_is_all_active_hydro(ci, e))
@@ -1967,11 +2150,13 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
  * @param r #runner
  * @param ci #cell ci
  * @param cj #cell cj
- *
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOPAIR2_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
+void DOPAIR2_BRANCH(struct runner *r, struct cell *ci, struct cell *cj,
+                    const int limit_min_h, const int limit_max_h) {
 
-  const struct engine *restrict e = r->e;
+  const struct engine *e = r->e;
 
   /* Anything to do here? */
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
@@ -1985,71 +2170,27 @@ void DOPAIR2_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
 
   /* Get the sort ID. */
   double shift[3] = {0.0, 0.0, 0.0};
-  const int sid = space_getsid(e->s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
 
   /* Have the cells been sorted? */
   if (!(ci->hydro.sorted & (1 << sid)) ||
       ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin)
-    error("Interacting unsorted cells.");
+    error("Interacting unsorted cells (ci).");
+
   if (!(cj->hydro.sorted & (1 << sid)) ||
       cj->hydro.dx_max_sort_old > space_maxreldx * cj->dmin)
-    error("Interacting unsorted cells.");
-
-#ifdef SWIFT_DEBUG_CHECKS
-  /* Pick-out the sorted lists. */
-  const struct sort_entry *restrict sort_i = cell_get_hydro_sorts(ci, sid);
-  const struct sort_entry *restrict sort_j = cell_get_hydro_sorts(cj, sid);
-
-  /* Check that the dx_max_sort values in the cell are indeed an upper
-     bound on particle movement. */
-  for (int pid = 0; pid < ci->hydro.count; pid++) {
-    const struct part *p = &ci->hydro.parts[sort_i[pid].i];
-    if (part_is_inhibited(p, e)) continue;
-
-    const float d = p->x[0] * runner_shift[sid][0] +
-                    p->x[1] * runner_shift[sid][1] +
-                    p->x[2] * runner_shift[sid][2];
-    if (fabsf(d - sort_i[pid].d) - ci->hydro.dx_max_sort >
-            1.0e-4 * max(fabsf(d), ci->hydro.dx_max_sort_old) &&
-        fabsf(d - sort_i[pid].d) - ci->hydro.dx_max_sort >
-            ci->width[0] * 1.0e-10)
-      error(
-          "particle shift diff exceeds dx_max_sort in cell ci. ci->nodeID=%d "
-          "cj->nodeID=%d d=%e sort_i[pid].d=%e ci->hydro.dx_max_sort=%e "
-          "ci->hydro.dx_max_sort_old=%e",
-          ci->nodeID, cj->nodeID, d, sort_i[pid].d, ci->hydro.dx_max_sort,
-          ci->hydro.dx_max_sort_old);
-  }
-  for (int pjd = 0; pjd < cj->hydro.count; pjd++) {
-    const struct part *p = &cj->hydro.parts[sort_j[pjd].i];
-    if (part_is_inhibited(p, e)) continue;
-
-    const float d = p->x[0] * runner_shift[sid][0] +
-                    p->x[1] * runner_shift[sid][1] +
-                    p->x[2] * runner_shift[sid][2];
-    if (fabsf(d - sort_j[pjd].d) - cj->hydro.dx_max_sort >
-            1.0e-4 * max(fabsf(d), cj->hydro.dx_max_sort_old) &&
-        fabsf(d - sort_j[pjd].d) - cj->hydro.dx_max_sort >
-            cj->width[0] * 1.0e-10)
-      error(
-          "particle shift diff exceeds dx_max_sort in cell cj. cj->nodeID=%d "
-          "ci->nodeID=%d d=%e sort_j[pjd].d=%e cj->hydro.dx_max_sort=%e "
-          "cj->hydro.dx_max_sort_old=%e",
-          cj->nodeID, ci->nodeID, d, sort_j[pjd].d, cj->hydro.dx_max_sort,
-          cj->hydro.dx_max_sort_old);
-  }
-#endif /* SWIFT_DEBUG_CHECKS */
+    error("Interacting unsorted cells (cj).");
 
 #ifdef SWIFT_USE_NAIVE_INTERACTIONS
-  DOPAIR2_NAIVE(r, ci, cj);
+  DOPAIR2_NAIVE(r, ci, cj, limit_min_h, limit_max_h);
 #elif defined(WITH_VECTORIZATION) && defined(GADGET2_SPH) && \
     (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
   if (!sort_is_corner(sid))
     runner_dopair2_force_vec(r, ci, cj, sid, shift);
   else
-    DOPAIR2(r, ci, cj, sid, shift);
+    DOPAIR2(r, ci, cj, limit_min_h, limit_max_h, sid, shift);
 #else
-  DOPAIR2(r, ci, cj, sid, shift);
+  DOPAIR2(r, ci, cj, limit_min_h, limit_max_h, sid, shift);
 #endif
 }
 
@@ -2058,8 +2199,11 @@ void DOPAIR2_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
  *
  * @param r The #runner.
  * @param c The #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOSELF1(struct runner *r, struct cell *restrict c) {
+void DOSELF1(struct runner *r, const struct cell *c, const int limit_min_h,
+             const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -2071,50 +2215,73 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
 
   TIMER_TIC;
 
-  struct part *restrict parts = c->hydro.parts;
+  struct part *parts = c->hydro.parts;
   const int count = c->hydro.count;
 
-  /* Set up indt. */
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? c->depth : 0;
+  const char max_depth = limit_min_h ? c->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? c->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? c->h_max_allowed : FLT_MAX;
+#endif
+
+  /* Set up a list of the particles for which we want to compute interactions */
   int *indt = NULL;
   int countdt = 0, firstdt = 0;
   if (posix_memalign((void **)&indt, VEC_SIZE * sizeof(int),
                      count * sizeof(int)) != 0)
     error("Failed to allocate indt.");
-  for (int k = 0; k < count; k++)
-    if (PART_IS_ACTIVE(&parts[k], e)) {
+  for (int k = 0; k < count; k++) {
+    const struct part *p = &parts[k];
+    const char depth = p->depth_h;
+    if (PART_IS_ACTIVE(p, e) && (depth >= min_depth) && (depth <= max_depth)) {
       indt[countdt] = k;
       countdt += 1;
     }
+  }
 
-  /* Cosmological terms and physical constants */
+  /* Cosmological terms */
   const float a = cosmo->a;
   const float H = cosmo->H;
   GET_MU0();
 
-  /* Loop over the particles in the cell. */
-  for (int pid = 0; pid < count; pid++) {
+  /* Loop over *all* the particles (i.e. the ones to update and not to update).
+   *
+   * Note the additional condition to make the loop abort if all the active
+   * particles have been processed. */
+  for (int pid = 0; pid < count && firstdt < countdt; pid++) {
 
     /* Get a pointer to the ith particle. */
     struct part *restrict pi = &parts[pid];
+    const char depth_i = pi->depth_h;
 
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
 
-    /* Get the particle position and radius. */
-    double pix[3];
-    for (int k = 0; k < 3; k++) pix[k] = pi->x[k];
+    /* Get the particle position and (square of) search radius. */
+    const double pix[3] = {pi->x[0], pi->x[1], pi->x[2]};
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
 
-    /* Is the ith particle inactive? */
-    if (!PART_IS_ACTIVE(pi, e)) {
+    /* Is the ith particle active and in the range of h we care about? */
+    const int update_i = PART_IS_ACTIVE(pi, e) && (depth_i >= min_depth) &&
+                         (depth_i <= max_depth);
+
+    /* If false then it can only act as a neighbour of others */
+    if (!update_i) {
 
-      /* Loop over the other particles .*/
+      /* Loop over the particles we want to update. */
       for (int pjd = firstdt; pjd < countdt; pjd++) {
 
-        /* Get a pointer to the jth particle. */
+        /* Get a pointer to the jth particle. (by construction pi != pj) */
         struct part *restrict pj = &parts[indt[pjd]];
+
+        /* This particle's (square of) search radius. */
         const float hj = pj->h;
+        const float hjg2 = hj * hj * kernel_gamma2;
 
 #if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
@@ -2124,16 +2291,19 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
           error("Particle pj not drifted to current time");
 #endif
 
-        /* Compute the pairwise distance. */
-        float r2 = 0.0f;
-        float dx[3];
-        for (int k = 0; k < 3; k++) {
-          dx[k] = pj->x[k] - pix[k];
-          r2 += dx[k] * dx[k];
-        }
+        /* Compute the (square of) pairwise distance. */
+        const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+        const float dx[3] = {(float)(pjx[0] - pix[0]), (float)(pjx[1] - pix[1]),
+                             (float)(pjx[2] - pix[2])};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
         /* Hit or miss? */
-        if (r2 < hj * hj * kernel_gamma2) {
+        if (r2 < hjg2) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
 
           IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
           IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
@@ -2141,8 +2311,7 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
           runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -2151,37 +2320,32 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
                                        t_current, cosmo, with_cosmology);
 #endif
         }
-      } /* loop over all other particles. */
+      } /* loop over all the particles we want to update. */
     }
 
     /* Otherwise, interact with all candidates. */
     else {
 
-      /* We caught a live one! */
+      /* We caught a live one!
+       * Move the start of the list of active ones by one slot as it will have
+       * been fully processed after the following loop so no need to consider it
+       * in the previous loop any more. */
       firstdt += 1;
 
-      /* Loop over the other particles .*/
+      /* Loop over *all* the particles (i.e. the ones to update and not to
+       * update) but starting from where we are in the overall list. */
       for (int pjd = pid + 1; pjd < count; pjd++) {
 
-        /* Get a pointer to the jth particle. */
+        /* Get a pointer to the jth particle (by construction pi != pj). */
         struct part *restrict pj = &parts[pjd];
+        const char depth_j = pj->depth_h;
 
         /* Skip inhibited particles. */
         if (part_is_inhibited(pj, e)) continue;
 
+        /* This particle's (square of) search radius. */
         const float hj = pj->h;
-
-        /* Compute the pairwise distance. */
-        float r2 = 0.0f;
-        float dx[3];
-        for (int k = 0; k < 3; k++) {
-          dx[k] = pix[k] - pj->x[k];
-          r2 += dx[k] * dx[k];
-        }
-        const int doj =
-            (PART_IS_ACTIVE(pj, e)) && (r2 < hj * hj * kernel_gamma2);
-
-        const int doi = (r2 < hig2);
+        const float hjg2 = hj * hj * kernel_gamma2;
 
 #if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
@@ -2191,68 +2355,101 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
           error("Particle pj not drifted to current time");
 #endif
 
+        /* Compute the (square of) pairwise distance. */
+        const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+        float dx[3] = {(float)(pix[0] - pjx[0]), (float)(pix[1] - pjx[1]),
+                       (float)(pix[2] - pjx[2])};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+        /* Decide which of the two particles to update */
+
+        /* We know pi is active and in the right range of h
+         * -> Only check the distance to pj */
+        const int doi = (r2 < hig2);
+
+        /* We know nothing about pj
+         * -> Check whether it is active
+         * -> Check whether it is in the right range of h
+         * -> Check the distance to pi */
+        const int doj = (PART_IS_ACTIVE(pj, e)) && (depth_j >= min_depth) &&
+                        (depth_j <= max_depth) && (r2 < hjg2);
+
         /* Hit or miss? */
-        if (doi || doj) {
+        if (doi && doj) {
 
-          /* Which parts need to be updated? */
-          if (doi && doj) {
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+          /* Update both pi and pj */
 
-            IACT(r2, dx, hi, hj, pi, pj, a, H);
-            IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
+          IACT(r2, dx, hi, hj, pi, pj, a, H);
+          IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-            runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-            runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
-                                  t_current, cosmo, with_cosmology);
+          runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
+                                t_current, cosmo, with_cosmology);
 #endif
-          } else if (doi) {
+        } else if (doi) {
 
-            IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
-            IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+          /* Update only pi */
+
+          IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
+          IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-            runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                    e->sink_properties);
+          runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-            runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_diffusion(r2, dx, hi, hj, pi, pj, a, H,
-                                         time_base, t_current, cosmo,
-                                         with_cosmology);
+          runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
+                                       t_current, cosmo, with_cosmology);
 #endif
-          } else if (doj) {
+        } else if (doj) {
 
-            dx[0] = -dx[0];
-            dx[1] = -dx[1];
-            dx[2] = -dx[2];
-            IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
-            IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
+          /* Update only pj */
+
+          dx[0] = -dx[0];
+          dx[1] = -dx[1];
+          dx[2] = -dx[2];
+          IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
+          IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-            runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
-            runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
-            runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-            runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                    e->sink_properties);
+          runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
+          runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
+          runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
+          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-            runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
-            runner_iact_nonsym_rt_timebin(r2, dx, hj, hi, pj, pi, a, H);
-            runner_iact_nonsym_diffusion(r2, dx, hj, hi, pj, pi, a, H,
-                                         time_base, t_current, cosmo,
-                                         with_cosmology);
+          runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
+          runner_iact_nonsym_rt_timebin(r2, dx, hj, hi, pj, pi, a, H);
+          runner_iact_nonsym_diffusion(r2, dx, hj, hi, pj, pi, a, H, time_base,
+                                       t_current, cosmo, with_cosmology);
 #endif
-          }
-        }
+        } /* Hit or miss */
       } /* loop over all other particles. */
-    }
+    } /* pi is active */
   } /* loop over all particles. */
 
   free(indt);
@@ -2266,11 +2463,13 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
  *
  * @param r #runner
  * @param c #cell c
- *
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOSELF1_BRANCH(struct runner *r, struct cell *c) {
+void DOSELF1_BRANCH(struct runner *r, const struct cell *c,
+                    const int limit_min_h, const int limit_max_h) {
 
-  const struct engine *restrict e = r->e;
+  const struct engine *e = r->e;
 
   /* Anything to do here? */
   if (c->hydro.count == 0) return;
@@ -2278,20 +2477,28 @@ void DOSELF1_BRANCH(struct runner *r, struct cell *c) {
   /* Anything to do here? */
   if (!CELL_IS_ACTIVE(c, e)) return;
 
+#ifdef SWIFT_DEBUG_CHECKS
+
   /* Did we mess up the recursion? */
   if (c->hydro.h_max_old * kernel_gamma > c->dmin)
-    error("Cell smaller than smoothing length");
+    if (!limit_max_h && c->hydro.h_max_active * kernel_gamma > c->dmin)
+      error("Cell smaller than smoothing length");
+
+  /* Did we mess up the recursion? */
+  if (limit_min_h && !limit_max_h)
+    error("Fundamental error in the recursion logic");
+#endif
 
   /* Check that cells are drifted. */
   if (!CELL_ARE_PART_DRIFTED(c, e)) error("Interacting undrifted cell.");
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
-  DOSELF1_NAIVE(r, c);
+  DOSELF1_NAIVE(r, c, limit_min_h, limit_max_h);
 #elif defined(WITH_VECTORIZATION) && defined(GADGET2_SPH) && \
     (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
   runner_doself1_density_vec(r, c);
 #else
-  DOSELF1(r, c);
+  DOSELF1(r, c, limit_min_h, limit_max_h);
 #endif
 }
 
@@ -2300,8 +2507,11 @@ void DOSELF1_BRANCH(struct runner *r, struct cell *c) {
  *
  * @param r The #runner.
  * @param c The #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOSELF2(struct runner *r, struct cell *restrict c) {
+void DOSELF2(struct runner *r, const struct cell *c, const int limit_min_h,
+             const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -2313,58 +2523,73 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
 
   TIMER_TIC;
 
-  struct part *restrict parts = c->hydro.parts;
+  struct part *parts = c->hydro.parts;
   const int count = c->hydro.count;
 
-  /* Set up indt. */
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? c->depth : 0;
+  const char max_depth = limit_min_h ? c->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? c->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? c->h_max_allowed : FLT_MAX;
+#endif
+
+  /* Set up a list of the particles for which we want to compute interactions */
   int *indt = NULL;
   int countdt = 0, firstdt = 0;
   if (posix_memalign((void **)&indt, VEC_SIZE * sizeof(int),
                      count * sizeof(int)) != 0)
     error("Failed to allocate indt.");
-  for (int k = 0; k < count; k++)
-    if (PART_IS_ACTIVE(&parts[k], e)) {
+  for (int k = 0; k < count; k++) {
+    const struct part *p = &parts[k];
+    const char depth = p->depth_h;
+    if (PART_IS_ACTIVE(p, e) && (depth >= min_depth) && (depth <= max_depth)) {
       indt[countdt] = k;
       countdt += 1;
     }
+  }
 
   /* Cosmological terms and physical constants */
   const float a = cosmo->a;
   const float H = cosmo->H;
   GET_MU0();
 
-  /* Loop over the particles in the cell. */
-  for (int pid = 0; pid < count; pid++) {
+  /* Loop over *all* the particles (the ones to update and others!) in the cell.
+   *
+   * Note the additional condition to make the loop abort if all the active
+   * particles have been processed. */
+  for (int pid = 0; pid < count && firstdt < countdt; pid++) {
 
     /* Get a pointer to the ith particle. */
     struct part *restrict pi = &parts[pid];
+    const char depth_i = pi->depth_h;
 
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
 
-    /* Get the particle position and radius. */
-    double pix[3];
-    for (int k = 0; k < 3; k++) pix[k] = pi->x[k];
+    /* Get the particle position and (square of) search radius. */
+    const double pix[3] = {pi->x[0], pi->x[1], pi->x[2]};
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
 
-    /* Is the ith particle not active? */
-    if (!PART_IS_ACTIVE(pi, e)) {
+    /* Is the ith particle active and in the range of h we care about? */
+    const int update_i = PART_IS_ACTIVE(pi, e) && (depth_i >= min_depth) &&
+                         (depth_i <= max_depth);
 
-      /* Loop over the other particles .*/
+    /* If false then it can only act as a neighbour of others */
+    if (!update_i) {
+
+      /* Loop over the active particles we want to update. */
       for (int pjd = firstdt; pjd < countdt; pjd++) {
 
-        /* Get a pointer to the jth particle. */
+        /* Get a pointer to the jth particle. (by construction pi != pj) */
         struct part *restrict pj = &parts[indt[pjd]];
-        const float hj = pj->h;
 
-        /* Compute the pairwise distance. */
-        float r2 = 0.0f;
-        float dx[3];
-        for (int k = 0; k < 3; k++) {
-          dx[k] = pj->x[k] - pix[k];
-          r2 += dx[k] * dx[k];
-        }
+        /* This particle's (square of) search radius. */
+        const float hj = pj->h;
+        const float hjg2 = hj * hj * kernel_gamma2;
 
 #if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
@@ -2374,8 +2599,19 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
           error("Particle pj not drifted to current time");
 #endif
 
+        /* Compute the (square of) pairwise distance. */
+        const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+        const float dx[3] = {(float)(pjx[0] - pix[0]), (float)(pjx[1] - pix[1]),
+                             (float)(pjx[2] - pix[2])};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
         /* Hit or miss? */
-        if (r2 < hig2 || r2 < hj * hj * kernel_gamma2) {
+        if (r2 < hig2 || r2 < hjg2) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
 
           IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
           IACT_NONSYM_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
@@ -2383,8 +2619,7 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
           runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
           runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
-          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H,
-                                  e->sink_properties);
+          runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
           runner_iact_nonsym_timebin(r2, dx, hj, hi, pj, pi, a, H);
@@ -2399,27 +2634,26 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
     /* Otherwise, interact with all candidates. */
     else {
 
-      /* We caught a live one! */
+      /* We caught a live one!
+       * Move the start of the list of active ones by one slot as it will have
+       * been fully processed after the following loop so no need to consider it
+       * in the previous loop any more. */
       firstdt += 1;
 
-      /* Loop over the other particles .*/
+      /* Loop over *all* the particles (i.e. the ones to update and not to
+       * update) but starting from where we are in the overall list. */
       for (int pjd = pid + 1; pjd < count; pjd++) {
 
         /* Get a pointer to the jth particle. */
         struct part *restrict pj = &parts[pjd];
+        const char depth_j = pj->depth_h;
 
         /* Skip inhibited particles. */
         if (part_is_inhibited(pj, e)) continue;
 
+        /* This particle's (square of) search radius. */
         const float hj = pj->h;
-
-        /* Compute the pairwise distance. */
-        float r2 = 0.0f;
-        float dx[3];
-        for (int k = 0; k < 3; k++) {
-          dx[k] = pix[k] - pj->x[k];
-          r2 += dx[k] * dx[k];
-        }
+        const float hjg2 = hj * hj * kernel_gamma2;
 
 #if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
@@ -2429,45 +2663,86 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
           error("Particle pj not drifted to current time");
 #endif
 
+        /* Compute the (square of) pairwise distance. */
+        const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+        float dx[3] = {(float)(pix[0] - pjx[0]), (float)(pix[1] - pjx[1]),
+                       (float)(pix[2] - pjx[2])};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+        /* Decide which of the two particles to update */
+
+        /* We know pi is active and in the right range of h
+         * -> Only check the distance to pj */
+        const int doi = (r2 < hig2) || (r2 < hjg2);
+
+        /* We know nothing about pj
+         * -> Check whether it is active
+         * -> Check whether it is in the right range of h
+         * -> Check the distance to pi */
+        const int doj = (PART_IS_ACTIVE(pj, e)) && (depth_j >= min_depth) &&
+                        (depth_j <= max_depth) && ((r2 < hjg2) || (r2 < hig2));
+
         /* Hit or miss? */
-        if (r2 < hig2 || r2 < hj * hj * kernel_gamma2) {
+        if (doi && doj) {
 
-          /* Does pj need to be updated too? */
-          if (PART_IS_ACTIVE(pj, e)) {
-            IACT(r2, dx, hi, hj, pi, pj, a, H);
-            IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
+          /* Update both pi and pj */
+
+          IACT(r2, dx, hi, hj, pi, pj, a, H);
+          IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-            runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_chemistry(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-            runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
-                                  t_current, cosmo, with_cosmology);
+          runner_iact_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
+                                t_current, cosmo, with_cosmology);
 #endif
-          } else {
-            IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
-            IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
+        } else if (doi) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
+          /* Update only pi */
+
+          IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
+          IACT_NONSYM_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
-            runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H,
-                                    e->sink_properties);
+          runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
 #endif
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
-            runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
-            runner_iact_nonsym_diffusion(r2, dx, hi, hj, pi, pj, a, H,
-                                         time_base, t_current, cosmo,
-                                         with_cosmology);
+          runner_iact_nonsym_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_rt_timebin(r2, dx, hi, hj, pi, pj, a, H);
+          runner_iact_nonsym_diffusion(r2, dx, hi, hj, pi, pj, a, H, time_base,
+                                       t_current, cosmo, with_cosmology);
 #endif
-          }
-        }
+        } else if (doj) {
+
+          /* Update only doj
+           *
+           * Note: This is impossible since if doj==True so does doi */
+
+#ifdef SWIFT_DEBUG_CHECKS
+          error("Impossible problem in the logic!!!");
+#endif
+        } /* Hit or miss */
       } /* loop over all other particles. */
-    }
+    } /* pi is active */
   } /* loop over all particles. */
 
   free(indt);
@@ -2481,11 +2756,13 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
  *
  * @param r #runner
  * @param c #cell c
- *
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  */
-void DOSELF2_BRANCH(struct runner *r, struct cell *c) {
+void DOSELF2_BRANCH(struct runner *r, const struct cell *c,
+                    const int limit_min_h, const int limit_max_h) {
 
-  const struct engine *restrict e = r->e;
+  const struct engine *e = r->e;
 
   /* Anything to do here? */
   if (c->hydro.count == 0) return;
@@ -2493,20 +2770,29 @@ void DOSELF2_BRANCH(struct runner *r, struct cell *c) {
   /* Anything to do here? */
   if (!CELL_IS_ACTIVE(c, e)) return;
 
+#ifdef SWIFT_DEBUG_CHECKS
+
   /* Did we mess up the recursion? */
   if (c->hydro.h_max_old * kernel_gamma > c->dmin)
-    error("Cell smaller than smoothing length");
+    if (!limit_max_h && c->hydro.h_max_active * kernel_gamma > c->dmin)
+      error("Cell smaller than smoothing length");
+
+  /* Did we mess up the recursion? */
+  if (limit_min_h && !limit_max_h)
+    error("Fundamental error in the recursion logic");
+
+#endif
 
   /* Check that cells are drifted. */
   if (!CELL_ARE_PART_DRIFTED(c, e)) error("Interacting undrifted cell.");
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
-  DOSELF2_NAIVE(r, c);
+  DOSELF2_NAIVE(r, c, limit_min_h, limit_max_h);
 #elif defined(WITH_VECTORIZATION) && defined(GADGET2_SPH) && \
     (FUNCTION_TASK_LOOP == TASK_LOOP_FORCE)
   runner_doself2_force_vec(r, c);
 #else
-  DOSELF2(r, c);
+  DOSELF2(r, c, limit_min_h, limit_max_h);
 #endif
 }
 
@@ -2516,13 +2802,12 @@ void DOSELF2_BRANCH(struct runner *r, struct cell *c) {
  * @param r The #runner.
  * @param ci The first #cell.
  * @param cj The second #cell.
+ * @param recurse_below_h_max Are we currently recursing at a level where we
+ * violated the h < cell size condition.
  * @param gettimer Do we have a timer ?
- *
- * @todo Hard-code the sid on the recursive calls to avoid the
- * redundant computations to find the sid on-the-fly.
  */
 void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
-                 int gettimer) {
+                 int recurse_below_h_max, const int gettimer) {
 
   struct space *s = r->e->s;
   const struct engine *e = r->e;
@@ -2535,43 +2820,83 @@ void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
 
   /* Get the type of pair and flip ci/cj if needed. */
   double shift[3];
-  const int sid = space_getsid(s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
 
-  /* Recurse? */
-  if (cell_can_recurse_in_pair_hydro_task(ci) &&
-      cell_can_recurse_in_pair_hydro_task(cj)) {
-    struct cell_split_pair *csp = &cell_split_pairs[sid];
-    for (int k = 0; k < csp->count; k++) {
-      const int pid = csp->pairs[k].pid;
-      const int pjd = csp->pairs[k].pjd;
-      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL)
-        DOSUB_PAIR1(r, ci->progeny[pid], cj->progeny[pjd], 0);
+  /* We reached a leaf OR a cell small enough to be processed quickly */
+  if (!ci->split || ci->hydro.count < space_recurse_size_pair_hydro ||
+      !cj->split || cj->hydro.count < space_recurse_size_pair_hydro) {
+
+    /* Do any of the cells need to be sorted first?
+     * Since h_max might have changed, we may not have sorted at this level */
+    if (!(ci->hydro.sorted & (1 << sid)) ||
+        ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
+      /* Bert: RT probably broken here! */
+      runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                           /*rt_request=*/0, /*clock=*/0);
+    }
+    if (!(cj->hydro.sorted & (1 << sid)) ||
+        cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+      /* Bert: RT probably broken here! */
+      runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                           /*rt_request=*/0, /*clock=*/0);
     }
-  }
 
-  /* Otherwise, compute the pair directly. */
-  else if (CELL_IS_ACTIVE(ci, e) || CELL_IS_ACTIVE(cj, e)) {
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOPAIR1_BRANCH(r, ci, cj, /*limit_h_min=*/0,
+                   /*limit_h_max=*/recurse_below_h_max);
 
-    /* Make sure both cells are drifted to the current timestep. */
-    if (!CELL_ARE_PART_DRIFTED(ci, e) || !CELL_ARE_PART_DRIFTED(cj, e))
-      error("Interacting undrifted cells.");
+  } else {
 
-    /* Do any of the cells need to be sorted first? */
-    if (!(ci->hydro.sorted & (1 << sid)) ||
-        ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx)
-      error(
-          "Interacting unsorted cell. ci->hydro.dx_max_sort_old=%e ci->dmin=%e "
-          "ci->sorted=%d sid=%d",
-          ci->hydro.dx_max_sort_old, ci->dmin, ci->hydro.sorted, sid);
-    if (!(cj->hydro.sorted & (1 << sid)) ||
-        cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx)
-      error(
-          "Interacting unsorted cell. cj->hydro.dx_max_sort_old=%e cj->dmin=%e "
-          "cj->sorted=%d sid=%d",
-          cj->hydro.dx_max_sort_old, cj->dmin, cj->hydro.sorted, sid);
-
-    /* Compute the interactions. */
-    DOPAIR1_BRANCH(r, ci, cj);
+    /* Both ci and cj are split */
+
+    /* Should we change the recursion regime because we encountered a large
+       particle? */
+    if (!recurse_below_h_max && (!cell_can_recurse_in_subpair_hydro_task(ci) ||
+                                 !cell_can_recurse_in_subpair_hydro_task(cj))) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
+
+      /* Do any of the cells need to be sorted first?
+       * Since h_max might have changed, we may not have sorted at this level */
+      if (!(ci->hydro.sorted & (1 << sid)) ||
+          ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
+      }
+      if (!(cj->hydro.sorted & (1 << sid)) ||
+          cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
+      }
+
+      /* message("Multi-level PAIR! ci->count=%d cj->count=%d", ci->hydro.count,
+       */
+      /* 	      cj->hydro.count); */
+
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOPAIR1_BRANCH(r, ci, cj, /*limit_h_min=*/1, /*limit_h_max=*/1);
+    }
+
+    /* Recurse to the lower levels. */
+    const struct cell_split_pair *const csp = &cell_split_pairs[sid];
+    for (int k = 0; k < csp->count; k++) {
+      const int pid = csp->pairs[k].pid;
+      const int pjd = csp->pairs[k].pjd;
+      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL) {
+        DOSUB_PAIR1(r, ci->progeny[pid], cj->progeny[pjd], recurse_below_h_max,
+                    /*gettimer=*/0);
+      }
+    }
   }
 
   if (gettimer) TIMER_TOC(TIMER_DOSUB_PAIR);
@@ -2581,36 +2906,60 @@ void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
  * @brief Compute grouped sub-cell interactions for self tasks
  *
  * @param r The #runner.
- * @param ci The first #cell.
+ * @param c The #cell.
+ * @param recurse_below_h_max Are we currently recursing at a level where we
+ * violated the h < cell size condition.
  * @param gettimer Do we have a timer ?
  */
-void DOSUB_SELF1(struct runner *r, struct cell *ci, int gettimer) {
+void DOSUB_SELF1(struct runner *r, struct cell *c, int recurse_below_h_max,
+                 const int gettimer) {
 
   TIMER_TIC;
 
   /* Should we even bother? */
-  if (ci->hydro.count == 0 || !CELL_IS_ACTIVE(ci, r->e)) return;
+  if (c->hydro.count == 0 || !CELL_IS_ACTIVE(c, r->e)) return;
 
-  /* Recurse? */
-  if (cell_can_recurse_in_self_hydro_task(ci)) {
+  /* We reached a leaf OR a cell small enough to process quickly */
+  if (!c->split || c->hydro.count < space_recurse_size_self_hydro) {
 
-    /* Loop over all progeny. */
-    for (int k = 0; k < 8; k++)
-      if (ci->progeny[k] != NULL) {
-        DOSUB_SELF1(r, ci->progeny[k], 0);
-        for (int j = k + 1; j < 8; j++)
-          if (ci->progeny[j] != NULL)
-            DOSUB_PAIR1(r, ci->progeny[k], ci->progeny[j], 0);
-      }
-  }
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOSELF1_BRANCH(r, c, /*limit_h_min=*/0,
+                   /*limit_h_max=*/recurse_below_h_max);
 
-  /* Otherwise, compute self-interaction. */
-  else {
+  } else {
+
+    /* Should we change the recursion regime because we encountered a large
+       particle at this level? */
+    if (!recurse_below_h_max && !cell_can_recurse_in_subself_hydro_task(c)) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
 
-    /* Drift the cell to the current timestep if needed. */
-    if (!CELL_ARE_PART_DRIFTED(ci, r->e)) error("Interacting undrifted cell.");
+      /* message("Multi-level SELF! c->count=%d", c->hydro.count); */
 
-    DOSELF1_BRANCH(r, ci);
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOSELF1_BRANCH(r, c, /*limit_h_min=*/1, /*limit_h_max=*/1);
+    }
+
+    /* Recurse to the lower levels. */
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL) {
+        DOSUB_SELF1(r, c->progeny[k], recurse_below_h_max, /*gettimer=*/0);
+        for (int j = k + 1; j < 8; j++) {
+          if (c->progeny[j] != NULL) {
+            DOSUB_PAIR1(r, c->progeny[k], c->progeny[j], recurse_below_h_max,
+                        /*gettimer=*/0);
+          }
+        }
+      }
+    }
   }
 
   if (gettimer) TIMER_TOC(TIMER_DOSUB_SELF);
@@ -2628,7 +2977,7 @@ void DOSUB_SELF1(struct runner *r, struct cell *ci, int gettimer) {
  * redundant computations to find the sid on-the-fly.
  */
 void DOSUB_PAIR2(struct runner *r, struct cell *ci, struct cell *cj,
-                 int gettimer) {
+                 int recurse_below_h_max, const int gettimer) {
 
   const struct engine *e = r->e;
   struct space *s = e->s;
@@ -2641,43 +2990,82 @@ void DOSUB_PAIR2(struct runner *r, struct cell *ci, struct cell *cj,
 
   /* Get the type of pair and flip ci/cj if needed. */
   double shift[3];
-  const int sid = space_getsid(s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
 
-  /* Recurse? */
-  if (cell_can_recurse_in_pair_hydro_task(ci) &&
-      cell_can_recurse_in_pair_hydro_task(cj)) {
-    struct cell_split_pair *csp = &cell_split_pairs[sid];
-    for (int k = 0; k < csp->count; k++) {
-      const int pid = csp->pairs[k].pid;
-      const int pjd = csp->pairs[k].pjd;
-      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL)
-        DOSUB_PAIR2(r, ci->progeny[pid], cj->progeny[pjd], 0);
+  /* We reached a leaf OR a cell small enough to be processed quickly */
+  if (!ci->split || ci->hydro.count < space_recurse_size_pair_hydro ||
+      !cj->split || cj->hydro.count < space_recurse_size_pair_hydro) {
+
+    /* Do any of the cells need to be sorted first?
+     * Since h_max might have changed, we may not have sorted at this level */
+    if (!(ci->hydro.sorted & (1 << sid)) ||
+        ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
+      /* Bert: RT probably broken here! */
+      runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                           /*rt_request=*/0, /*clock=*/0);
+    }
+    if (!(cj->hydro.sorted & (1 << sid)) ||
+        cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+      /* Bert: RT probably broken here! */
+      runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                           /*rt_request=*/0, /*clock=*/0);
     }
-  }
 
-  /* Otherwise, compute the pair directly. */
-  else if (CELL_IS_ACTIVE(ci, e) || CELL_IS_ACTIVE(cj, e)) {
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOPAIR2_BRANCH(r, ci, cj, /*limit_h_min=*/0,
+                   /*limit_h_max=*/recurse_below_h_max);
 
-    /* Make sure both cells are drifted to the current timestep. */
-    if (!CELL_ARE_PART_DRIFTED(ci, e) || !CELL_ARE_PART_DRIFTED(cj, e))
-      error("Interacting undrifted cells.");
+  } else {
 
-    /* Do any of the cells need to be sorted first? */
-    if (!(ci->hydro.sorted & (1 << sid)) ||
-        ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx)
-      error(
-          "Interacting unsorted cell. ci->hydro.dx_max_sort_old=%e ci->dmin=%e "
-          "ci->sorted=%d sid=%d",
-          ci->hydro.dx_max_sort_old, ci->dmin, ci->hydro.sorted, sid);
-    if (!(cj->hydro.sorted & (1 << sid)) ||
-        cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx)
-      error(
-          "Interacting unsorted cell. cj->hydro.dx_max_sort_old=%e cj->dmin=%e "
-          "cj->sorted=%d sid=%d",
-          cj->hydro.dx_max_sort_old, cj->dmin, cj->hydro.sorted, sid);
-
-    /* Compute the interactions. */
-    DOPAIR2_BRANCH(r, ci, cj);
+    /* Should we change the recursion regime because we encountered a large
+       particle? */
+    if (!recurse_below_h_max &&
+        (!cell_can_recurse_in_subpair2_hydro_task(ci) ||
+         !cell_can_recurse_in_subpair2_hydro_task(cj))) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
+
+      /* Do any of the cells need to be sorted first?
+       * Since h_max might have changed, we may not have sorted at this level */
+      if (!(ci->hydro.sorted & (1 << sid)) ||
+          ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
+      }
+      if (!(cj->hydro.sorted & (1 << sid)) ||
+          cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
+      }
+
+      /* message("Multi-level PAIR! ci->count=%d cj->count=%d", ci->hydro.count,
+       */
+      /* 	      cj->hydro.count); */
+
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOPAIR2_BRANCH(r, ci, cj, /*limit_h_min=*/1, /*limit_h_max=*/1);
+    }
+
+    /* Recurse to the lower levels. */
+    const struct cell_split_pair *const csp = &cell_split_pairs[sid];
+    for (int k = 0; k < csp->count; k++) {
+      const int pid = csp->pairs[k].pid;
+      const int pjd = csp->pairs[k].pjd;
+      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL) {
+        DOSUB_PAIR2(r, ci->progeny[pid], cj->progeny[pjd], recurse_below_h_max,
+                    /*gettimer=*/0);
+      }
+    }
   }
 
   if (gettimer) TIMER_TOC(TIMER_DOSUB_PAIR);
@@ -2690,36 +3078,96 @@ void DOSUB_PAIR2(struct runner *r, struct cell *ci, struct cell *cj,
  * @param ci The first #cell.
  * @param gettimer Do we have a timer ?
  */
-void DOSUB_SELF2(struct runner *r, struct cell *ci, int gettimer) {
+void DOSUB_SELF2(struct runner *r, struct cell *c, int recurse_below_h_max,
+                 const int gettimer) {
 
   TIMER_TIC;
 
   /* Should we even bother? */
-  if (ci->hydro.count == 0 || !CELL_IS_ACTIVE(ci, r->e)) return;
+  if (c->hydro.count == 0 || !CELL_IS_ACTIVE(c, r->e)) return;
 
-  /* Recurse? */
-  if (cell_can_recurse_in_self_hydro_task(ci)) {
+  /* We reached a leaf OR a cell small enough to process quickly */
+  if (!c->split || c->hydro.count < space_recurse_size_self_hydro) {
 
-    /* Loop over all progeny. */
-    for (int k = 0; k < 8; k++)
-      if (ci->progeny[k] != NULL) {
-        DOSUB_SELF2(r, ci->progeny[k], 0);
-        for (int j = k + 1; j < 8; j++)
-          if (ci->progeny[j] != NULL)
-            DOSUB_PAIR2(r, ci->progeny[k], ci->progeny[j], 0);
-      }
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOSELF2_BRANCH(r, c, /*limit_h_min=*/0,
+                   /*limit_h_max=*/recurse_below_h_max);
 
-  }
+  } else {
 
-  /* Otherwise, compute self-interaction. */
-  else {
-    DOSELF2_BRANCH(r, ci);
+    /* Should we change the recursion regime because we encountered a large
+       particle at this level? */
+    if (!recurse_below_h_max && !cell_can_recurse_in_subself2_hydro_task(c)) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
+
+      /* message("Multi-level SELF! c->count=%d", c->hydro.count); */
+
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOSELF2_BRANCH(r, c, /*limit_h_min=*/1, /*limit_h_max=*/1);
+    }
+
+    /* Recurse to the lower levels. */
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL) {
+        DOSUB_SELF2(r, c->progeny[k], recurse_below_h_max, /*gettimer=*/0);
+        for (int j = k + 1; j < 8; j++) {
+          if (c->progeny[j] != NULL) {
+            DOSUB_PAIR2(r, c->progeny[k], c->progeny[j], recurse_below_h_max,
+                        /*gettimer=*/0);
+          }
+        }
+      }
+    }
   }
+
   if (gettimer) TIMER_TOC(TIMER_DOSUB_SELF);
 }
 
-void DOSUB_SUBSET(struct runner *r, struct cell *ci, struct part *parts,
-                  int *ind, int count, struct cell *cj, int gettimer) {
+/**
+ * @brief Find which sub-cell of a cell contain the subset of particles given
+ * by the list of indices.
+ *
+ * Will throw an error if the sub-cell can't be found.
+ *
+ * @param c The #cell
+ * @param parts An array of #part.
+ * @param ind Index of the #part's in the particle array to find in the subs.
+ */
+struct cell *FIND_SUB(const struct cell *const c,
+                      const struct part *const parts, const int *const ind) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (!c->split) error("Can't search for subs in a non-split cell");
+#endif
+
+  /* Find out in which sub-cell of ci the parts are.
+   *
+   * Note: We only need to check the first particle in the list */
+  for (int k = 0; k < 8; k++) {
+    if (c->progeny[k] != NULL) {
+      if (&parts[ind[0]] >= &c->progeny[k]->hydro.parts[0] &&
+          &parts[ind[0]] <
+              &c->progeny[k]->hydro.parts[c->progeny[k]->hydro.count]) {
+        return c->progeny[k];
+      }
+    }
+  }
+  error("Invalid sub!");
+  return NULL;
+}
+
+void DOSUB_PAIR_SUBSET(struct runner *r, struct cell *ci, struct part *parts,
+                       const int *ind, const int count, struct cell *cj,
+                       const int gettimer) {
 
   const struct engine *e = r->e;
   struct space *s = e->s;
@@ -2727,79 +3175,71 @@ void DOSUB_SUBSET(struct runner *r, struct cell *ci, struct part *parts,
   TIMER_TIC;
 
   /* Should we even bother? */
-  if (!cell_is_active_hydro(ci, e) &&
-      (cj == NULL || !cell_is_active_hydro(cj, e)))
-    return;
-  if (ci->hydro.count == 0 || (cj != NULL && cj->hydro.count == 0)) return;
-
-  /* Find out in which sub-cell of ci the parts are. */
-  struct cell *sub = NULL;
-  if (ci->split) {
-    for (int k = 0; k < 8; k++) {
-      if (ci->progeny[k] != NULL) {
-        if (&parts[ind[0]] >= &ci->progeny[k]->hydro.parts[0] &&
-            &parts[ind[0]] <
-                &ci->progeny[k]->hydro.parts[ci->progeny[k]->hydro.count]) {
-          sub = ci->progeny[k];
-          break;
-        }
-      }
-    }
-  }
+  if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
+  if (!cell_is_active_hydro(ci, e)) return;
 
-  /* Is this a single cell? */
-  if (cj == NULL) {
+  /* Recurse? */
+  if (cell_can_recurse_in_pair_hydro_task(ci) &&
+      cell_can_recurse_in_pair_hydro_task(cj)) {
 
-    /* Recurse? */
-    if (cell_can_recurse_in_self_hydro_task(ci)) {
+    /* Find in which sub-cell of ci the particles are */
+    struct cell *const sub = FIND_SUB(ci, parts, ind);
 
-      /* Loop over all progeny. */
-      DOSUB_SUBSET(r, sub, parts, ind, count, NULL, 0);
-      for (int j = 0; j < 8; j++)
-        if (ci->progeny[j] != sub && ci->progeny[j] != NULL)
-          DOSUB_SUBSET(r, sub, parts, ind, count, ci->progeny[j], 0);
+    /* Get the type of pair and flip ci/cj if needed. */
+    double shift[3];
+    const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
 
+    struct cell_split_pair *csp = &cell_split_pairs[sid];
+    for (int k = 0; k < csp->count; k++) {
+      const int pid = csp->pairs[k].pid;
+      const int pjd = csp->pairs[k].pjd;
+      if (ci->progeny[pid] == sub && cj->progeny[pjd] != NULL)
+        DOSUB_PAIR_SUBSET(r, ci->progeny[pid], parts, ind, count,
+                          cj->progeny[pjd],
+                          /*gettimer=*/0);
+      if (ci->progeny[pid] != NULL && cj->progeny[pjd] == sub)
+        DOSUB_PAIR_SUBSET(r, cj->progeny[pjd], parts, ind, count,
+                          ci->progeny[pid],
+                          /*gettimer=*/0);
     }
 
-    /* Otherwise, compute self-interaction. */
-    else
-      DOSELF_SUBSET_BRANCH(r, ci, parts, ind, count);
-  } /* self-interaction. */
+  }
+  /* Otherwise, compute the pair directly. */
+  else if (cell_is_active_hydro(ci, e)) {
 
-  /* Otherwise, it's a pair interaction. */
-  else {
+    /* Do any of the cells need to be drifted first? */
+    if (!cell_are_part_drifted(cj, e)) error("Cell should be drifted!");
 
-    /* Recurse? */
-    if (cell_can_recurse_in_pair_hydro_task(ci) &&
-        cell_can_recurse_in_pair_hydro_task(cj)) {
-
-      /* Get the type of pair and flip ci/cj if needed. */
-      double shift[3] = {0.0, 0.0, 0.0};
-      const int sid = space_getsid(s, &ci, &cj, shift);
-
-      struct cell_split_pair *csp = &cell_split_pairs[sid];
-      for (int k = 0; k < csp->count; k++) {
-        const int pid = csp->pairs[k].pid;
-        const int pjd = csp->pairs[k].pjd;
-        if (ci->progeny[pid] == sub && cj->progeny[pjd] != NULL)
-          DOSUB_SUBSET(r, ci->progeny[pid], parts, ind, count, cj->progeny[pjd],
-                       0);
-        if (ci->progeny[pid] != NULL && cj->progeny[pjd] == sub)
-          DOSUB_SUBSET(r, cj->progeny[pjd], parts, ind, count, ci->progeny[pid],
-                       0);
-      }
-    }
+    DOPAIR_SUBSET_BRANCH(r, ci, parts, ind, count, cj);
+  }
 
-    /* Otherwise, compute the pair directly. */
-    else if (cell_is_active_hydro(ci, e) || cell_is_active_hydro(cj, e)) {
+  if (gettimer) TIMER_TOC(timer_dosub_subset);
+}
 
-      /* Do any of the cells need to be drifted first? */
-      if (!cell_are_part_drifted(cj, e)) error("Cell should be drifted!");
+void DOSUB_SELF_SUBSET(struct runner *r, struct cell *ci, struct part *parts,
+                       const int *ind, const int count, const int gettimer) {
 
-      DOPAIR_SUBSET_BRANCH(r, ci, parts, ind, count, cj);
-    }
+  const struct engine *e = r->e;
 
-  } /* otherwise, pair interaction. */
+  /* Should we even bother? */
+  if (ci->hydro.count == 0) return;
+  if (!cell_is_active_hydro(ci, e)) return;
 
-  if (gettimer) TIMER_TOC(timer_dosub_subset);
+  /* Recurse? */
+  if (ci->split && cell_can_recurse_in_self_hydro_task(ci)) {
+
+    /* Find in which sub-cell of ci the particles are */
+    struct cell *const sub = FIND_SUB(ci, parts, ind);
+
+    /* Loop over all progeny. */
+    DOSUB_SELF_SUBSET(r, sub, parts, ind, count, /*gettimer=*/0);
+    for (int j = 0; j < 8; j++)
+      if (ci->progeny[j] != sub && ci->progeny[j] != NULL)
+        DOSUB_PAIR_SUBSET(r, sub, parts, ind, count, ci->progeny[j],
+                          /*gettimer=*/0);
+  }
+
+  /* Otherwise, compute self-interaction. */
+  else
+    DOSELF_SUBSET_BRANCH(r, ci, parts, ind, count);
 }
diff --git a/src/runner_doiact_functions_limiter.h b/src/runner_doiact_functions_limiter.h
index ee60c7d322457f0abe4044cf8bb2bdd5e115c0ea..90f2d1ffe6e139a18617f1df6befcde0b3ce2ba2 100644
--- a/src/runner_doiact_functions_limiter.h
+++ b/src/runner_doiact_functions_limiter.h
@@ -36,7 +36,8 @@
  * @param cj The second #cell.
  */
 void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
-                   struct cell *restrict cj) {
+                   struct cell *restrict cj, const int limit_min_h,
+                   const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -46,14 +47,28 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
   /* Anything to do here? */
   if (!cell_is_starting_hydro(ci, e) && !cell_is_starting_hydro(cj, e)) return;
 
+  /* Cosmological terms */
+  const float a = cosmo->a;
+  const float H = cosmo->H;
+
   const int count_i = ci->hydro.count;
   const int count_j = cj->hydro.count;
   struct part *restrict parts_i = ci->hydro.parts;
   struct part *restrict parts_j = cj->hydro.parts;
 
-  /* Cosmological terms */
-  const float a = cosmo->a;
-  const float H = cosmo->H;
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci->dmin != cj->dmin) error("Cells of different size!");
+#endif
+
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+#endif
 
   /* Get the relative distance between the pairs, wrapping. */
   double shift[3] = {0.0, 0.0, 0.0};
@@ -74,6 +89,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
     if (part_is_inhibited(pi, e)) continue;
 
     const int pi_active = part_is_starting(pi, e);
+    const char depth_i = pi->depth_h;
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - (cj->loc[0] + shift[0])),
@@ -85,6 +101,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
 
       /* Get a pointer to the jth particle. */
       struct part *restrict pj = &parts_j[pjd];
+      const char depth_j = pj->depth_h;
 
       /* Skip inhibited particles. */
       if (part_is_inhibited(pj, e)) continue;
@@ -108,12 +125,25 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
         error("Particle pj not drifted to current time");
 #endif
 
+      const int doi = pi_active && (r2 < hig2) && (depth_i >= min_depth) &&
+                      (depth_i <= max_depth);
+      const int doj = pj_active && (r2 < hjg2) && (depth_j >= min_depth) &&
+                      (depth_j <= max_depth);
+
       /* Hit or miss? */
-      if (r2 < hig2 && pi_active) {
+      if (doi) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
 
         IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
       }
-      if (r2 < hjg2 && pj_active) {
+      if (doj) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
 
         dx[0] = -dx[0];
         dx[1] = -dx[1];
@@ -123,7 +153,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
       }
 
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(TIMER_DOPAIR);
 }
@@ -136,7 +166,8 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
  * @param r The #runner.
  * @param c The #cell.
  */
-void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
+void DOSELF1_NAIVE(struct runner *r, const struct cell *c,
+                   const int limit_min_h, const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -151,7 +182,17 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
   const float H = cosmo->H;
 
   const int count = c->hydro.count;
-  struct part *restrict parts = c->hydro.parts;
+  struct part *parts = c->hydro.parts;
+
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? c->depth : 0;
+  const char max_depth = limit_min_h ? c->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? c->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? c->h_max_allowed : FLT_MAX;
+#endif
 
   /* Loop over the parts in ci. */
   for (int pid = 0; pid < count; pid++) {
@@ -163,6 +204,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
     if (part_is_inhibited(pi, e)) continue;
 
     const int pi_active = part_is_starting(pi, e);
+    const char depth_i = pi->depth_h;
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - c->loc[0]),
@@ -181,6 +223,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
       const int pj_active = part_is_starting(pj, e);
+      const char depth_j = pj->depth_h;
 
       /* Compute the pairwise distance. */
       const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
@@ -189,8 +232,10 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
       float dx[3] = {pix[0] - pjx[0], pix[1] - pjx[1], pix[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-      const int doi = pi_active && (r2 < hig2);
-      const int doj = pj_active && (r2 < hjg2);
+      const int doi = pi_active && (r2 < hig2) && (depth_i >= min_depth) &&
+                      (depth_i <= max_depth);
+      const int doj = pj_active && (r2 < hjg2) && (depth_j >= min_depth) &&
+                      (depth_j <= max_depth);
 
 #ifdef SWIFT_DEBUG_CHECKS
       /* Check that particles have been drifted to the current time */
@@ -203,12 +248,25 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
       /* Hit or miss? */
       if (doi && doj) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT(r2, dx, hi, hj, pi, pj, a, H);
       } else if (doi) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
       } else if (doj) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hj < h_min || hj >= h_max) error("Inappropriate h for this level!");
+#endif
+
         dx[0] = -dx[0];
         dx[1] = -dx[1];
         dx[2] = -dx[2];
@@ -216,7 +274,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
         IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(TIMER_DOSELF);
 }
@@ -230,8 +288,9 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
  * @param sid The direction of the pair.
  * @param shift The shift vector to apply to the particles in ci.
  */
-void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
-             const double *shift) {
+void DOPAIR1(struct runner *r, const struct cell *restrict ci,
+             const struct cell *restrict cj, const int limit_min_h,
+             const int limit_max_h, const int sid, const double shift[3]) {
 
   const struct engine *restrict e = r->e;
   const struct cosmology *restrict cosmo = e->cosmology;
@@ -259,9 +318,20 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
       2. * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
 #endif /* SWIFT_DEBUG_CHECKS */
 
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+  /* Get the limits in h (if any). Note ci and cj are the same size */
+#ifdef SWIFT_DEBUG_CHECKS
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+#endif
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+
   /* Get some other useful values. */
-  const double hi_max = ci->hydro.h_max * kernel_gamma - rshift;
-  const double hj_max = cj->hydro.h_max * kernel_gamma;
+  const double hi_max =
+      min(h_max, ci->hydro.h_max_active) * kernel_gamma - rshift;
+  const double hj_max = min(h_max, cj->hydro.h_max_active) * kernel_gamma;
   const int count_i = ci->hydro.count;
   const int count_j = cj->hydro.count;
   struct part *restrict parts_i = ci->hydro.parts;
@@ -276,17 +346,28 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
   if (cell_is_starting_hydro(ci, e)) {
 
-    /* Loop over the parts in ci. */
+    /* Loop over the *active* parts in ci that are within range (on the axis)
+       of any particle in cj. */
     for (int pid = count_i - 1;
          pid >= 0 && sort_i[pid].d + hi_max + dx_max > dj_min; pid--) {
 
       /* Get a hold of the ith part in ci. */
       struct part *restrict pi = &parts_i[sort_i[pid].i];
+      const char depth_i = pi->depth_h;
       const float hi = pi->h;
 
       /* Skip inactive particles */
       if (!part_is_starting(pi, e)) continue;
 
+#ifdef SWIFT_DEBUG_CHECKS
+      if (hi > ci->hydro.h_max_active)
+        error("Particle has h larger than h_max_active");
+#endif
+
+      /* Skip particles not in the range of h we care about */
+      if (depth_i < min_depth) continue;
+      if (depth_i > max_depth) continue;
+
       /* Is there anything we need to interact with ? */
       const double di = sort_i[pid].d + hi * kernel_gamma + dx_max - rshift;
       if (di < dj_min) continue;
@@ -312,7 +393,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         const float pjz = pj->x[2] - cj->loc[2];
 
         /* Compute the pairwise distance. */
-        float dx[3] = {pix - pjx, piy - pjy, piz - pjz};
+        const float dx[3] = {pix - pjx, piy - pjy, piz - pjz};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -352,11 +433,16 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         /* Hit or miss? */
         if (r2 < hig2) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the parts in ci. */
-  }     /* Cell ci is active */
+    } /* loop over the parts in ci. */
+  } /* Cell ci is active */
 
   if (cell_is_starting_hydro(cj, e)) {
 
@@ -366,11 +452,21 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
       /* Get a hold of the jth part in cj. */
       struct part *pj = &parts_j[sort_j[pjd].i];
+      const char depth_j = pj->depth_h;
       const float hj = pj->h;
 
       /* Skip inactive particles */
       if (!part_is_starting(pj, e)) continue;
 
+#ifdef SWIFT_DEBUG_CHECKS
+      if (hj > cj->hydro.h_max_active)
+        error("Particle has h larger than h_max_active");
+#endif
+
+      /* Skip particles not in the range of h we care about */
+      if (depth_j < min_depth) continue;
+      if (depth_j > max_depth) continue;
+
       /* Is there anything we need to interact with ? */
       const double dj = sort_j[pjd].d - hj * kernel_gamma - dx_max + rshift;
       if (dj - rshift > di_max) continue;
@@ -396,7 +492,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         const float piz = pi->x[2] - (cj->loc[2] + shift[2]);
 
         /* Compute the pairwise distance. */
-        float dx[3] = {pjx - pix, pjy - piy, pjz - piz};
+        const float dx[3] = {pjx - pix, pjy - piy, pjz - piz};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -436,11 +532,16 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         /* Hit or miss? */
         if (r2 < hjg2) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
         }
       } /* loop over the parts in ci. */
-    }   /* loop over the parts in cj. */
-  }     /* Cell cj is active */
+    } /* loop over the parts in cj. */
+  } /* Cell cj is active */
 
   TIMER_TOC(TIMER_DOPAIR);
 }
@@ -454,9 +555,10 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
  * @param cj #cell cj
  *
  */
-void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
+void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj,
+                    const int limit_min_h, const int limit_max_h) {
 
-  const struct engine *restrict e = r->e;
+  const struct engine *e = r->e;
 
   /* Anything to do here? */
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
@@ -468,67 +570,24 @@ void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
   if (!cell_are_part_drifted(ci, e) || !cell_are_part_drifted(cj, e))
     error("Interacting undrifted cells.");
 
-  /* Get the sort ID. */
+  /* Get the sort ID.
+   * Note: this may swap the ci and cj pointers!! */
   double shift[3] = {0.0, 0.0, 0.0};
-  const int sid = space_getsid(e->s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
 
   /* Have the cells been sorted? */
   if (!(ci->hydro.sorted & (1 << sid)) ||
       ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin)
-    error("Interacting unsorted cells.");
+    error("Interacting unsorted cells (ci).");
+
   if (!(cj->hydro.sorted & (1 << sid)) ||
       cj->hydro.dx_max_sort_old > space_maxreldx * cj->dmin)
-    error("Interacting unsorted cells.");
-
-#ifdef SWIFT_DEBUG_CHECKS
-  /* Pick-out the sorted lists. */
-  const struct sort_entry *restrict sort_i = cell_get_hydro_sorts(ci, sid);
-  const struct sort_entry *restrict sort_j = cell_get_hydro_sorts(cj, sid);
-
-  /* Check that the dx_max_sort values in the cell are indeed an upper
-     bound on particle movement. */
-  for (int pid = 0; pid < ci->hydro.count; pid++) {
-    const struct part *p = &ci->hydro.parts[sort_i[pid].i];
-    if (part_is_inhibited(p, e)) continue;
-
-    const float d = p->x[0] * runner_shift[sid][0] +
-                    p->x[1] * runner_shift[sid][1] +
-                    p->x[2] * runner_shift[sid][2];
-    if (fabsf(d - sort_i[pid].d) - ci->hydro.dx_max_sort >
-            1.0e-4 * max(fabsf(d), ci->hydro.dx_max_sort_old) &&
-        fabsf(d - sort_i[pid].d) - ci->hydro.dx_max_sort >
-            ci->width[0] * 1.0e-10)
-      error(
-          "particle shift diff exceeds dx_max_sort in cell ci. ci->nodeID=%d "
-          "cj->nodeID=%d d=%e sort_i[pid].d=%e ci->hydro.dx_max_sort=%e "
-          "ci->hydro.dx_max_sort_old=%e",
-          ci->nodeID, cj->nodeID, d, sort_i[pid].d, ci->hydro.dx_max_sort,
-          ci->hydro.dx_max_sort_old);
-  }
-  for (int pjd = 0; pjd < cj->hydro.count; pjd++) {
-    const struct part *p = &cj->hydro.parts[sort_j[pjd].i];
-    if (part_is_inhibited(p, e)) continue;
-
-    const float d = p->x[0] * runner_shift[sid][0] +
-                    p->x[1] * runner_shift[sid][1] +
-                    p->x[2] * runner_shift[sid][2];
-    if ((fabsf(d - sort_j[pjd].d) - cj->hydro.dx_max_sort) >
-            1.0e-4 * max(fabsf(d), cj->hydro.dx_max_sort_old) &&
-        (fabsf(d - sort_j[pjd].d) - cj->hydro.dx_max_sort) >
-            cj->width[0] * 1.0e-10)
-      error(
-          "particle shift diff exceeds dx_max_sort in cell cj. cj->nodeID=%d "
-          "ci->nodeID=%d d=%e sort_j[pjd].d=%e cj->hydro.dx_max_sort=%e "
-          "cj->hydro.dx_max_sort_old=%e",
-          cj->nodeID, ci->nodeID, d, sort_j[pjd].d, cj->hydro.dx_max_sort,
-          cj->hydro.dx_max_sort_old);
-  }
-#endif /* SWIFT_DEBUG_CHECKS */
+    error("Interacting unsorted cells (cj).");
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
-  DOPAIR1_NAIVE(r, ci, cj);
+  DOPAIR1_NAIVE(r, ci, cj, limit_min_h, limit_max_h);
 #else
-  DOPAIR1(r, ci, cj, sid, shift);
+  DOPAIR1(r, ci, cj, limit_min_h, limit_max_h, sid, shift);
 #endif
 }
 
@@ -538,56 +597,81 @@ void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
  * @param r The #runner.
  * @param c The #cell.
  */
-void DOSELF1(struct runner *r, struct cell *restrict c) {
+void DOSELF1(struct runner *r, const struct cell *c, const int limit_min_h,
+             const int limit_max_h) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
 
   TIMER_TIC;
 
-  struct part *restrict parts = c->hydro.parts;
+  struct part *parts = c->hydro.parts;
   const int count = c->hydro.count;
 
-  /* Set up indt. */
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? c->depth : 0;
+  const char max_depth = limit_min_h ? c->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? c->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? c->h_max_allowed : FLT_MAX;
+#endif
+
+  /* Set up a list of the particles for which we want to compute interactions */
   int *indt = NULL;
   int countdt = 0, firstdt = 0;
   if (posix_memalign((void **)&indt, VEC_SIZE * sizeof(int),
                      count * sizeof(int)) != 0)
     error("Failed to allocate indt.");
-  for (int k = 0; k < count; k++)
-    if (part_is_starting(&parts[k], e)) {
+  for (int k = 0; k < count; k++) {
+    const struct part *p = &parts[k];
+    const char depth = p->depth_h;
+    if (part_is_starting(&parts[k], e) && (depth >= min_depth) &&
+        (depth <= max_depth)) {
       indt[countdt] = k;
       countdt += 1;
     }
+  }
 
   /* Cosmological terms */
   const float a = cosmo->a;
   const float H = cosmo->H;
 
-  /* Loop over the particles in the cell. */
+  /* Loop over *all* the particles (i.e. the ones to update and not to update).
+   *
+   * Note the additional condition to make the loop abort if all the active
+   * particles have been processed. */
   for (int pid = 0; pid < count; pid++) {
 
     /* Get a pointer to the ith particle. */
     struct part *restrict pi = &parts[pid];
+    const char depth_i = pi->depth_h;
 
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
 
-    /* Get the particle position and radius. */
-    double pix[3];
-    for (int k = 0; k < 3; k++) pix[k] = pi->x[k];
+    /* Get the particle position and (square of) search radius. */
+    const double pix[3] = {pi->x[0], pi->x[1], pi->x[2]};
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
 
-    /* Is the ith particle inactive? */
-    if (!part_is_starting(pi, e)) {
+    /* Is the ith particle active and in the range of h we care about? */
+    const int update_i = part_is_starting(pi, e) && (depth_i >= min_depth) &&
+                         (depth_i <= max_depth);
+
+    /* If false then it can only act as a neighbour of others */
+    if (!update_i) {
 
-      /* Loop over the other particles .*/
+      /* Loop over the particles we want to update. */
       for (int pjd = firstdt; pjd < countdt; pjd++) {
 
-        /* Get a pointer to the jth particle. */
+        /* Get a pointer to the jth particle. (by construction pi != pj) */
         struct part *restrict pj = &parts[indt[pjd]];
+
+        /* This particle's (square of) search radius. */
         const float hj = pj->h;
+        const float hjg2 = hj * hj * kernel_gamma2;
 
 #ifdef SWIFT_DEBUG_CHECKS
         /* Check that particles have been drifted to the current time */
@@ -597,50 +681,48 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
           error("Particle pj not drifted to current time");
 #endif
 
-        /* Compute the pairwise distance. */
-        float r2 = 0.0f;
-        float dx[3];
-        for (int k = 0; k < 3; k++) {
-          dx[k] = pj->x[k] - pix[k];
-          r2 += dx[k] * dx[k];
-        }
+        /* Compute the (square of) pairwise distance. */
+        const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+        const float dx[3] = {(float)(pjx[0] - pix[0]), (float)(pjx[1] - pix[1]),
+                             (float)(pjx[2] - pix[2])};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
         /* Hit or miss? */
-        if (r2 < hj * hj * kernel_gamma2) {
+        if (r2 < hjg2) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
 
           IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
         }
-      } /* loop over all other particles. */
+      } /* loop over all the particles we want to update. */
     }
 
     /* Otherwise, interact with all candidates. */
     else {
 
-      /* We caught a live one! */
+      /* We caught a live one!
+       * Move the start of the list of active ones by one slot as it will have
+       * been fully processed after the following loop so no need to consider it
+       * in the previous loop any more. */
       firstdt += 1;
 
-      /* Loop over the other particles .*/
+      /* Loop over *all* the particles (i.e. the ones to update and not to
+       * update) but starting from where we are in the overall list. */
       for (int pjd = pid + 1; pjd < count; pjd++) {
 
-        /* Get a pointer to the jth particle. */
+        /* Get a pointer to the jth particle (by construction pi != pj). */
         struct part *restrict pj = &parts[pjd];
+        const char depth_j = pj->depth_h;
 
         /* Skip inhibited particles. */
         if (part_is_inhibited(pj, e)) continue;
 
+        /* This particle's (square of) search radius. */
         const float hj = pj->h;
-
-        /* Compute the pairwise distance. */
-        float r2 = 0.0f;
-        float dx[3];
-        for (int k = 0; k < 3; k++) {
-          dx[k] = pix[k] - pj->x[k];
-          r2 += dx[k] * dx[k];
-        }
-        const int doj =
-            (part_is_starting(pj, e)) && (r2 < hj * hj * kernel_gamma2);
-
-        const int doi = (r2 < hig2);
+        const float hjg2 = hj * hj * kernel_gamma2;
 
 #ifdef SWIFT_DEBUG_CHECKS
         /* Check that particles have been drifted to the current time */
@@ -650,26 +732,64 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
           error("Particle pj not drifted to current time");
 #endif
 
+        /* Compute the (square of) pairwise distance. */
+        const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+        float dx[3] = {(float)(pix[0] - pjx[0]), (float)(pix[1] - pjx[1]),
+                       (float)(pix[2] - pjx[2])};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+        /* Decide which of the two particles to update */
+
+        /* We know pi is active and in the right range of h
+         * -> Only check the distance to pj */
+        const int doi = (r2 < hig2);
+
+        /* We know nothing about pj
+         * -> Check whether it is active
+         * -> Check whether it is in the right range of h
+         * -> Check the distance to pi */
+        const int doj = (part_is_starting(pj, e)) && (depth_j >= min_depth) &&
+                        (depth_j <= max_depth) && (r2 < hjg2);
+
         /* Hit or miss? */
-        if (doi || doj) {
+        if (doi && doj) {
 
-          /* Which parts need to be updated? */
-          if (doi && doj) {
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+          /* Update both pi and pj */
 
-            IACT(r2, dx, hi, hj, pi, pj, a, H);
-          } else if (doi) {
+          IACT(r2, dx, hi, hj, pi, pj, a, H);
+        } else if (doi) {
 
-            IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
-          } else if (doj) {
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+#endif
 
-            dx[0] = -dx[0];
-            dx[1] = -dx[1];
-            dx[2] = -dx[2];
-            IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
-          }
-        }
+          /* Update only pi */
+
+          IACT_NONSYM(r2, dx, hi, hj, pi, pj, a, H);
+        } else if (doj) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
+          /* Update only pj */
+
+          dx[0] = -dx[0];
+          dx[1] = -dx[1];
+          dx[2] = -dx[2];
+          IACT_NONSYM(r2, dx, hj, hi, pj, pi, a, H);
+
+        } /* Hit or miss */
       } /* loop over all other particles. */
-    }
+    } /* pi is active */
   } /* loop over all particles. */
 
   free(indt);
@@ -685,7 +805,8 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
  * @param c #cell c
  *
  */
-void DOSELF1_BRANCH(struct runner *r, struct cell *c) {
+void DOSELF1_BRANCH(struct runner *r, const struct cell *c,
+                    const int limit_min_h, const int limit_max_h) {
 
   const struct engine *restrict e = r->e;
 
@@ -695,17 +816,25 @@ void DOSELF1_BRANCH(struct runner *r, struct cell *c) {
   /* Anything to do here? */
   if (!cell_is_starting_hydro(c, e)) return;
 
+#ifdef SWIFT_DEBUG_CHECKS
+
   /* Did we mess up the recursion? */
   if (c->hydro.h_max_old * kernel_gamma > c->dmin)
-    error("Cell smaller than smoothing length");
+    if (!limit_max_h && c->hydro.h_max_active * kernel_gamma > c->dmin)
+      error("Cell smaller than smoothing length");
+
+  /* Did we mess up the recursion? */
+  if (limit_min_h && !limit_max_h)
+    error("Fundamental error in the recursion logic");
+#endif
 
   /* Check that cells are drifted. */
   if (!cell_are_part_drifted(c, e)) error("Interacting undrifted cell.");
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
-  DOSELF1_NAIVE(r, c);
+  DOSELF1_NAIVE(r, c, limit_min_h, limit_max_h);
 #else
-  DOSELF1(r, c);
+  DOSELF1(r, c, limit_min_h, limit_max_h);
 #endif
 }
 
@@ -721,7 +850,7 @@ void DOSELF1_BRANCH(struct runner *r, struct cell *c) {
  * redundant computations to find the sid on-the-fly.
  */
 void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
-                 int gettimer) {
+                 int recurse_below_h_max, const int gettimer) {
 
   struct space *s = r->e->s;
   const struct engine *e = r->e;
@@ -730,54 +859,89 @@ void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
 
   /* Get the type of pair and flip ci/cj if needed. */
   double shift[3];
-  const int sid = space_getsid(s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
 
   /* Should we even bother? */
-  const int do_i = cell_get_flag(ci, cell_flag_do_hydro_limiter);
-  const int do_j = cell_get_flag(cj, cell_flag_do_hydro_limiter);
-  const int do_sub_i = cell_get_flag(ci, cell_flag_do_hydro_sub_limiter);
-  const int do_sub_j = cell_get_flag(cj, cell_flag_do_hydro_sub_limiter);
+  /* const int do_i = cell_get_flag(ci, cell_flag_do_hydro_limiter); */
+  /* const int do_j = cell_get_flag(cj, cell_flag_do_hydro_limiter); */
+  /* const int do_sub_i = cell_get_flag(ci, cell_flag_do_hydro_sub_limiter); */
+  /* const int do_sub_j = cell_get_flag(cj, cell_flag_do_hydro_sub_limiter); */
 
-  if (!do_i && !do_j && !do_sub_i && !do_sub_j) return;
+  /* if (!do_i && !do_j && !do_sub_i && !do_sub_j) return; */
   if (!cell_is_starting_hydro(ci, e) && !cell_is_starting_hydro(cj, e)) return;
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
 
-  /* Recurse? */
-  if (cell_can_recurse_in_pair_hydro_task(ci) &&
-      cell_can_recurse_in_pair_hydro_task(cj)) {
-    struct cell_split_pair *csp = &cell_split_pairs[sid];
-    for (int k = 0; k < csp->count; k++) {
-      const int pid = csp->pairs[k].pid;
-      const int pjd = csp->pairs[k].pjd;
-      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL)
-        DOSUB_PAIR1(r, ci->progeny[pid], cj->progeny[pjd], 0);
+  /* We reached a leaf OR a cell small enough to be processed quickly */
+  if (!ci->split || ci->hydro.count < space_recurse_size_pair_hydro ||
+      !cj->split || cj->hydro.count < space_recurse_size_pair_hydro) {
+
+    /* Do any of the cells need to be sorted first?
+     * Since h_max might have changed, we may not have sorted at this level */
+    if (!(ci->hydro.sorted & (1 << sid)) ||
+        ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
+      /* Bert: RT probably broken here! */
+      runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                           /*rt_request=*/0, /*clock=*/0);
+    }
+    if (!(cj->hydro.sorted & (1 << sid)) ||
+        cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+      /* Bert: RT probably broken here! */
+      runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                           /*rt_request=*/0, /*clock=*/0);
     }
-  }
 
-  /* Otherwise, compute the pair directly. */
-  else if ((cell_is_starting_hydro(ci, e) && (do_i || do_sub_i)) ||
-           (cell_is_starting_hydro(cj, e) && (do_j || do_sub_j))) {
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOPAIR1_BRANCH(r, ci, cj, /*limit_h_min=*/0,
+                   /*limit_h_max=*/recurse_below_h_max);
 
-    /* Make sure both cells are drifted to the current timestep. */
-    if (!cell_are_part_drifted(ci, e) || !cell_are_part_drifted(cj, e))
-      error("Interacting undrifted cells.");
+  } else {
 
-    /* Do any of the cells need to be sorted first? */
-    if (!(ci->hydro.sorted & (1 << sid)) ||
-        ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx)
-      error(
-          "Interacting unsorted cell. ci->hydro.dx_max_sort_old=%e ci->dmin=%e "
-          "ci->sorted=%d sid=%d",
-          ci->hydro.dx_max_sort_old, ci->dmin, ci->hydro.sorted, sid);
-    if (!(cj->hydro.sorted & (1 << sid)) ||
-        cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx)
-      error(
-          "Interacting unsorted cell. cj->hydro.dx_max_sort_old=%e cj->dmin=%e "
-          "cj->sorted=%d sid=%d",
-          cj->hydro.dx_max_sort_old, cj->dmin, cj->hydro.sorted, sid);
-
-    /* Compute the interactions. */
-    DOPAIR1_BRANCH(r, ci, cj);
+    /* Both ci and cj are split */
+
+    /* Should we change the recursion regime because we encountered a large
+       particle? */
+    if (!recurse_below_h_max && (!cell_can_recurse_in_subpair_hydro_task(ci) ||
+                                 !cell_can_recurse_in_subpair_hydro_task(cj))) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
+
+      /* Do any of the cells need to be sorted first?
+       * Since h_max might have changed, we may not have sorted at this level */
+      if (!(ci->hydro.sorted & (1 << sid)) ||
+          ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
+      }
+      if (!(cj->hydro.sorted & (1 << sid)) ||
+          cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
+      }
+
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOPAIR1_BRANCH(r, ci, cj, /*limit_h_min=*/1, /*limit_h_max=*/1);
+    }
+
+    /* Recurse to the lower levels. */
+    const struct cell_split_pair *const csp = &cell_split_pairs[sid];
+    for (int k = 0; k < csp->count; k++) {
+      const int pid = csp->pairs[k].pid;
+      const int pjd = csp->pairs[k].pjd;
+      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL) {
+        DOSUB_PAIR1(r, ci->progeny[pid], cj->progeny[pjd], recurse_below_h_max,
+                    /*gettimer=*/0);
+      }
+    }
   }
 
   if (gettimer) TIMER_TOC(TIMER_DOSUB_PAIR);
@@ -790,38 +954,58 @@ void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
  * @param ci The first #cell.
  * @param gettimer Do we have a timer ?
  */
-void DOSUB_SELF1(struct runner *r, struct cell *ci, int gettimer) {
+void DOSUB_SELF1(struct runner *r, struct cell *c, int recurse_below_h_max,
+                 const int gettimer) {
 
   TIMER_TIC;
 
   /* Should we even bother? */
-  const int do_i = cell_get_flag(ci, cell_flag_do_hydro_limiter);
-  const int do_sub_i = cell_get_flag(ci, cell_flag_do_hydro_sub_limiter);
-
-  if (!do_i && !do_sub_i) return;
-  if (!cell_is_starting_hydro(ci, r->e)) return;
-  if (ci->hydro.count == 0) return;
-
-  /* Recurse? */
-  if (cell_can_recurse_in_self_hydro_task(ci)) {
-
-    /* Loop over all progeny. */
-    for (int k = 0; k < 8; k++)
-      if (ci->progeny[k] != NULL) {
-        DOSUB_SELF1(r, ci->progeny[k], 0);
-        for (int j = k + 1; j < 8; j++)
-          if (ci->progeny[j] != NULL)
-            DOSUB_PAIR1(r, ci->progeny[k], ci->progeny[j], 0);
-      }
-  }
+  /* const int do_i = cell_get_flag(c, cell_flag_do_hydro_limiter); */
+  /* const int do_sub_i = cell_get_flag(c, cell_flag_do_hydro_sub_limiter); */
+
+  /* if (!do_i && !do_sub_i) return; */
+  if (!cell_is_starting_hydro(c, r->e)) return;
+  if (c->hydro.count == 0) return;
+
+  /* We reached a leaf OR a cell small enough to process quickly */
+  if (!c->split || c->hydro.count < space_recurse_size_self_hydro) {
 
-  /* Otherwise, compute self-interaction. */
-  else {
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOSELF1_BRANCH(r, c, /*limit_h_min=*/0,
+                   /*limit_h_max=*/recurse_below_h_max);
 
-    /* Drift the cell to the current timestep if needed. */
-    if (!cell_are_part_drifted(ci, r->e)) error("Interacting undrifted cell.");
+  } else {
 
-    DOSELF1_BRANCH(r, ci);
+    /* Should we change the recursion regime because we encountered a large
+       particle at this level? */
+    if (!recurse_below_h_max && !cell_can_recurse_in_subself_hydro_task(c)) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
+
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOSELF1_BRANCH(r, c, /*limit_h_min=*/1, /*limit_h_max=*/1);
+    }
+
+    /* Recurse to the lower levels. */
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL) {
+        DOSUB_SELF1(r, c->progeny[k], recurse_below_h_max, /*gettimer=*/0);
+        for (int j = k + 1; j < 8; j++) {
+          if (c->progeny[j] != NULL) {
+            DOSUB_PAIR1(r, c->progeny[k], c->progeny[j], recurse_below_h_max,
+                        /*gettimer=*/0);
+          }
+        }
+      }
+    }
   }
 
   if (gettimer) TIMER_TOC(TIMER_DOSUB_SELF);
diff --git a/src/runner_doiact_functions_sinks.h b/src/runner_doiact_functions_sinks.h
new file mode 100644
index 0000000000000000000000000000000000000000..274fbbcba1df5fae174c09d9f27a63a1275a4901
--- /dev/null
+++ b/src/runner_doiact_functions_sinks.h
@@ -0,0 +1,847 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#include "runner_doiact_sinks.h"
+
+/**
+ * @brief Calculate gas and sink interaction around #sink
+ *
+ * @param r runner task
+ * @param c cell
+ * @param timer 1 if the time is to be recorded.
+ */
+void DOSELF1_SINKS(struct runner *r, struct cell *c, int timer) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c->nodeID != engine_rank) error("Should be run on a different node");
+#endif
+
+  TIMER_TIC;
+
+  const struct engine *e = r->e;
+  const struct cosmology *cosmo = e->cosmology;
+  const int with_cosmology = e->policy & engine_policy_cosmology;
+
+  /* Anything to do here? */
+  if (c->sinks.count == 0) return;
+  if (!cell_is_active_sinks(c, e)) return;
+
+  const int scount = c->sinks.count;
+  const int count = c->hydro.count;
+  struct sink *restrict sinks = c->sinks.parts;
+  struct part *restrict parts = c->hydro.parts;
+
+  /* Do we actually have any gas neighbours? */
+  if (c->hydro.count != 0) {
+
+    /* Loop over the sinks in ci. */
+    for (int sid = 0; sid < scount; sid++) {
+
+      /* Get a hold of the ith sinks in ci. */
+      struct sink *restrict si = &sinks[sid];
+
+      /* Skip inactive particles */
+      if (!sink_is_active(si, e)) continue;
+
+      const float hi = si->h;
+      const float hig2 = hi * hi * kernel_gamma2;
+      const float six[3] = {(float)(si->x[0] - c->loc[0]),
+                            (float)(si->x[1] - c->loc[1]),
+                            (float)(si->x[2] - c->loc[2])};
+
+      /* Loop over the parts (gas) in cj. */
+      for (int pjd = 0; pjd < count; pjd++) {
+
+        /* Get a pointer to the jth particle. */
+        struct part *restrict pj = &parts[pjd];
+        const float hj = pj->h;
+
+        /* Early abort? */
+        if (part_is_inhibited(pj, e)) continue;
+
+        /* Compute the pairwise distance. */
+        const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
+                              (float)(pj->x[1] - c->loc[1]),
+                              (float)(pj->x[2] - c->loc[2])};
+        const float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+#ifdef SWIFT_DEBUG_CHECKS
+        /* Check that particles have been drifted to the current time */
+        if (si->ti_drift != e->ti_current)
+          error("Particle si not drifted to current time");
+        if (pj->ti_drift != e->ti_current)
+          error("Particle pj not drifted to current time");
+#endif
+
+        if (r2 < hig2) {
+          IACT_SINKS_GAS(r2, dx, hi, hj, si, pj, with_cosmology, cosmo,
+                         e->gravity_properties, e->sink_properties,
+                         e->ti_current, e->time);
+        }
+      } /* loop over the parts in ci. */
+    } /* loop over the sinks in ci. */
+  } /* Do we have gas particles in the cell? */
+
+  /* When doing sink swallowing, we need a quick loop also over the sink
+   * neighbours */
+#if (FUNCTION_TASK_LOOP == TASK_LOOP_SWALLOW)
+
+  /* Loop over the sinks in ci. */
+  for (int sid = 0; sid < scount; sid++) {
+
+    /* Get a hold of the ith sink in ci. */
+    struct sink *restrict si = &sinks[sid];
+
+    /* Skip inactive particles */
+    if (!sink_is_active(si, e)) continue;
+
+    const float hi = si->h;
+    const float hig2 = hi * hi * kernel_gamma2;
+    const float six[3] = {(float)(si->x[0] - c->loc[0]),
+                          (float)(si->x[1] - c->loc[1]),
+                          (float)(si->x[2] - c->loc[2])};
+
+    /* Loop over the sinks in cj. */
+    for (int sjd = 0; sjd < scount; sjd++) {
+
+      /* Skip self interaction */
+      if (sid == sjd) continue;
+
+      /* Get a pointer to the jth particle. */
+      struct sink *restrict sj = &sinks[sjd];
+      const float hj = sj->h;
+      const float hjg2 = hj * hj * kernel_gamma2;
+
+      /* Early abort? */
+      if (sink_is_inhibited(sj, e)) continue;
+
+      /* Compute the pairwise distance. */
+      const float sjx[3] = {(float)(sj->x[0] - c->loc[0]),
+                            (float)(sj->x[1] - c->loc[1]),
+                            (float)(sj->x[2] - c->loc[2])};
+      const float dx[3] = {six[0] - sjx[0], six[1] - sjx[1], six[2] - sjx[2]};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+#ifdef SWIFT_DEBUG_CHECKS
+      /* Check that particles have been drifted to the current time */
+      if (si->ti_drift != e->ti_current)
+        error("Particle si not drifted to current time");
+      if (sj->ti_drift != e->ti_current)
+        error("Particle bj not drifted to current time");
+#endif
+
+      if (r2 < hig2 || r2 < hjg2) {
+        IACT_SINKS_SINK(r2, dx, hi, hj, si, sj, with_cosmology, cosmo,
+                        e->gravity_properties, e->sink_properties,
+                        e->ti_current, e->time);
+      }
+    } /* loop over the sinks in ci. */
+  } /* loop over the sinks in ci. */
+
+#endif /* (FUNCTION_TASK_LOOP == TASK_LOOP_SWALLOW) */
+
+  if (timer) TIMER_TOC(TIMER_DOSELF_SINKS);
+}
+
+/**
+ * @brief Calculate gas and sink interaction around #sink
+ *
+ * @param r runner task
+ * @param ci The first #cell
+ * @param cj The second #cell
+ */
+void DO_NONSYM_PAIR1_SINKS_NAIVE(struct runner *r, struct cell *restrict ci,
+                                 struct cell *restrict cj) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+#if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
+  if (ci->nodeID != engine_rank) error("Should be run on a different node");
+#endif
+#endif
+
+  const struct engine *e = r->e;
+  const struct cosmology *cosmo = e->cosmology;
+  const int with_cosmology = e->policy & engine_policy_cosmology;
+
+  /* Anything to do here? */
+  if (ci->sinks.count == 0) return;
+  if (!cell_is_active_sinks(ci, e)) return;
+
+  const int scount_i = ci->sinks.count;
+  const int count_j = cj->hydro.count;
+  struct sink *restrict sinks_i = ci->sinks.parts;
+  struct part *restrict parts_j = cj->hydro.parts;
+
+  /* Get the relative distance between the pairs, wrapping. */
+  double shift[3] = {0.0, 0.0, 0.0};
+  for (int k = 0; k < 3; k++) {
+    if (cj->loc[k] - ci->loc[k] < -e->s->dim[k] / 2)
+      shift[k] = e->s->dim[k];
+    else if (cj->loc[k] - ci->loc[k] > e->s->dim[k] / 2)
+      shift[k] = -e->s->dim[k];
+  }
+
+  /* Do we actually have any gas neighbours? */
+  if (cj->hydro.count != 0) {
+
+    /* Loop over the sinks in ci. */
+    for (int sid = 0; sid < scount_i; sid++) {
+
+      /* Get a hold of the ith sink in ci. */
+      struct sink *restrict si = &sinks_i[sid];
+
+      /* Skip inactive particles */
+      if (!sink_is_active(si, e)) continue;
+
+      const float hi = si->h;
+      const float hig2 = hi * hi * kernel_gamma2;
+      const float six[3] = {(float)(si->x[0] - (cj->loc[0] + shift[0])),
+                            (float)(si->x[1] - (cj->loc[1] + shift[1])),
+                            (float)(si->x[2] - (cj->loc[2] + shift[2]))};
+
+      /* Loop over the parts (gas) in cj. */
+      for (int pjd = 0; pjd < count_j; pjd++) {
+
+        /* Get a pointer to the jth particle. */
+        struct part *restrict pj = &parts_j[pjd];
+        const float hj = pj->h;
+
+        /* Skip inhibited particles. */
+        if (part_is_inhibited(pj, e)) continue;
+
+        /* Compute the pairwise distance. */
+        const float pjx[3] = {(float)(pj->x[0] - cj->loc[0]),
+                              (float)(pj->x[1] - cj->loc[1]),
+                              (float)(pj->x[2] - cj->loc[2])};
+        const float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
+        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+#ifdef SWIFT_DEBUG_CHECKS
+        /* Check that particles have been drifted to the current time */
+        if (si->ti_drift != e->ti_current)
+          error("Particle si not drifted to current time");
+        if (pj->ti_drift != e->ti_current)
+          error("Particle pj not drifted to current time");
+#endif
+
+        if (r2 < hig2) {
+          IACT_SINKS_GAS(r2, dx, hi, hj, si, pj, with_cosmology, cosmo,
+                         e->gravity_properties, e->sink_properties,
+                         e->ti_current, e->time);
+        }
+      } /* loop over the parts in cj. */
+    } /* loop over the sinks in ci. */
+  } /* Do we have gas particles in the cell? */
+
+  /* When doing sink swallowing, we need a quick loop also over the sinks
+   * neighbours */
+#if (FUNCTION_TASK_LOOP == TASK_LOOP_SWALLOW)
+
+  const int scount_j = cj->sinks.count;
+  struct sink *restrict sinks_j = cj->sinks.parts;
+
+  /* Loop over the sinks in ci. */
+  for (int sid = 0; sid < scount_i; sid++) {
+
+    /* Get a hold of the ith sink in ci. */
+    struct sink *restrict si = &sinks_i[sid];
+
+    /* Skip inactive particles */
+    if (!sink_is_active(si, e)) continue;
+
+    const float hi = si->h;
+    const float hig2 = hi * hi * kernel_gamma2;
+    const float six[3] = {(float)(si->x[0] - (cj->loc[0] + shift[0])),
+                          (float)(si->x[1] - (cj->loc[1] + shift[1])),
+                          (float)(si->x[2] - (cj->loc[2] + shift[2]))};
+
+    /* Loop over the sinks in cj. */
+    for (int sjd = 0; sjd < scount_j; sjd++) {
+
+      /* Get a pointer to the jth particle. */
+      struct sink *restrict sj = &sinks_j[sjd];
+      const float hj = sj->h;
+      const float hjg2 = hj * hj * kernel_gamma2;
+
+      /* Skip inhibited particles. */
+      if (sink_is_inhibited(sj, e)) continue;
+
+      /* Compute the pairwise distance. */
+      const float sjx[3] = {(float)(sj->x[0] - cj->loc[0]),
+                            (float)(sj->x[1] - cj->loc[1]),
+                            (float)(sj->x[2] - cj->loc[2])};
+      const float dx[3] = {six[0] - sjx[0], six[1] - sjx[1], six[2] - sjx[2]};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+#ifdef SWIFT_DEBUG_CHECKS
+      /* Check that particles have been drifted to the current time */
+      if (si->ti_drift != e->ti_current)
+        error("Particle si not drifted to current time");
+      if (sj->ti_drift != e->ti_current)
+        error("Particle sj not drifted to current time");
+#endif
+
+      if (r2 < hig2 || r2 < hjg2) {
+        IACT_SINKS_SINK(r2, dx, hi, hj, si, sj, with_cosmology, cosmo,
+                        e->gravity_properties, e->sink_properties,
+                        e->ti_current, e->time);
+      }
+    } /* loop over the sinks in cj. */
+  } /* loop over the sinks in ci. */
+
+#endif /* (FUNCTION_TASK_LOOP == TASK_LOOP_SWALLOW) */
+}
+
+/**
+ * @brief Calculate swallow for ci #sink part around the cj #part and sinks and
+ *                              cj #sink part around the ci #part and sinks
+ *
+ * @param r runner task
+ * @param ci The first #cell
+ * @param cj The second #cell
+ */
+void DOPAIR1_SINKS_NAIVE(struct runner *r, struct cell *restrict ci,
+                         struct cell *restrict cj, int timer) {
+
+  TIMER_TIC;
+
+#if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
+  const int do_ci_sink = ci->nodeID == r->e->nodeID;
+  const int do_cj_sink = cj->nodeID == r->e->nodeID;
+#else
+  /* The swallow task is executed on both sides */
+  const int do_ci_sink = 1;
+  const int do_cj_sink = 1;
+#endif
+
+  if (do_ci_sink) DO_NONSYM_PAIR1_SINKS_NAIVE(r, ci, cj);
+  if (do_cj_sink) DO_NONSYM_PAIR1_SINKS_NAIVE(r, cj, ci);
+
+  if (timer) TIMER_TOC(TIMER_DOPAIR_SINKS);
+}
+
+/**
+ * @brief Compute the interactions between a cell pair, but only for the
+ *      given indices in ci.
+ *
+ * Version using a brute-force algorithm.
+ *
+ * @param r The #runner.
+ * @param ci The first #cell.
+ * @param sinks_i The #sink to interact with @c cj.
+ * @param ind The list of indices of particles in @c ci to interact with.
+ * @param scount The number of particles in @c ind.
+ * @param cj The second #cell.
+ * @param shift The shift vector to apply to the particles in ci.
+ */
+void DOPAIR1_SUBSET_SINKS_NAIVE(struct runner *r, struct cell *restrict ci,
+                                struct sink *restrict sinks_i,
+                                int *restrict ind, const int scount,
+                                struct cell *restrict cj, const double *shift) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci->nodeID != engine_rank) error("Should be run on a different node");
+#endif
+
+  const struct engine *e = r->e;
+  const struct cosmology *cosmo = e->cosmology;
+  const int with_cosmology = e->policy & engine_policy_cosmology;
+  const int count_j = cj->hydro.count;
+  struct part *restrict parts_j = cj->hydro.parts;
+
+  /* Early abort? */
+  if (count_j == 0) return;
+
+  /* Loop over the parts_i. */
+  for (int sid = 0; sid < scount; sid++) {
+
+    /* Get a hold of the ith part in ci. */
+    struct sink *restrict si = &sinks_i[ind[sid]];
+
+    const double six = si->x[0] - (shift[0]);
+    const double siy = si->x[1] - (shift[1]);
+    const double siz = si->x[2] - (shift[2]);
+    const float hi = si->h;
+    const float hig2 = hi * hi * kernel_gamma2;
+
+#ifdef SWIFT_DEBUG_CHECKS
+    if (!sink_is_active(si, e))
+      error("Trying to correct smoothing length of inactive particle !");
+#endif
+
+    /* Loop over the parts in cj. */
+    for (int pjd = 0; pjd < count_j; pjd++) {
+
+      /* Get a pointer to the jth particle. */
+      struct part *restrict pj = &parts_j[pjd];
+
+      /* Skip inhibited particles */
+      if (part_is_inhibited(pj, e)) continue;
+
+      const double pjx = pj->x[0];
+      const double pjy = pj->x[1];
+      const double pjz = pj->x[2];
+      const float hj = pj->h;
+
+      /* Compute the pairwise distance. */
+      const float dx[3] = {(float)(six - pjx), (float)(siy - pjy),
+                           (float)(siz - pjz)};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+#ifdef SWIFT_DEBUG_CHECKS
+      /* Check that particles have been drifted to the current time */
+      if (pj->ti_drift != e->ti_current)
+        error("Particle pj not drifted to current time");
+#endif
+      /* Hit or miss? */
+      if (r2 < hig2) {
+        IACT_SINKS_GAS(r2, dx, hi, hj, si, pj, with_cosmology, cosmo,
+                       e->gravity_properties, e->sink_properties, e->ti_current,
+                       e->time);
+      }
+    } /* loop over the parts in cj. */
+  } /* loop over the parts in ci. */
+}
+
+/**
+ * @brief Compute the interactions between a cell pair, but only for the
+ *      given indices in ci.
+ *
+ * @param r The #runner.
+ * @param ci The first #cell.
+ * @param sinks The #sink to interact.
+ * @param ind The list of indices of particles in @c ci to interact with.
+ * @param scount The number of particles in @c ind.
+ */
+void DOSELF1_SUBSET_SINKS(struct runner *r, struct cell *restrict ci,
+                          struct sink *restrict sinks, int *restrict ind,
+                          const int scount) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci->nodeID != engine_rank) error("Should be run on a different node");
+#endif
+
+  const struct engine *e = r->e;
+  const struct cosmology *cosmo = e->cosmology;
+  const int with_cosmology = e->policy & engine_policy_cosmology;
+  const int count_i = ci->hydro.count;
+  struct part *restrict parts_j = ci->hydro.parts;
+
+  /* Early abort? */
+  if (count_i == 0) return;
+
+  /* Loop over the parts in ci. */
+  for (int sid = 0; sid < scount; sid++) {
+
+    /* Get a hold of the ith part in ci. */
+    struct sink *si = &sinks[ind[sid]];
+    const float six[3] = {(float)(si->x[0] - ci->loc[0]),
+                          (float)(si->x[1] - ci->loc[1]),
+                          (float)(si->x[2] - ci->loc[2])};
+    const float hi = si->h;
+    const float hig2 = hi * hi * kernel_gamma2;
+
+#ifdef SWIFT_DEBUG_CHECKS
+    if (!sink_is_active(si, e)) error("Inactive particle in subset function!");
+#endif
+
+    /* Loop over the parts in cj. */
+    for (int pjd = 0; pjd < count_i; pjd++) {
+
+      /* Get a pointer to the jth particle. */
+      struct part *restrict pj = &parts_j[pjd];
+
+      /* Early abort? */
+      if (part_is_inhibited(pj, e)) continue;
+
+      /* Compute the pairwise distance. */
+      const float pjx[3] = {(float)(pj->x[0] - ci->loc[0]),
+                            (float)(pj->x[1] - ci->loc[1]),
+                            (float)(pj->x[2] - ci->loc[2])};
+      const float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+#ifdef SWIFT_DEBUG_CHECKS
+      /* Check that particles have been drifted to the current time */
+      if (pj->ti_drift != e->ti_current)
+        error("Particle pj not drifted to current time");
+#endif
+
+      /* Hit or miss? */
+      if (r2 < hig2) {
+        IACT_SINKS_GAS(r2, dx, hi, pj->h, si, pj, with_cosmology, cosmo,
+                       e->gravity_properties, e->sink_properties, e->ti_current,
+                       e->time);
+      }
+    } /* loop over the parts in cj. */
+  } /* loop over the parts in ci. */
+}
+
+/**
+ * @brief Determine which version of DOSELF1_SUBSET_SINKS needs to be called
+ * depending on the optimisation level.
+ *
+ * @param r The #runner.
+ * @param ci The first #cell.
+ * @param sinks The #sink to interact.
+ * @param ind The list of indices of particles in @c ci to interact with.
+ * @param scount The number of particles in @c ind.
+ */
+void DOSELF1_SUBSET_BRANCH_SINKS(struct runner *r, struct cell *restrict ci,
+                                 struct sink *restrict sinks, int *restrict ind,
+                                 const int scount) {
+
+  DOSELF1_SUBSET_SINKS(r, ci, sinks, ind, scount);
+}
+
+/**
+ * @brief Determine which version of DOPAIR1_SUBSET_SINKS needs to be called
+ * depending on the orientation of the cells or whether DOPAIR1_SUBSET_SINKS
+ * needs to be called at all.
+ *
+ * @param r The #runner.
+ * @param ci The first #cell.
+ * @param sinks_i The #sink to interact with @c cj.
+ * @param ind The list of indices of particles in @c ci to interact with.
+ * @param scount The number of particles in @c ind.
+ * @param cj The second #cell.
+ */
+void DOPAIR1_SUBSET_BRANCH_SINKS(struct runner *r, struct cell *restrict ci,
+                                 struct sink *restrict sinks_i,
+                                 int *restrict ind, int const scount,
+                                 struct cell *restrict cj) {
+
+  const struct engine *e = r->e;
+
+  /* Anything to do here? */
+  if (cj->hydro.count == 0) return;
+
+  /* Get the relative distance between the pairs, wrapping. */
+  double shift[3] = {0.0, 0.0, 0.0};
+  for (int k = 0; k < 3; k++) {
+    if (cj->loc[k] - ci->loc[k] < -e->s->dim[k] / 2)
+      shift[k] = e->s->dim[k];
+    else if (cj->loc[k] - ci->loc[k] > e->s->dim[k] / 2)
+      shift[k] = -e->s->dim[k];
+  }
+
+  DOPAIR1_SUBSET_SINKS_NAIVE(r, ci, sinks_i, ind, scount, cj, shift);
+}
+
+void DOSUB_SUBSET_SINKS(struct runner *r, struct cell *ci, struct sink *sinks,
+                        int *ind, const int scount, struct cell *cj,
+                        int gettimer) {
+
+  const struct engine *e = r->e;
+  struct space *s = e->s;
+
+  /* Should we even bother? */
+  if (!cell_is_active_sinks(ci, e) &&
+      (cj == NULL || !cell_is_active_sinks(cj, e)))
+    return;
+
+  /* Find out in which sub-cell of ci the parts are. */
+  struct cell *sub = NULL;
+  if (ci->split) {
+    for (int k = 0; k < 8; k++) {
+      if (ci->progeny[k] != NULL) {
+        if (&sinks[ind[0]] >= &ci->progeny[k]->sinks.parts[0] &&
+            &sinks[ind[0]] <
+                &ci->progeny[k]->sinks.parts[ci->progeny[k]->sinks.count]) {
+          sub = ci->progeny[k];
+          break;
+        }
+      }
+    }
+  }
+
+  /* Is this a single cell? */
+  if (cj == NULL) {
+
+    /* Recurse? */
+    if (cell_can_recurse_in_self_sinks_task(ci)) {
+
+      /* Loop over all progeny. */
+      DOSUB_SUBSET_SINKS(r, sub, sinks, ind, scount, NULL, 0);
+      for (int j = 0; j < 8; j++)
+        if (ci->progeny[j] != sub && ci->progeny[j] != NULL)
+          DOSUB_SUBSET_SINKS(r, sub, sinks, ind, scount, ci->progeny[j], 0);
+
+    }
+
+    /* Otherwise, compute self-interaction. */
+    else
+      DOSELF1_SUBSET_BRANCH_SINKS(r, ci, sinks, ind, scount);
+  } /* self-interaction. */
+
+  /* Otherwise, it's a pair interaction. */
+  else {
+
+    /* Recurse? */
+    if (cell_can_recurse_in_pair_sinks_task(ci, cj) &&
+        cell_can_recurse_in_pair_sinks_task(cj, ci)) {
+
+      /* Get the type of pair and flip ci/cj if needed. */
+      double shift[3] = {0.0, 0.0, 0.0};
+      const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
+
+      struct cell_split_pair *csp = &cell_split_pairs[sid];
+      for (int k = 0; k < csp->count; k++) {
+        const int pid = csp->pairs[k].pid;
+        const int pjd = csp->pairs[k].pjd;
+        if (ci->progeny[pid] == sub && cj->progeny[pjd] != NULL)
+          DOSUB_SUBSET_SINKS(r, ci->progeny[pid], sinks, ind, scount,
+                             cj->progeny[pjd], 0);
+        if (ci->progeny[pid] != NULL && cj->progeny[pjd] == sub)
+          DOSUB_SUBSET_SINKS(r, cj->progeny[pjd], sinks, ind, scount,
+                             ci->progeny[pid], 0);
+      }
+    }
+
+    /* Otherwise, compute the pair directly. */
+    else if (cell_is_active_sinks(ci, e) && cj->hydro.count > 0) {
+
+      /* Do any of the cells need to be drifted first? */
+      if (cell_is_active_sinks(ci, e)) {
+        if (!cell_are_sink_drifted(ci, e)) error("Cell should be drifted!");
+        if (!cell_are_part_drifted(cj, e)) error("Cell should be drifted!");
+      }
+
+      DOPAIR1_SUBSET_BRANCH_SINKS(r, ci, sinks, ind, scount, cj);
+    }
+
+  } /* otherwise, pair interaction. */
+}
+
+/**
+ * @brief Wrapper to runner_doself_sinks_swallow
+ *
+ * @param r #runner
+ * @param c #cell c
+ *
+ */
+void DOSELF1_BRANCH_SINKS(struct runner *r, struct cell *c) {
+
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
+
+  const struct engine *restrict e = r->e;
+
+  /* Anything to do here? */
+  if (c->sinks.count == 0) return;
+
+  /* Anything to do here? */
+  if (!cell_is_active_sinks(c, e)) return;
+
+  /* Did we mess up the recursion? */
+  if (c->sinks.h_max_old * kernel_gamma > c->dmin)
+    error("Cell smaller than the cut off radius or smoothing length");
+
+  DOSELF1_SINKS(r, c, 1);
+}
+
+/**
+ * @brief Wrapper for runner_dopair_sinks_naive_swallow.
+ *
+ * @param r #runner
+ * @param ci #cell ci
+ * @param cj #cell cj
+ *
+ */
+void DOPAIR1_BRANCH_SINKS(struct runner *r, struct cell *ci, struct cell *cj) {
+
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
+
+  const struct engine *restrict e = r->e;
+
+  const int ci_active = cell_is_active_sinks(ci, e);
+  const int cj_active = cell_is_active_sinks(cj, e);
+
+#if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
+  const int do_ci_sink = ci->nodeID == e->nodeID;
+  const int do_cj_sink = cj->nodeID == e->nodeID;
+#else
+  /* The swallow task is executed on both sides */
+  const int do_ci_sink = 1;
+  const int do_cj_sink = 1;
+#endif
+
+  const int do_ci =
+      (ci->sinks.count != 0 && cj->hydro.count != 0 && ci_active && do_ci_sink);
+  const int do_cj =
+      (cj->sinks.count != 0 && ci->hydro.count != 0 && cj_active && do_cj_sink);
+
+  /* Anything to do here? */
+  if (!do_ci && !do_cj) return;
+
+  /* Check that cells are drifted. */
+  if (do_ci && (!cell_are_sink_drifted(ci, e) || !cell_are_part_drifted(cj, e)))
+    error("Interacting undrifted cells.");
+
+  if (do_cj && (!cell_are_part_drifted(ci, e) || !cell_are_sink_drifted(cj, e)))
+    error("Interacting undrifted cells.");
+
+  /* No sorted interactions here -> use the naive ones */
+  DOPAIR1_SINKS_NAIVE(r, ci, cj, 1);
+}
+
+/**
+ * @brief Compute grouped sub-cell interactions for pairs
+ *
+ * @param r The #runner.
+ * @param ci The first #cell.
+ * @param cj The second #cell.
+ * @param gettimer Do we have a timer ?
+ *
+ * @todo Hard-code the sid on the recursive calls to avoid the
+ * redundant computations to find the sid on-the-fly.
+ */
+void DOSUB_PAIR1_SINKS(struct runner *r, struct cell *ci, struct cell *cj,
+                       int timer) {
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
+
+  TIMER_TIC;
+
+  struct space *s = r->e->s;
+  const struct engine *e = r->e;
+
+  /* Should we even bother? */
+  const int should_do_ci = ci->sinks.count != 0 && cell_is_active_sinks(ci, e);
+  const int should_do_cj = cj->sinks.count != 0 && cell_is_active_sinks(cj, e);
+
+  if (!should_do_ci && !should_do_cj) return;
+
+  /* Get the type of pair and flip ci/cj if needed. */
+  double shift[3];
+  const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
+
+  /* Recurse? */
+  if (cell_can_recurse_in_pair_sinks_task(ci, cj) &&
+      cell_can_recurse_in_pair_sinks_task(cj, ci)) {
+    struct cell_split_pair *csp = &cell_split_pairs[sid];
+    for (int k = 0; k < csp->count; k++) {
+      const int pid = csp->pairs[k].pid;
+      const int pjd = csp->pairs[k].pjd;
+      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL)
+        DOSUB_PAIR1_SINKS(r, ci->progeny[pid], cj->progeny[pjd], 0);
+    }
+  }
+
+  /* Otherwise, compute the pair directly. */
+  else {
+
+#if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
+    const int do_ci_sink = ci->nodeID == e->nodeID;
+    const int do_cj_sink = cj->nodeID == e->nodeID;
+#else
+    /* Here we perform the task on both sides */
+    const int do_ci_sink = 1;
+    const int do_cj_sink = 1;
+#endif
+
+    const int do_ci =
+        ci->sinks.count != 0 && cell_is_active_sinks(ci, e) && do_ci_sink;
+    const int do_cj =
+        cj->sinks.count != 0 && cell_is_active_sinks(cj, e) && do_cj_sink;
+
+    if (do_ci) {
+
+      /* Make sure both cells are drifted to the current timestep. */
+      if (!cell_are_sink_drifted(ci, e))
+        error("Interacting undrifted cells (sinks).");
+
+      if (cj->hydro.count != 0 && !cell_are_part_drifted(cj, e))
+        error("Interacting undrifted cells (parts).");
+    }
+
+    if (do_cj) {
+
+      /* Make sure both cells are drifted to the current timestep. */
+      if (ci->hydro.count != 0 && !cell_are_part_drifted(ci, e))
+        error("Interacting undrifted cells (parts).");
+
+      if (!cell_are_sink_drifted(cj, e))
+        error("Interacting undrifted cells (sinks).");
+    }
+
+    if (do_ci || do_cj) DOPAIR1_BRANCH_SINKS(r, ci, cj);
+  }
+
+  if (timer) TIMER_TOC(TIMER_DOSUB_PAIR_SINKS);
+}
+
+/**
+ * @brief Compute grouped sub-cell interactions for self tasks
+ *
+ * @param r The #runner.
+ * @param ci The first #cell.
+ * @param gettimer Do we have a timer ?
+ */
+void DOSUB_SELF1_SINKS(struct runner *r, struct cell *ci, int timer) {
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
+
+  TIMER_TIC;
+
+  const struct engine *e = r->e;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci->nodeID != engine_rank)
+    error("This function should not be called on foreign cells");
+#endif
+
+  /* Should we even bother? */
+  const int should_do_ci = ci->sinks.count != 0 && cell_is_active_sinks(ci, e);
+
+  if (!should_do_ci) return;
+
+  /* Recurse? */
+  if (cell_can_recurse_in_self_sinks_task(ci)) {
+
+    /* Loop over all progeny. */
+    for (int k = 0; k < 8; k++)
+      if (ci->progeny[k] != NULL) {
+        DOSUB_SELF1_SINKS(r, ci->progeny[k], 0);
+        for (int j = k + 1; j < 8; j++)
+          if (ci->progeny[j] != NULL)
+            DOSUB_PAIR1_SINKS(r, ci->progeny[k], ci->progeny[j], 0);
+      }
+  }
+
+  /* Otherwise, compute self-interaction. */
+  else {
+
+    /* Check we did drift to the current time */
+    if (!cell_are_sink_drifted(ci, e)) error("Interacting undrifted cell.");
+
+    if (ci->hydro.count != 0 && !cell_are_part_drifted(ci, e))
+      error("Interacting undrifted cells (parts).");
+
+    DOSELF1_BRANCH_SINKS(r, ci);
+  }
+
+  if (timer) TIMER_TOC(TIMER_DOSUB_SELF_SINKS);
+}
diff --git a/src/runner_doiact_functions_stars.h b/src/runner_doiact_functions_stars.h
index b163c6fc8a290c93238960e68dbb142283c49003..abdb0bfdca6b2cbd343189707080f7978d6c3453 100644
--- a/src/runner_doiact_functions_stars.h
+++ b/src/runner_doiact_functions_stars.h
@@ -24,6 +24,7 @@
    and runner_dosub_FUNCTION calling the pairwise interaction function
    runner_iact_FUNCTION. */
 
+#include "feedback.h"
 #include "runner_doiact_stars.h"
 
 #ifdef RT_NONE
@@ -42,9 +43,13 @@
  *
  * @param r runner task
  * @param c cell
- * @param timer 1 if the time is to be recorded.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
+ * @param offset First particle in the cell to treat (for split tasks).
+ * @param increment Interval between successive particles that are treated.
  */
-void DOSELF1_STARS(struct runner *r, struct cell *c, int timer) {
+void DOSELF1_STARS(struct runner *r, const struct cell *c,
+                   const int limit_min_h, const int limit_max_h) {
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (c->nodeID != engine_rank) error("Should be run on a different node");
@@ -74,11 +79,24 @@ void DOSELF1_STARS(struct runner *r, struct cell *c, int timer) {
 
   const int with_rt = WITH_RT;
 
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? c->depth : 0;
+  const char max_depth = limit_min_h ? c->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? c->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? c->h_max_allowed : FLT_MAX;
+#endif
+
   /* Loop over the sparts in ci. */
   for (int sid = 0; sid < scount; sid++) {
 
     /* Get a hold of the ith spart in ci. */
-    struct spart *restrict si = &sparts[sid];
+    struct spart *si = &sparts[sid];
+    const char depth_i = si->depth_h;
+    const float hi = si->h;
+    const float hig2 = hi * hi * kernel_gamma2;
 
     /* Skip inhibited particles */
     if (spart_is_inhibited(si, e)) continue;
@@ -87,11 +105,13 @@ void DOSELF1_STARS(struct runner *r, struct cell *c, int timer) {
     if (!spart_is_active(si, e)) continue;
 
     /* Skip inactive particles */
-    int si_active_feedback = feedback_is_active(si, e);
+    const int si_active_feedback = feedback_is_active(si, e);
     if (!si_active_feedback && !with_rt) continue;
 
-    const float hi = si->h;
-    const float hig2 = hi * hi * kernel_gamma2;
+    /* Skip particles not in the range of h we care about */
+    if (depth_i > max_depth) continue;
+    if (depth_i < min_depth) continue;
+
     const float six[3] = {(float)(si->x[0] - c->loc[0]),
                           (float)(si->x[1] - c->loc[1]),
                           (float)(si->x[2] - c->loc[2])};
@@ -113,7 +133,7 @@ void DOSELF1_STARS(struct runner *r, struct cell *c, int timer) {
       const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
                             (float)(pj->x[1] - c->loc[1]),
                             (float)(pj->x[2] - c->loc[2])};
-      float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
+      const float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -123,6 +143,11 @@ void DOSELF1_STARS(struct runner *r, struct cell *c, int timer) {
 #endif
 
       if (r2 < hig2 && si_active_feedback) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT_STARS(r2, dx, hi, hj, si, pj, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
         runner_iact_nonsym_feedback_density(r2, dx, hi, hj, si, pj, NULL, cosmo,
@@ -150,7 +175,7 @@ void DOSELF1_STARS(struct runner *r, struct cell *c, int timer) {
 #endif
       }
     } /* loop over the parts in ci. */
-  }   /* loop over the sparts in ci. */
+  } /* loop over the sparts in ci. */
 
   TIMER_TOC(TIMER_DOSELF_STARS);
 }
@@ -161,9 +186,15 @@ void DOSELF1_STARS(struct runner *r, struct cell *c, int timer) {
  * @param r runner task
  * @param ci The first #cell
  * @param cj The second #cell
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
+ * @param offset First particle in the cell to treat (for split tasks).
+ * @param increment Interval between successive particles that are treated.
  */
-void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
-                                 struct cell *restrict cj) {
+void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r,
+                                 const struct cell *restrict ci,
+                                 const struct cell *restrict cj,
+                                 const int limit_min_h, const int limit_max_h) {
 
 #ifdef SWIFT_DEBUG_CHECKS
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
@@ -194,6 +225,20 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
 #endif
   const int with_rt = WITH_RT;
 
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci->dmin != cj->dmin) error("Cells of different size!");
+#endif
+
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+#endif
+
   /* Get the relative distance between the pairs, wrapping. */
   double shift[3] = {0.0, 0.0, 0.0};
   for (int k = 0; k < 3; k++) {
@@ -207,7 +252,8 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
   for (int sid = 0; sid < scount_i; sid++) {
 
     /* Get a hold of the ith spart in ci. */
-    struct spart *restrict si = &sparts_i[sid];
+    struct spart *si = &sparts_i[sid];
+    const char depth_i = si->depth_h;
 
     /* Skip inhibited particles */
     if (spart_is_inhibited(si, e)) continue;
@@ -216,7 +262,7 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
     if (!spart_is_active(si, e)) continue;
 
     /* Skip inactive particles */
-    int si_active_feedback = feedback_is_active(si, e);
+    const int si_active_feedback = feedback_is_active(si, e);
     if (!si_active_feedback && !with_rt) continue;
 
     const float hi = si->h;
@@ -225,6 +271,15 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
                           (float)(si->x[1] - (cj->loc[1] + shift[1])),
                           (float)(si->x[2] - (cj->loc[2] + shift[2]))};
 
+#ifdef SWIFT_DEBUG_CHECKS
+    if (hi > ci->stars.h_max_active)
+      error("Particle has h larger than h_max_active");
+#endif
+
+    /* Skip particles not in the range of h we care about */
+    if (depth_i > max_depth) continue;
+    if (depth_i < min_depth) continue;
+
     /* Loop over the parts in cj. */
     for (int pjd = 0; pjd < count_j; pjd++) {
 
@@ -242,7 +297,7 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
       const float pjx[3] = {(float)(pj->x[0] - cj->loc[0]),
                             (float)(pj->x[1] - cj->loc[1]),
                             (float)(pj->x[2] - cj->loc[2])};
-      float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
+      const float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -252,6 +307,11 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
 #endif
 
       if (r2 < hig2 && si_active_feedback) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+        if (hi < h_min || hi >= h_max) error("Inappropriate h for this level!");
+#endif
+
         IACT_STARS(r2, dx, hi, hj, si, pj, a, H);
 
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
@@ -280,7 +340,7 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 }
 
 /**
@@ -289,11 +349,17 @@ void DO_NONSYM_PAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
  * @param r The #runner.
  * @param ci The first #cell.
  * @param cj The second #cell.
+ * @param limit_min_h Only consider particles with h >= c->dmin/2.
+ * @param limit_max_h Only consider particles with h < c->dmin.
  * @param sid The direction of the pair.
  * @param shift The shift vector to apply to the particles in ci.
+ * @param offset First particle in the cell to treat (for split tasks).
+ * @param increment Interval between successive particles that are treated.
  */
-void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
-                        const int sid, const double *shift) {
+void DO_SYM_PAIR1_STARS(struct runner *r, const struct cell *restrict ci,
+                        const struct cell *restrict cj, const int limit_min_h,
+                        const int limit_max_h, const int sid,
+                        const double shift[3]) {
 
   TIMER_TIC;
 
@@ -324,6 +390,20 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
 #endif
   const int with_rt = WITH_RT;
 
+#ifdef SWIFT_DEBUG_CHECKS
+  if (ci->dmin != cj->dmin) error("Cells of different size!");
+#endif
+
+  /* Get the depth limits (if any) */
+  const char min_depth = limit_max_h ? ci->depth : 0;
+  const char max_depth = limit_min_h ? ci->depth : CHAR_MAX;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Get the limits in h (if any) */
+  const float h_min = limit_min_h ? ci->h_min_allowed : 0.;
+#endif
+  const float h_max = limit_max_h ? ci->h_max_allowed : FLT_MAX;
+
   if (do_ci_stars) {
 
     /* Pick-out the sorted lists. */
@@ -344,24 +424,26 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
 #endif /* SWIFT_DEBUG_CHECKS */
 
     /* Get some other useful values. */
-    const double hi_max = ci->stars.h_max * kernel_gamma - rshift;
+    const double hi_max =
+        min(h_max, ci->stars.h_max_active) * kernel_gamma - rshift;
     const int count_i = ci->stars.count;
     const int count_j = cj->hydro.count;
-    struct spart *restrict sparts_i = ci->stars.parts;
-    struct part *restrict parts_j = cj->hydro.parts;
+    struct spart *sparts_i = ci->stars.parts;
+    struct part *parts_j = cj->hydro.parts;
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_FEEDBACK)
-    struct xpart *restrict xparts_j = cj->hydro.xparts;
+    struct xpart *xparts_j = cj->hydro.xparts;
 #endif
     const double dj_min = sort_j[0].d;
     const float dx_max = (ci->stars.dx_max_sort + cj->hydro.dx_max_sort);
-    const float hydro_dx_max_rshift = cj->hydro.dx_max_sort - rshift;
 
-    /* Loop over the sparts in ci. */
+    /* Loop over the *active* sparts in ci that are within range (on the axis)
+       of any particle in cj. */
     for (int pid = count_i - 1;
          pid >= 0 && sort_i[pid].d + hi_max + dx_max > dj_min; pid--) {
 
       /* Get a hold of the ith part in ci. */
-      struct spart *restrict spi = &sparts_i[sort_i[pid].i];
+      struct spart *spi = &sparts_i[sort_i[pid].i];
+      const char depth_i = spi->depth_h;
       const float hi = spi->h;
 
       /* Skip inhibited particles */
@@ -374,13 +456,17 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
       const int spi_active_feedback = feedback_is_active(spi, e);
       if (!spi_active_feedback && !with_rt) continue;
 
-      /* Compute distance from the other cell. */
-      const double px[3] = {spi->x[0], spi->x[1], spi->x[2]};
-      float dist = px[0] * runner_shift[sid][0] + px[1] * runner_shift[sid][1] +
-                   px[2] * runner_shift[sid][2];
+#ifdef SWIFT_DEBUG_CHECKS
+      if (hi > ci->stars.h_max_active)
+        error("Particle has h larger than h_max_active");
+#endif
+
+      /* Skip particles not in the range of h we care about */
+      if (depth_i > max_depth) continue;
+      if (depth_i < min_depth) continue;
 
       /* Is there anything we need to interact with ? */
-      const double di = dist + hi * kernel_gamma + hydro_dx_max_rshift;
+      const double di = sort_i[pid].d + hi * kernel_gamma + dx_max - rshift;
       if (di < dj_min) continue;
 
       /* Get some additional information about pi */
@@ -407,7 +493,7 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
         const float pjz = pj->x[2] - cj->loc[2];
 
         /* Compute the pairwise distance. */
-        float dx[3] = {pix - pjx, piy - pjy, piz - pjz};
+        const float dx[3] = {pix - pjx, piy - pjy, piz - pjz};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -446,6 +532,12 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
 
         /* Hit or miss? */
         if (r2 < hig2 && spi_active_feedback) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hi < h_min || hi >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_STARS(r2, dx, hi, hj, spi, pj, a, H);
 
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
@@ -475,8 +567,8 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
 #endif
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the parts in ci. */
-  }     /* do_ci_stars */
+    } /* loop over the parts in ci. */
+  } /* do_ci_stars */
 
   if (do_cj_stars) {
     /* Pick-out the sorted lists. */
@@ -497,7 +589,7 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
 #endif /* SWIFT_DEBUG_CHECKS */
 
     /* Get some other useful values. */
-    const double hj_max = cj->stars.h_max * kernel_gamma;
+    const double hj_max = min(h_max, cj->stars.h_max_active) * kernel_gamma;
     const int count_i = ci->hydro.count;
     const int count_j = cj->stars.count;
     struct spart *restrict sparts_j = cj->stars.parts;
@@ -507,14 +599,15 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
 #endif
     const double di_max = sort_i[count_i - 1].d - rshift;
     const float dx_max = (ci->hydro.dx_max_sort + cj->stars.dx_max_sort);
-    const float hydro_dx_max_rshift = ci->hydro.dx_max_sort - rshift;
 
-    /* Loop over the parts in cj. */
+    /* Loop over the *active* sparts in cj that are within range (on the axis)
+       of any particle in ci. */
     for (int pjd = 0; pjd < count_j && sort_j[pjd].d - hj_max - dx_max < di_max;
          pjd++) {
 
       /* Get a hold of the jth part in cj. */
       struct spart *spj = &sparts_j[sort_j[pjd].i];
+      const char depth_j = spj->depth_h;
       const float hj = spj->h;
 
       /* Skip inhibited particles */
@@ -524,16 +617,20 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
       if (!spart_is_active(spj, e)) continue;
 
       /* Skip inactive particles */
-      int spj_active_feedback = feedback_is_active(spj, e);
+      const int spj_active_feedback = feedback_is_active(spj, e);
       if (!spj_active_feedback && !with_rt) continue;
 
-      /* Compute distance from the other cell. */
-      const double px[3] = {spj->x[0], spj->x[1], spj->x[2]};
-      float dist = px[0] * runner_shift[sid][0] + px[1] * runner_shift[sid][1] +
-                   px[2] * runner_shift[sid][2];
+#ifdef SWIFT_DEBUG_CHECKS
+      if (hj > cj->stars.h_max_active)
+        error("Particle has h larger than h_max_active");
+#endif
+
+      /* Skip particles not in the range of h we care about */
+      if (depth_j > max_depth) continue;
+      if (depth_j < min_depth) continue;
 
       /* Is there anything we need to interact with ? */
-      const double dj = dist - hj * kernel_gamma - hydro_dx_max_rshift;
+      const double dj = sort_j[pjd].d - hj * kernel_gamma - dx_max + rshift;
       if (dj - rshift > di_max) continue;
 
       /* Get some additional information about pj */
@@ -560,7 +657,7 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
         const float piz = pi->x[2] - (cj->loc[2] + shift[2]);
 
         /* Compute the pairwise distance. */
-        float dx[3] = {pjx - pix, pjy - piy, pjz - piz};
+        const float dx[3] = {pjx - pix, pjy - piy, pjz - piz};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -600,6 +697,11 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
         /* Hit or miss? */
         if (r2 < hjg2 && spj_active_feedback) {
 
+#ifdef SWIFT_DEBUG_CHECKS
+          if (hj < h_min || hj >= h_max)
+            error("Inappropriate h for this level!");
+#endif
+
           IACT_STARS(r2, dx, hj, hi, spj, pi, a, H);
 
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
@@ -629,14 +731,15 @@ void DO_SYM_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
 #endif
         }
       } /* loop over the parts in ci. */
-    }   /* loop over the parts in cj. */
-  }     /* Cell cj is active */
+    } /* loop over the parts in cj. */
+  } /* Cell cj is active */
 
   TIMER_TOC(TIMER_DOPAIR_STARS);
 }
 
-void DOPAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
-                         struct cell *restrict cj, int timer) {
+void DOPAIR1_STARS_NAIVE(struct runner *r, const struct cell *restrict ci,
+                         const struct cell *restrict cj, const int limit_min_h,
+                         const int limit_max_h) {
 
   TIMER_TIC;
 
@@ -650,9 +753,9 @@ void DOPAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
   const int do_cj_stars = ci->nodeID == r->e->nodeID;
 #endif
   if (do_ci_stars && ci->stars.count != 0 && cj->hydro.count != 0)
-    DO_NONSYM_PAIR1_STARS_NAIVE(r, ci, cj);
+    DO_NONSYM_PAIR1_STARS_NAIVE(r, ci, cj, limit_min_h, limit_max_h);
   if (do_cj_stars && cj->stars.count != 0 && ci->hydro.count != 0)
-    DO_NONSYM_PAIR1_STARS_NAIVE(r, cj, ci);
+    DO_NONSYM_PAIR1_STARS_NAIVE(r, cj, ci, limit_min_h, limit_max_h);
 
   TIMER_TOC(TIMER_DOPAIR_STARS);
 }
@@ -673,10 +776,11 @@ void DOPAIR1_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
  * @param flipped Flag to check whether the cells have been flipped or not.
  * @param shift The shift vector to apply to the particles in ci.
  */
-void DOPAIR1_SUBSET_STARS(struct runner *r, struct cell *restrict ci,
-                          struct spart *restrict sparts_i, int *restrict ind,
-                          int scount, struct cell *restrict cj, const int sid,
-                          const int flipped, const double *shift) {
+void DOPAIR1_SUBSET_STARS(struct runner *r, const struct cell *restrict ci,
+                          struct spart *restrict sparts_i, const int *ind,
+                          const int scount, const struct cell *restrict cj,
+                          const int sid, const int flipped,
+                          const double shift[3]) {
 
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -755,7 +859,7 @@ void DOPAIR1_SUBSET_STARS(struct runner *r, struct cell *restrict ci,
 #endif
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the sparts in ci. */
+    } /* loop over the sparts in ci. */
   }
 
   /* Sparts are on the right. */
@@ -818,7 +922,7 @@ void DOPAIR1_SUBSET_STARS(struct runner *r, struct cell *restrict ci,
 #endif
         }
       } /* loop over the parts in cj. */
-    }   /* loop over the sparts in ci. */
+    } /* loop over the sparts in ci. */
   }
 }
 
@@ -836,10 +940,11 @@ void DOPAIR1_SUBSET_STARS(struct runner *r, struct cell *restrict ci,
  * @param cj The second #cell.
  * @param shift The shift vector to apply to the particles in ci.
  */
-void DOPAIR1_SUBSET_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
-                                struct spart *restrict sparts_i,
-                                int *restrict ind, int scount,
-                                struct cell *restrict cj, const double *shift) {
+void DOPAIR1_SUBSET_STARS_NAIVE(struct runner *r,
+                                const struct cell *restrict ci,
+                                struct spart *restrict sparts_i, const int *ind,
+                                const int scount, struct cell *restrict cj,
+                                const double shift[3]) {
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (ci->nodeID != engine_rank) error("Should be run on a different node");
@@ -916,7 +1021,7 @@ void DOPAIR1_SUBSET_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 }
 
 /**
@@ -929,9 +1034,9 @@ void DOPAIR1_SUBSET_STARS_NAIVE(struct runner *r, struct cell *restrict ci,
  * @param ind The list of indices of particles in @c ci to interact with.
  * @param scount The number of particles in @c ind.
  */
-void DOSELF1_SUBSET_STARS(struct runner *r, struct cell *restrict ci,
-                          struct spart *restrict sparts, int *restrict ind,
-                          int scount) {
+void DOSELF1_SUBSET_STARS(struct runner *r, const struct cell *ci,
+                          struct spart *restrict sparts, const int *const ind,
+                          const int scount) {
 #ifdef SWIFT_DEBUG_CHECKS
   if (ci->nodeID != engine_rank) error("Should be run on a different node");
 #endif
@@ -1003,7 +1108,7 @@ void DOSELF1_SUBSET_STARS(struct runner *r, struct cell *restrict ci,
 #endif
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 }
 
 /**
@@ -1016,9 +1121,9 @@ void DOSELF1_SUBSET_STARS(struct runner *r, struct cell *restrict ci,
  * @param ind The list of indices of particles in @c ci to interact with.
  * @param scount The number of particles in @c ind.
  */
-void DOSELF1_SUBSET_BRANCH_STARS(struct runner *r, struct cell *restrict ci,
+void DOSELF1_SUBSET_BRANCH_STARS(struct runner *r, const struct cell *ci,
                                  struct spart *restrict sparts,
-                                 int *restrict ind, int scount) {
+                                 const int *const ind, const int scount) {
 
   DOSELF1_SUBSET_STARS(r, ci, sparts, ind, scount);
 }
@@ -1035,9 +1140,10 @@ void DOSELF1_SUBSET_BRANCH_STARS(struct runner *r, struct cell *restrict ci,
  * @param scount The number of particles in @c ind.
  * @param cj The second #cell.
  */
-void DOPAIR1_SUBSET_BRANCH_STARS(struct runner *r, struct cell *restrict ci,
+void DOPAIR1_SUBSET_BRANCH_STARS(struct runner *r,
+                                 const struct cell *restrict ci,
                                  struct spart *restrict sparts_i,
-                                 int *restrict ind, int scount,
+                                 const int *ind, const int scount,
                                  struct cell *restrict cj) {
 
   const struct engine *e = r->e;
@@ -1054,13 +1160,10 @@ void DOPAIR1_SUBSET_BRANCH_STARS(struct runner *r, struct cell *restrict ci,
       shift[k] = -e->s->dim[k];
   }
 
-#ifdef SWIFT_USE_NAIVE_INTERACTIONS_STARS
-  DOPAIR1_SUBSET_STARS_NAIVE(r, ci, sparts_i, ind, scount, cj, shift);
-#else
   /* Get the sorting index. */
   int sid = 0;
   for (int k = 0; k < 3; k++)
-    sid = 3 * sid + ((cj->loc[k] - ci->loc[k] + shift[k] < 0) ? 0
+    sid = 3 * sid + ((cj->loc[k] - ci->loc[k] + shift[k] < 0)   ? 0
                      : (cj->loc[k] - ci->loc[k] + shift[k] > 0) ? 2
                                                                 : 1);
 
@@ -1068,97 +1171,29 @@ void DOPAIR1_SUBSET_BRANCH_STARS(struct runner *r, struct cell *restrict ci,
   const int flipped = runner_flip[sid];
   sid = sortlistID[sid];
 
-  /* Has the cell cj been sorted? */
-  if (!(cj->hydro.sorted & (1 << sid)) ||
-      cj->hydro.dx_max_sort_old > space_maxreldx * cj->dmin)
-    error("Interacting unsorted cells.");
-
-  DOPAIR1_SUBSET_STARS(r, ci, sparts_i, ind, scount, cj, sid, flipped, shift);
-#endif
-}
-
-void DOSUB_SUBSET_STARS(struct runner *r, struct cell *ci, struct spart *sparts,
-                        int *ind, int scount, struct cell *cj, int gettimer) {
+  /* Let's first lock the cell */
+  lock_lock(&cj->hydro.extra_sort_lock);
 
-  const struct engine *e = r->e;
-  struct space *s = e->s;
+  const int is_sorted =
+      (cj->hydro.sorted & (1 << sid)) &&
+      (cj->hydro.dx_max_sort_old <= space_maxreldx * cj->dmin);
 
-  /* Should we even bother? */
-  if (!cell_is_active_stars(ci, e) &&
-      (cj == NULL || !cell_is_active_stars(cj, e)))
-    return;
+#if defined(SWIFT_USE_NAIVE_INTERACTIONS)
+  const int force_naive = 1;
+#else
+  const int force_naive = 0;
+#endif
 
-  /* Find out in which sub-cell of ci the parts are. */
-  struct cell *sub = NULL;
-  if (ci->split) {
-    for (int k = 0; k < 8; k++) {
-      if (ci->progeny[k] != NULL) {
-        if (&sparts[ind[0]] >= &ci->progeny[k]->stars.parts[0] &&
-            &sparts[ind[0]] <
-                &ci->progeny[k]->stars.parts[ci->progeny[k]->stars.count]) {
-          sub = ci->progeny[k];
-          break;
-        }
-      }
-    }
+  /* Can we use the sorted interactions or do we default to naive? */
+  if (force_naive || !is_sorted) {
+    DOPAIR1_SUBSET_STARS_NAIVE(r, ci, sparts_i, ind, scount, cj, shift);
+  } else {
+    DOPAIR1_SUBSET_STARS(r, ci, sparts_i, ind, scount, cj, sid, flipped, shift);
   }
 
-  /* Is this a single cell? */
-  if (cj == NULL) {
-
-    /* Recurse? */
-    if (cell_can_recurse_in_self_stars_task(ci)) {
-
-      /* Loop over all progeny. */
-      DOSUB_SUBSET_STARS(r, sub, sparts, ind, scount, NULL, 0);
-      for (int j = 0; j < 8; j++)
-        if (ci->progeny[j] != sub && ci->progeny[j] != NULL)
-          DOSUB_SUBSET_STARS(r, sub, sparts, ind, scount, ci->progeny[j], 0);
-
-    }
-
-    /* Otherwise, compute self-interaction. */
-    else
-      DOSELF1_SUBSET_BRANCH_STARS(r, ci, sparts, ind, scount);
-  } /* self-interaction. */
-
-  /* Otherwise, it's a pair interaction. */
-  else {
-
-    /* Recurse? */
-    if (cell_can_recurse_in_pair_stars_task(ci, cj) &&
-        cell_can_recurse_in_pair_stars_task(cj, ci)) {
-
-      /* Get the type of pair and flip ci/cj if needed. */
-      double shift[3] = {0.0, 0.0, 0.0};
-      const int sid = space_getsid(s, &ci, &cj, shift);
-
-      struct cell_split_pair *csp = &cell_split_pairs[sid];
-      for (int k = 0; k < csp->count; k++) {
-        const int pid = csp->pairs[k].pid;
-        const int pjd = csp->pairs[k].pjd;
-        if (ci->progeny[pid] == sub && cj->progeny[pjd] != NULL)
-          DOSUB_SUBSET_STARS(r, ci->progeny[pid], sparts, ind, scount,
-                             cj->progeny[pjd], 0);
-        if (ci->progeny[pid] != NULL && cj->progeny[pjd] == sub)
-          DOSUB_SUBSET_STARS(r, cj->progeny[pjd], sparts, ind, scount,
-                             ci->progeny[pid], 0);
-      }
-    }
-
-    /* Otherwise, compute the pair directly. */
-    else if (cell_is_active_stars(ci, e) && cj->hydro.count > 0) {
-
-      /* Do any of the cells need to be drifted first? */
-      if (cell_is_active_stars(ci, e)) {
-        if (!cell_are_spart_drifted(ci, e)) error("Cell should be drifted!");
-        if (!cell_are_part_drifted(cj, e)) error("Cell should be drifted!");
-      }
-
-      DOPAIR1_SUBSET_BRANCH_STARS(r, ci, sparts, ind, scount, cj);
-    }
-
-  } /* otherwise, pair interaction. */
+  /* Now we can unlock */
+  if (lock_unlock(&cj->hydro.extra_sort_lock) != 0)
+    error("Impossible to unlock cell!");
 }
 
 /**
@@ -1167,54 +1202,45 @@ void DOSUB_SUBSET_STARS(struct runner *r, struct cell *ci, struct spart *sparts,
  *
  * @param r #runner
  * @param c #cell c
- *
+ * @param offset First particle in the cell to treat (for split tasks).
+ * @param increment Interval between successive particles that are treated.
  */
-void DOSELF1_BRANCH_STARS(struct runner *r, struct cell *c) {
+void DOSELF1_BRANCH_STARS(struct runner *r, const struct cell *c,
+                          const int limit_min_h, const int limit_max_h) {
 
   const struct engine *restrict e = r->e;
 
   /* Anything to do here? */
   if (c->stars.count == 0) return;
 
+  /* Anything to do here? */
+  if (c->hydro.count == 0) return;
+
   /* Anything to do here? */
   if (!cell_is_active_stars(c, e)) return;
 
+#ifdef SWIFT_DEBUG_CHECKS
+
   /* Did we mess up the recursion? */
   if (c->stars.h_max_old * kernel_gamma > c->dmin)
     error("Cell smaller than smoothing length");
 
-  DOSELF1_STARS(r, c, 1);
-}
+  if (!limit_max_h && c->stars.h_max_active * kernel_gamma > c->dmin)
+    error("Cell smaller than smoothing length");
 
-#define RUNNER_CHECK_SORT(TYPE, PART, cj, ci, sid)                          \
-  ({                                                                        \
-    const struct sort_entry *restrict sort_j =                              \
-        cell_get_##TYPE##_sorts(cj, sid);                                   \
-                                                                            \
-    for (int pjd = 0; pjd < cj->TYPE.count; pjd++) {                        \
-      const struct PART *p = &cj->TYPE.parts[sort_j[pjd].i];                \
-      if (PART##_is_inhibited(p, e)) continue;                              \
-                                                                            \
-      const float d = p->x[0] * runner_shift[sid][0] +                      \
-                      p->x[1] * runner_shift[sid][1] +                      \
-                      p->x[2] * runner_shift[sid][2];                       \
-      if ((fabsf(d - sort_j[pjd].d) - cj->TYPE.dx_max_sort) >               \
-              1.0e-4 * max(fabsf(d), cj->TYPE.dx_max_sort_old) &&           \
-          (fabsf(d - sort_j[pjd].d) - cj->TYPE.dx_max_sort) >               \
-              cj->width[0] * 1.0e-10)                                       \
-        error(                                                              \
-            "particle shift diff exceeds dx_max_sort in cell cj. "          \
-            "cj->nodeID=%d "                                                \
-            "ci->nodeID=%d d=%e sort_j[pjd].d=%e cj->" #TYPE                \
-            ".dx_max_sort=%e "                                              \
-            "cj->" #TYPE                                                    \
-            ".dx_max_sort_old=%e, cellID=%lld super->cellID=%lld"           \
-            "cj->depth=%d cj->maxdepth=%d",                                 \
-            cj->nodeID, ci->nodeID, d, sort_j[pjd].d, cj->TYPE.dx_max_sort, \
-            cj->TYPE.dx_max_sort_old, cj->cellID, cj->hydro.super->cellID,  \
-            cj->depth, cj->maxdepth);                                       \
-    }                                                                       \
-  })
+  /* Did we mess up the recursion? */
+  if (limit_min_h && !limit_max_h)
+    error("Fundamental error in the recursion logic");
+#endif
+
+  /* Check that cells are drifted. */
+  if (!cell_are_part_drifted(c, e))
+    error("Interacting undrifted cell (hydro).");
+  if (!cell_are_spart_drifted(c, e))
+    error("Interacting undrifted cell (stars).");
+
+  DOSELF1_STARS(r, c, limit_min_h, limit_max_h);
+}
 
 /**
  * @brief Determine which version of DOPAIR1_STARS needs to be called depending
@@ -1224,31 +1250,36 @@ void DOSELF1_BRANCH_STARS(struct runner *r, struct cell *c) {
  * @param r #runner
  * @param ci #cell ci
  * @param cj #cell cj
- *
+ * @param offset First particle in the cell to treat (for split tasks).
+ * @param increment Interval between successive particles that are treated.
  */
-void DOPAIR1_BRANCH_STARS(struct runner *r, struct cell *ci, struct cell *cj) {
+void DOPAIR1_BRANCH_STARS(struct runner *r, struct cell *ci, struct cell *cj,
+                          const int limit_min_h, const int limit_max_h) {
 
   const struct engine *restrict e = r->e;
 
   /* Get the sort ID. */
   double shift[3] = {0.0, 0.0, 0.0};
-  const int sid = space_getsid(e->s, &ci, &cj, shift);
+  const int sid = space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
 
-  const int ci_active = cell_is_active_stars(ci, e);
-  const int cj_active = cell_is_active_stars(cj, e);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY || \
      FUNCTION_TASK_LOOP == TASK_LOOP_STARS_PREP2)
-  const int do_ci_stars = ci->nodeID == e->nodeID;
-  const int do_cj_stars = cj->nodeID == e->nodeID;
+  /* Here we update the stars --> the star cell must be local */
+  const int ci_local = (ci->nodeID == e->nodeID);
+  const int cj_local = (cj->nodeID == e->nodeID);
+#elif (FUNCTION_TASK_LOOP == TASK_LOOP_FEEDBACK || \
+       FUNCTION_TASK_LOOP == TASK_LOOP_STARS_PREP1)
+  /* Here we update the gas --> the gas cell must be local */
+  const int ci_local = (cj->nodeID == e->nodeID);
+  const int cj_local = (ci->nodeID == e->nodeID);
 #else
-  /* here we are updating the hydro -> switch ci, cj */
-  const int do_ci_stars = cj->nodeID == e->nodeID;
-  const int do_cj_stars = ci->nodeID == e->nodeID;
+  error("Invalid loop type!");
 #endif
-  const int do_ci = (ci->stars.count != 0 && cj->hydro.count != 0 &&
-                     ci_active && do_ci_stars);
-  const int do_cj = (cj->stars.count != 0 && ci->hydro.count != 0 &&
-                     cj_active && do_cj_stars);
+
+  const int do_ci = ci->stars.count != 0 && cj->hydro.count != 0 &&
+                    cell_is_active_stars(ci, e) && ci_local;
+  const int do_cj = cj->stars.count != 0 && ci->hydro.count != 0 &&
+                    cell_is_active_stars(cj, e) && cj_local;
 
   /* Anything to do here? */
   if (!do_ci && !do_cj) return;
@@ -1258,46 +1289,32 @@ void DOPAIR1_BRANCH_STARS(struct runner *r, struct cell *ci, struct cell *cj) {
       (!cell_are_spart_drifted(ci, e) || !cell_are_part_drifted(cj, e)))
     error("Interacting undrifted cells.");
 
+  if (do_cj &&
+      (!cell_are_part_drifted(ci, e) || !cell_are_spart_drifted(cj, e)))
+    error("Interacting undrifted cells.");
+
   /* Have the cells been sorted? */
   if (do_ci && (!(ci->stars.sorted & (1 << sid)) ||
                 ci->stars.dx_max_sort_old > space_maxreldx * ci->dmin))
-    error("Interacting unsorted cells.");
+    error("Interacting unsorted cells (ci stars).");
 
   if (do_ci && (!(cj->hydro.sorted & (1 << sid)) ||
                 cj->hydro.dx_max_sort_old > space_maxreldx * cj->dmin))
-    error("Interacting unsorted cells.");
-
-  if (do_cj &&
-      (!cell_are_part_drifted(ci, e) || !cell_are_spart_drifted(cj, e)))
-    error("Interacting undrifted cells.");
+    error("Interacting unsorted cells (cj hydro).");
 
   /* Have the cells been sorted? */
   if (do_cj && (!(ci->hydro.sorted & (1 << sid)) ||
                 ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin))
-    error("Interacting unsorted cells.");
+    error("Interacting unsorted cells. (ci hydro)");
 
   if (do_cj && (!(cj->stars.sorted & (1 << sid)) ||
                 cj->stars.dx_max_sort_old > space_maxreldx * cj->dmin))
-    error("Interacting unsorted cells.");
-
-#ifdef SWIFT_DEBUG_CHECKS
-  if (do_ci) {
-    // MATTHIEU: This test is faulty. To be fixed...
-    // RUNNER_CHECK_SORT(hydro, part, cj, ci, sid);
-    RUNNER_CHECK_SORT(stars, spart, ci, cj, sid);
-  }
-
-  if (do_cj) {
-    // MATTHIEU: This test is faulty. To be fixed...
-    // RUNNER_CHECK_SORT(hydro, part, ci, cj, sid);
-    RUNNER_CHECK_SORT(stars, spart, cj, ci, sid);
-  }
-#endif /* SWIFT_DEBUG_CHECKS */
+    error("Interacting unsorted cells. (cj stars)");
 
 #ifdef SWIFT_USE_NAIVE_INTERACTIONS_STARS
-  DOPAIR1_STARS_NAIVE(r, ci, cj, 1);
+  DOPAIR1_STARS_NAIVE(r, ci, cj, limit_min_h, limit_max_h);
 #else
-  DO_SYM_PAIR1_STARS(r, ci, cj, sid, shift);
+  DO_SYM_PAIR1_STARS(r, ci, cj, limit_min_h, limit_max_h, sid, shift);
 #endif
 }
 
@@ -1308,100 +1325,146 @@ void DOPAIR1_BRANCH_STARS(struct runner *r, struct cell *ci, struct cell *cj) {
  * @param ci The first #cell.
  * @param cj The second #cell.
  * @param gettimer Do we have a timer ?
+ * @param offset First particle in the cell to treat (for split tasks).
+ * @param increment Interval between successive particles that are treated.
  *
  * @todo Hard-code the sid on the recursive calls to avoid the
  * redundant computations to find the sid on-the-fly.
  */
 void DOSUB_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
-                       int gettimer) {
-
+                       int recurse_below_h_max, const int gettimer) {
   TIMER_TIC;
 
   struct space *s = r->e->s;
   const struct engine *e = r->e;
 
-  /* Should we even bother? */
-  const int should_do_ci = ci->stars.count != 0 && cj->hydro.count != 0 &&
-                           cell_is_active_stars(ci, e);
-  const int should_do_cj = cj->stars.count != 0 && ci->hydro.count != 0 &&
-                           cell_is_active_stars(cj, e);
-  if (!should_do_ci && !should_do_cj) return;
-
   /* Get the type of pair and flip ci/cj if needed. */
   double shift[3];
-  const int sid = space_getsid(s, &ci, &cj, shift);
-
-  /* Recurse? */
-  if (cell_can_recurse_in_pair_stars_task(ci, cj) &&
-      cell_can_recurse_in_pair_stars_task(cj, ci)) {
-    struct cell_split_pair *csp = &cell_split_pairs[sid];
-    for (int k = 0; k < csp->count; k++) {
-      const int pid = csp->pairs[k].pid;
-      const int pjd = csp->pairs[k].pjd;
-      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL)
-        DOSUB_PAIR1_STARS(r, ci->progeny[pid], cj->progeny[pjd], 0);
-    }
-  }
-
-  /* Otherwise, compute the pair directly. */
-  else {
+  const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
 
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY || \
      FUNCTION_TASK_LOOP == TASK_LOOP_STARS_PREP2)
-    const int do_ci_stars = ci->nodeID == e->nodeID;
-    const int do_cj_stars = cj->nodeID == e->nodeID;
+  /* Here we update the stars --> the star cell must be local */
+  const int ci_local = (ci->nodeID == e->nodeID);
+  const int cj_local = (cj->nodeID == e->nodeID);
+#elif (FUNCTION_TASK_LOOP == TASK_LOOP_FEEDBACK || \
+       FUNCTION_TASK_LOOP == TASK_LOOP_STARS_PREP1)
+  /* Here we update the gas --> the gas cell must be local */
+  const int ci_local = (cj->nodeID == e->nodeID);
+  const int cj_local = (ci->nodeID == e->nodeID);
 #else
-    /* here we are updating the hydro -> switch ci, cj */
-    const int do_ci_stars = cj->nodeID == e->nodeID;
-    const int do_cj_stars = ci->nodeID == e->nodeID;
+  error("Invalid loop type!");
 #endif
-    const int do_ci = ci->stars.count != 0 && cj->hydro.count != 0 &&
-                      cell_is_active_stars(ci, e) && do_ci_stars;
-    const int do_cj = cj->stars.count != 0 && ci->hydro.count != 0 &&
-                      cell_is_active_stars(cj, e) && do_cj_stars;
 
-    if (do_ci) {
+  /* What kind of pair are we doing here? */
+  const int do_ci = ci->stars.count != 0 && cj->hydro.count != 0 &&
+                    cell_is_active_stars(ci, e) && ci_local;
+  const int do_cj = cj->stars.count != 0 && ci->hydro.count != 0 &&
+                    cell_is_active_stars(cj, e) && cj_local;
 
-      /* Make sure both cells are drifted to the current timestep. */
-      if (!cell_are_spart_drifted(ci, e))
-        error("Interacting undrifted cells (sparts).");
+  /* Should we even bother? */
+  if (!do_ci && !do_cj) return;
 
-      if (!cell_are_part_drifted(cj, e))
-        error("Interacting undrifted cells (parts).");
+  /* We reached a leaf OR a cell small enough to be processed quickly */
+  if (!ci->split || ci->stars.count < space_recurse_size_pair_stars ||
+      !cj->split || cj->stars.count < space_recurse_size_pair_stars) {
 
-      /* Do any of the cells need to be sorted first? */
+    /* Do any of the cells need to be sorted first?
+     * Since h_max might have changed, we may not have sorted at this level */
+    if (do_ci) {
       if (!(ci->stars.sorted & (1 << sid)) ||
           ci->stars.dx_max_sort_old > ci->dmin * space_maxreldx) {
-        error("Interacting unsorted cell (sparts).");
+        runner_do_stars_sort(r, ci, (1 << sid), 0, 0);
       }
-
       if (!(cj->hydro.sorted & (1 << sid)) ||
-          cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx)
-        error("Interacting unsorted cell (parts). %i", cj->nodeID);
+          cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
+      }
     }
-
     if (do_cj) {
-
-      /* Make sure both cells are drifted to the current timestep. */
-      if (!cell_are_part_drifted(ci, e))
-        error("Interacting undrifted cells (parts).");
-
-      if (!cell_are_spart_drifted(cj, e))
-        error("Interacting undrifted cells (sparts).");
-
-      /* Do any of the cells need to be sorted first? */
       if (!(ci->hydro.sorted & (1 << sid)) ||
           ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
-        error("Interacting unsorted cell (parts).");
+        /* Bert: RT probably broken here! */
+        runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                             /*rt_request=*/0, /*clock=*/0);
       }
-
       if (!(cj->stars.sorted & (1 << sid)) ||
           cj->stars.dx_max_sort_old > cj->dmin * space_maxreldx) {
-        error("Interacting unsorted cell (sparts).");
+        runner_do_stars_sort(r, cj, (1 << sid), 0, 0);
+      }
+    }
+
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOPAIR1_BRANCH_STARS(r, ci, cj, /*limit_h_min=*/0,
+                         /*limit_h_max=*/recurse_below_h_max);
+
+  } else {
+
+    /* Both ci and cj are split */
+
+    /* Should we change the recursion regime because we encountered a large
+       particle? */
+    if (!recurse_below_h_max && (!cell_can_recurse_in_subpair_stars_task(ci) ||
+                                 !cell_can_recurse_in_subpair_stars_task(cj))) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
+
+      /* Do any of the cells need to be sorted first?
+       * Since h_max might have changed, we may not have sorted at this level */
+      if (do_ci) {
+        if (!(ci->stars.sorted & (1 << sid)) ||
+            ci->stars.dx_max_sort_old > ci->dmin * space_maxreldx) {
+          runner_do_stars_sort(r, ci, (1 << sid), 0, 0);
+        }
+        if (!(cj->hydro.sorted & (1 << sid)) ||
+            cj->hydro.dx_max_sort_old > cj->dmin * space_maxreldx) {
+          /* Bert: RT probably broken here! */
+          runner_do_hydro_sort(r, cj, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                               /*rt_request=*/0,
+                               /*clock=*/0);
+        }
       }
+      if (do_cj) {
+        if (!(ci->hydro.sorted & (1 << sid)) ||
+            ci->hydro.dx_max_sort_old > ci->dmin * space_maxreldx) {
+          /* Bert: RT probably broken here! */
+          runner_do_hydro_sort(r, ci, (1 << sid), /*cleanup=*/0, /*lock=*/1,
+                               /*rt_request=*/0,
+                               /*clock=*/0);
+        }
+        if (!(cj->stars.sorted & (1 << sid)) ||
+            cj->stars.dx_max_sort_old > cj->dmin * space_maxreldx) {
+          runner_do_stars_sort(r, cj, (1 << sid), 0, 0);
+        }
+      }
+
+      /* message("Multi-level PAIR! ci->count=%d cj->count=%d",
+       * ci->hydro.count, cj->hydro.count); */
+
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOPAIR1_BRANCH_STARS(r, ci, cj, /*limit_h_min=*/1, /*limit_h_max=*/1);
     }
 
-    if (do_ci || do_cj) DOPAIR1_BRANCH_STARS(r, ci, cj);
+    /* Recurse to the lower levels. */
+    const struct cell_split_pair *const csp = &cell_split_pairs[sid];
+    for (int k = 0; k < csp->count; k++) {
+      const int pid = csp->pairs[k].pid;
+      const int pjd = csp->pairs[k].pjd;
+      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL) {
+        DOSUB_PAIR1_STARS(r, ci->progeny[pid], cj->progeny[pjd],
+                          recurse_below_h_max, /*gettimer=*/0);
+      }
+    }
   }
 
   TIMER_TOC(TIMER_DOSUB_PAIR_STARS);
@@ -1413,44 +1476,182 @@ void DOSUB_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
  * @param r The #runner.
  * @param ci The first #cell.
  * @param gettimer Do we have a timer ?
+ * @param offset First particle in the cell to treat (for split tasks).
+ * @param increment Interval between successive particles that are treated.
  */
-void DOSUB_SELF1_STARS(struct runner *r, struct cell *ci, int gettimer) {
+void DOSUB_SELF1_STARS(struct runner *r, struct cell *c,
+                       int recurse_below_h_max, const int gettimer) {
 
   TIMER_TIC;
 
 #ifdef SWIFT_DEBUG_CHECKS
-  if (ci->nodeID != engine_rank)
+  if (c->nodeID != engine_rank)
     error("This function should not be called on foreign cells");
 #endif
 
   /* Should we even bother? */
-  if (ci->hydro.count == 0 || ci->stars.count == 0 ||
-      !cell_is_active_stars(ci, r->e))
+  if (c->hydro.count == 0 || c->stars.count == 0 ||
+      !cell_is_active_stars(c, r->e))
     return;
 
-  /* Recurse? */
-  if (cell_can_recurse_in_self_stars_task(ci)) {
+  /* We reached a leaf OR a cell small enough to process quickly */
+  if (!c->split || c->stars.count < space_recurse_size_self_stars) {
 
-    /* Loop over all progeny. */
-    for (int k = 0; k < 8; k++)
-      if (ci->progeny[k] != NULL) {
-        DOSUB_SELF1_STARS(r, ci->progeny[k], 0);
-        for (int j = k + 1; j < 8; j++)
-          if (ci->progeny[j] != NULL)
-            DOSUB_PAIR1_STARS(r, ci->progeny[k], ci->progeny[j], 0);
+    /* We interact all particles in that cell:
+       - No limit on the smallest h
+       - Apply the max h limit if we are recursing below the level
+       where h is smaller than the cell size */
+    DOSELF1_BRANCH_STARS(r, c, /*limit_h_min=*/0,
+                         /*limit_h_max=*/recurse_below_h_max);
+
+  } else {
+
+    /* Should we change the recursion regime because we encountered a large
+       particle at this level? */
+    if (!recurse_below_h_max && !cell_can_recurse_in_subself_stars_task(c)) {
+      recurse_below_h_max = 1;
+    }
+
+    /* If some particles are larger than the daughter cells, we must
+       process them at this level before going deeper */
+    if (recurse_below_h_max) {
+
+      /* message("Multi-level SELF! c->count=%d", c->hydro.count); */
+
+      /* Interact all *active* particles with h in the range [dmin/2, dmin)
+         with all their neighbours */
+      DOSELF1_BRANCH_STARS(r, c, /*limit_h_min=*/1, /*limit_h_max=*/1);
+    }
+
+    /* Recurse to the lower levels. */
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL) {
+        DOSUB_SELF1_STARS(r, c->progeny[k], recurse_below_h_max,
+                          /*gettimer=*/0);
+        for (int j = k + 1; j < 8; j++) {
+          if (c->progeny[j] != NULL) {
+            DOSUB_PAIR1_STARS(r, c->progeny[k], c->progeny[j],
+                              recurse_below_h_max,
+                              /*gettimer=*/0);
+          }
+        }
       }
+    }
   }
 
-  /* Otherwise, compute self-interaction. */
-  else {
+  if (gettimer) TIMER_TOC(TIMER_DOSUB_SELF_STARS);
+}
+
+/**
+ * @brief Find which sub-cell of a cell contain the subset of particles given
+ * by the list of indices.
+ *
+ * Will throw an error if the sub-cell can't be found.
+ *
+ * @param c The #cell
+ * @param sparts An array of #spart.
+ * @param ind Index of the #spart's in the particle array to find in the subs.
+ */
+struct cell *FIND_SUB_STARS(const struct cell *const c,
+                            const struct spart *const sparts,
+                            const int *const ind) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (!c->split) error("Can't search for subs in a non-split cell");
+#endif
+
+  /* Find out in which sub-cell of ci the parts are.
+   *
+   * Note: We only need to check the first particle in the list */
+  for (int k = 0; k < 8; k++) {
+    if (c->progeny[k] != NULL) {
+      if (&sparts[ind[0]] >= &c->progeny[k]->stars.parts[0] &&
+          &sparts[ind[0]] <
+              &c->progeny[k]->stars.parts[c->progeny[k]->stars.count]) {
+        return c->progeny[k];
+        break;
+      }
+    }
+  }
+  error("Invalid sub!");
+  return NULL;
+}
+
+void DOSUB_PAIR_SUBSET_STARS(struct runner *r, struct cell *ci,
+                             struct spart *sparts, const int *ind,
+                             const int scount, struct cell *cj,
+                             const int gettimer) {
+
+  const struct engine *e = r->e;
+  struct space *s = e->s;
+
+  /* Should we even bother? */
+  if (cj->hydro.count == 0) return;
+  if (ci->stars.count == 0) return;
+  if (!cell_is_active_stars(ci, e)) return;
+
+  /* Recurse? */
+  if (cell_can_recurse_in_pair_stars_task(ci) &&
+      cell_can_recurse_in_pair_stars_task(cj)) {
+
+    /* Find in which sub-cell of ci the particles are */
+    struct cell *const sub = FIND_SUB_STARS(ci, sparts, ind);
+
+    /* Get the type of pair and flip ci/cj if needed. */
+    double shift[3];
+    const int sid = space_getsid_and_swap_cells(s, &ci, &cj, shift);
+
+    struct cell_split_pair *csp = &cell_split_pairs[sid];
+    for (int k = 0; k < csp->count; k++) {
+      const int pid = csp->pairs[k].pid;
+      const int pjd = csp->pairs[k].pjd;
+      if (ci->progeny[pid] == sub && cj->progeny[pjd] != NULL)
+        DOSUB_PAIR_SUBSET_STARS(r, ci->progeny[pid], sparts, ind, scount,
+                                cj->progeny[pjd], /*gettimer=*/0);
+      if (ci->progeny[pid] != NULL && cj->progeny[pjd] == sub)
+        DOSUB_PAIR_SUBSET_STARS(r, cj->progeny[pjd], sparts, ind, scount,
+                                ci->progeny[pid], /*gettimer=*/0);
+    }
+
+  }
+  /* Otherwise, compute the pair directly. */
+  else if (cell_is_active_stars(ci, e)) {
 
-    /* Drift the cell to the current timestep if needed. */
-    if (!cell_are_spart_drifted(ci, r->e)) error("Interacting undrifted cell.");
+    /* Do any of the cells need to be drifted first? */
+    if (!cell_are_part_drifted(cj, e)) error("Cell should be drifted!");
 
-    DOSELF1_BRANCH_STARS(r, ci);
+    DOPAIR1_SUBSET_BRANCH_STARS(r, ci, sparts, ind, scount, cj);
   }
+}
+
+void DOSUB_SELF_SUBSET_STARS(struct runner *r, struct cell *ci,
+                             struct spart *sparts, const int *ind,
+                             const int scount, const int gettimer) {
+
+  const struct engine *e = r->e;
+
+  /* Should we even bother? */
+  if (ci->hydro.count == 0) return;
+  if (ci->stars.count == 0) return;
+  if (!cell_is_active_stars(ci, e)) return;
 
-  TIMER_TOC(TIMER_DOSUB_SELF_STARS);
+  /* Recurse? */
+  if (ci->split && cell_can_recurse_in_self_stars_task(ci)) {
+
+    /* Find in which sub-cell of ci the particles are */
+    struct cell *const sub = FIND_SUB_STARS(ci, sparts, ind);
+
+    /* Loop over all progeny. */
+    DOSUB_SELF_SUBSET_STARS(r, sub, sparts, ind, scount, /*gettimer=*/0);
+    for (int j = 0; j < 8; j++)
+      if (ci->progeny[j] != sub && ci->progeny[j] != NULL)
+        DOSUB_PAIR_SUBSET_STARS(r, sub, sparts, ind, scount, ci->progeny[j],
+                                /*gettimer=*/0);
+  }
+
+  /* Otherwise, compute self-interaction. */
+  else
+    DOSELF1_SUBSET_BRANCH_STARS(r, ci, sparts, ind, scount);
 }
 
 #undef WITH_RT
diff --git a/src/runner_doiact_grav.c b/src/runner_doiact_grav.c
index a05f62cb188b9c1c86c974046136d15e1be30f3e..72025d83c26d79df37ff9a8ab66e50328f506cea 100644
--- a/src/runner_doiact_grav.c
+++ b/src/runner_doiact_grav.c
@@ -194,6 +194,8 @@ static INLINE void runner_dopair_grav_pp_full_no_cache(
 
   /* Prepare the i cache */
   const int gcount_padded_i = gcount_i - (gcount_i % VEC_SIZE) + VEC_SIZE;
+  if (cache_i->count < gcount_padded_i)
+    gravity_cache_init(cache_i, gcount_padded_i);
   gravity_cache_zero_output(cache_i, gcount_padded_i);
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -391,6 +393,8 @@ static INLINE void runner_dopair_grav_pp_truncated_no_cache(
 
   /* Prepare the i cache */
   const int gcount_padded_i = gcount_i - (gcount_i % VEC_SIZE) + VEC_SIZE;
+  if (cache_i->count < gcount_padded_i)
+    gravity_cache_init(cache_i, gcount_padded_i);
   gravity_cache_zero_output(cache_i, gcount_padded_i);
 
   /* Loop over sink particles */
@@ -651,7 +655,7 @@ static INLINE void runner_dopair_grav_pp_full(
 #ifdef SWIFT_DEBUG_CHECKS
       /* The gravity_cache are sometimes allocated with more
          place than required => flag with mass=0 */
-      if (gparts_j[pjd].time_bin == time_bin_not_created && mass_j != 0.f) {
+      if (mass_j != 0.f && gparts_j[pjd].time_bin == time_bin_not_created) {
         error("Found an extra gpart in the gravity interaction");
       }
       if (gparts_i[pid].time_bin == time_bin_not_created &&
@@ -1262,14 +1266,6 @@ void runner_dopair_grav_pp(struct runner *r, struct cell *ci, struct cell *cj,
   const int gcount_j = cj->grav.count;
   const int gcount_padded_i = gcount_i - (gcount_i % VEC_SIZE) + VEC_SIZE;
   const int gcount_padded_j = gcount_j - (gcount_j % VEC_SIZE) + VEC_SIZE;
-
-#ifdef SWIFT_DEBUG_CHECKS
-  /* Check that we fit in cache */
-  if (gcount_i > ci_cache->count || gcount_j > cj_cache->count)
-    error("Not enough space in the caches! gcount_i=%d gcount_j=%d", gcount_i,
-          gcount_j);
-#endif
-
   const int allow_multipole_i = allow_mpole && ci->grav.count > 1;
   const int allow_multipole_j = allow_mpole && cj->grav.count > 1;
 
@@ -1820,12 +1816,6 @@ void runner_doself_grav_pp(struct runner *r, struct cell *c) {
   const int gcount = c->grav.count;
   const int gcount_padded = gcount - (gcount % VEC_SIZE) + VEC_SIZE;
 
-#ifdef SWIFT_DEBUG_CHECKS
-  /* Check that we fit in cache */
-  if (gcount > ci_cache->count)
-    error("Not enough space in the cache! gcount=%d", gcount);
-#endif
-
   /* Fill the cache */
   gravity_cache_populate_no_mpole(e->max_active_bin, ci_cache, c->grav.parts,
                                   gcount, gcount_padded, loc, c,
@@ -2143,12 +2133,6 @@ void runner_dopair_recursive_grav_pm(struct runner *r, struct cell *ci,
     const int gcount_i = ci->grav.count;
     const int gcount_padded_i = gcount_i - (gcount_i % VEC_SIZE) + VEC_SIZE;
 
-#ifdef SWIFT_DEBUG_CHECKS
-    /* Check that we fit in cache */
-    if (gcount_i > ci_cache->count)
-      error("Not enough space in the cache! gcount_i=%d", gcount_i);
-#endif
-
     /* Recover the multipole info and the CoM locations */
     const struct multipole *multi_j = &cj->grav.multipole->m_pole;
     const float CoM_j[3] = {(float)(cj->grav.multipole->CoM[0]),
@@ -2525,7 +2509,7 @@ void runner_do_grav_long_range(struct runner *r, struct cell *ci,
       multi_i->pot.interacted = 1;
 
     } /* We are in charge of this pair */
-  }   /* Loop over top-level cells */
+  } /* Loop over top-level cells */
 
   if (timer) TIMER_TOC(timer_dograv_long_range);
 }
diff --git a/src/runner_doiact_hydro.c b/src/runner_doiact_hydro.c
index 3ec53e5077df9da65348b385abccaca9830255a8..ef0db583b0ae85a4d2219981104b07e1e7475ff1 100644
--- a/src/runner_doiact_hydro.c
+++ b/src/runner_doiact_hydro.c
@@ -32,7 +32,9 @@
 #include "rt.h"
 #include "runner.h"
 #include "runner_doiact_hydro_vec.h"
-#include "sink.h"
+#include "runner_doiact_sinks.h"
+#include "sink_iact.h"
+#include "sink_properties.h"
 #include "space_getsid.h"
 #include "star_formation_iact.h"
 #include "timers.h"
diff --git a/src/runner_doiact_hydro.h b/src/runner_doiact_hydro.h
index 04824e87eecc32f91aa4352176212c853ceaf56b..0b0e34feed74bbfe9758fce98324f8ddcbff5897 100644
--- a/src/runner_doiact_hydro.h
+++ b/src/runner_doiact_hydro.h
@@ -92,8 +92,14 @@
 #define _DOSUB_PAIR2(f) PASTE(runner_dosub_pair2, f)
 #define DOSUB_PAIR2 _DOSUB_PAIR2(FUNCTION)
 
-#define _DOSUB_SUBSET(f) PASTE(runner_dosub_subset, f)
-#define DOSUB_SUBSET _DOSUB_SUBSET(FUNCTION)
+#define _DOSUB_SELF_SUBSET(f) PASTE(runner_dosub_self_subset, f)
+#define DOSUB_SELF_SUBSET _DOSUB_SELF_SUBSET(FUNCTION)
+
+#define _DOSUB_PAIR_SUBSET(f) PASTE(runner_dosub_pair_subset, f)
+#define DOSUB_PAIR_SUBSET _DOSUB_PAIR_SUBSET(FUNCTION)
+
+#define _FIND_SUB(f) PASTE(runner_find_sub, f)
+#define FIND_SUB _FIND_SUB(FUNCTION)
 
 #define _IACT_NONSYM(f) PASTE(runner_iact_nonsym, f)
 #define IACT_NONSYM _IACT_NONSYM(FUNCTION)
@@ -165,27 +171,37 @@
 #define _TIMER_DOPAIR_SUBSET(f) PASTE(timer_dopair_subset, f)
 #define TIMER_DOPAIR_SUBSET _TIMER_DOPAIR_SUBSET(FUNCTION)
 
-void DOSELF1_BRANCH(struct runner *r, struct cell *c);
-void DOSELF2_BRANCH(struct runner *r, struct cell *c);
+void DOSELF1_BRANCH(struct runner *r, const struct cell *c,
+                    const int limit_min_h, const int limit_max_h);
+void DOSELF2_BRANCH(struct runner *r, const struct cell *c,
+                    const int limit_min_h, const int limit_max_h);
 
-void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj);
-void DOPAIR2_BRANCH(struct runner *r, struct cell *ci, struct cell *cj);
+void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj,
+                    const int limit_min_h, const int limit_max_h);
+void DOPAIR2_BRANCH(struct runner *r, struct cell *ci, struct cell *cj,
+                    const int limit_min_h, const int limit_max_h);
 
-void DOSUB_SELF1(struct runner *r, struct cell *ci, int gettimer);
-void DOSUB_SELF2(struct runner *r, struct cell *ci, int gettimer);
+void DOSUB_SELF1(struct runner *r, struct cell *c, int recurse_below_h_max,
+                 const int gettimer);
+void DOSUB_SELF2(struct runner *r, struct cell *c, int recurse_below_h_max,
+                 const int gettimer);
 
 void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
-                 int gettimer);
+                 int recurse_below_h_max, const int gettimer);
 void DOSUB_PAIR2(struct runner *r, struct cell *ci, struct cell *cj,
-                 int gettimer);
+                 int recurse_below_h_max, const int gettimer);
+
+void DOSELF_SUBSET_BRANCH(struct runner *r, const struct cell *ci,
+                          struct part *restrict parts, const int *ind,
+                          const int count);
 
-void DOSELF_SUBSET_BRANCH(struct runner *r, struct cell *restrict ci,
-                          struct part *restrict parts, int *restrict ind,
-                          int count);
+void DOPAIR_SUBSET_BRANCH(struct runner *r, const struct cell *restrict ci,
+                          struct part *restrict parts_i, const int *ind,
+                          const int count, struct cell *restrict cj);
 
-void DOPAIR_SUBSET_BRANCH(struct runner *r, struct cell *restrict ci,
-                          struct part *restrict parts_i, int *restrict ind,
-                          int count, struct cell *restrict cj);
+void DOSUB_PAIR_SUBSET(struct runner *r, struct cell *ci, struct part *parts,
+                       const int *ind, const int count, struct cell *cj,
+                       const int gettimer);
 
-void DOSUB_SUBSET(struct runner *r, struct cell *ci, struct part *parts,
-                  int *ind, int count, struct cell *cj, int gettimer);
+void DOSUB_SELF_SUBSET(struct runner *r, struct cell *ci, struct part *parts,
+                       const int *ind, const int count, const int gettimer);
diff --git a/src/runner_doiact_limiter.h b/src/runner_doiact_limiter.h
index 4671c39db4a569122e9a2de6b8087307f1445b99..b4fdff50680833ca256330c4510b540c8bdf470a 100644
--- a/src/runner_doiact_limiter.h
+++ b/src/runner_doiact_limiter.h
@@ -92,11 +92,14 @@
 #define _TIMER_DOSUB_PAIR(f) PASTE(timer_dosub_pair, f)
 #define TIMER_DOSUB_PAIR _TIMER_DOSUB_PAIR(FUNCTION)
 
-void DOSELF1_BRANCH(struct runner *r, struct cell *c);
+void DOSELF1_BRANCH(struct runner *r, const struct cell *c,
+                    const int limit_min_h, const int limit_max_h);
 
-void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj);
+void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj,
+                    const int limit_min_h, const int limit_max_h);
 
-void DOSUB_SELF1(struct runner *r, struct cell *ci, int gettimer);
+void DOSUB_SELF1(struct runner *r, struct cell *ci, int recurse_below_h_max,
+                 const int gettimer);
 
 void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
-                 int gettimer);
+                 int recurse_below_h_max, const int gettimer);
diff --git a/src/runner_doiact_nosort.h b/src/runner_doiact_nosort.h
index 6a442afbdb0c7dfb24c5e9cb386783746a1f05ed..51d2412d0f6904dd9492798c9e126dbb02098f92 100644
--- a/src/runner_doiact_nosort.h
+++ b/src/runner_doiact_nosort.h
@@ -20,7 +20,7 @@ void DOPAIR1_NOSORT(struct runner *r, struct cell *ci, struct cell *cj) {
 
   /* Get the relative distance between the pairs, wrapping. */
   double shift[3] = {0.0, 0.0, 0.0};
-  space_getsid(e->s, &ci, &cj, shift);
+  space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
 
   const int count_i = ci->count;
   const int count_j = cj->count;
@@ -145,7 +145,7 @@ void DOPAIR2_NOSORT(struct runner *r, struct cell *ci, struct cell *cj) {
 
   /* Get the relative distance between the pairs, wrapping. */
   double shift[3] = {0.0, 0.0, 0.0};
-  space_getsid(e->s, &ci, &cj, shift);
+  space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
 
   const int count_i = ci->count;
   const int count_j = cj->count;
@@ -315,7 +315,7 @@ void DOPAIR_SUBSET_NOSORT(struct runner *r, struct cell *restrict ci,
         IACT_NONSYM(r2, dx, hi, pj->h, pi, pj);
       }
     } /* loop over the parts in cj. */
-  }   /* loop over the parts in ci. */
+  } /* loop over the parts in ci. */
 
   TIMER_TOC(timer_dopair_subset);
 }
diff --git a/src/runner_doiact_sinks.c b/src/runner_doiact_sinks.c
new file mode 100644
index 0000000000000000000000000000000000000000..7e65564731371856eba74336b50a3c65bb58fb64
--- /dev/null
+++ b/src/runner_doiact_sinks.c
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2012 Pedro Gonnet (pedro.gonnet@durham.ac.uk)
+ *                    Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *               2015 Peter W. Draper (p.w.draper@durham.ac.uk)
+ *               2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+/* Config parameters. */
+#include <config.h>
+
+/* Local headers. */
+#include "active.h"
+#include "cell.h"
+#include "engine.h"
+#include "runner.h"
+#include "sink_iact.h"
+#include "space_getsid.h"
+#include "timers.h"
+
+/* Import the sink density loop functions. */
+#define FUNCTION density
+#define FUNCTION_TASK_LOOP TASK_LOOP_DENSITY
+#include "runner_doiact_functions_sinks.h"
+#include "runner_doiact_undef.h"
+
+/* Import the sink swallow loop functions. */
+#define FUNCTION swallow
+#define FUNCTION_TASK_LOOP TASK_LOOP_SWALLOW
+#include "runner_doiact_functions_sinks.h"
+#include "runner_doiact_undef.h"
diff --git a/src/runner_doiact_sinks.h b/src/runner_doiact_sinks.h
index 28239aa64ca34f7abcae25797e00937d2044d99a..679db023aab32e0ae4d428c20020a69f40f9f8d2 100644
--- a/src/runner_doiact_sinks.h
+++ b/src/runner_doiact_sinks.h
@@ -1,6 +1,7 @@
 /*******************************************************************************
  * This file is part of SWIFT.
  * Copyright (c) 2020 Loic Hausammann (loic.hausammann@epfl.ch)
+ *               2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -22,20 +23,87 @@
    runner_dopair_FUNCTION, runner_doself_FUNCTION and runner_dosub_FUNCTION
    calling the pairwise interaction function runner_iact_FUNCTION. */
 
-void runner_doself_branch_sinks_swallow(struct runner *r, struct cell *c);
-void runner_dopair_branch_sinks_swallow(struct runner *r, struct cell *ci,
-                                        struct cell *cj);
-void runner_dosub_self_sinks_swallow(struct runner *r, struct cell *ci,
-                                     int gettimer);
-void runner_dosub_pair_sinks_swallow(struct runner *r, struct cell *ci,
-                                     struct cell *cj, int gettimer);
-
-void runner_do_sinks_gas_swallow_self(struct runner *r, struct cell *c,
-                                      int timer);
-void runner_do_sinks_gas_swallow_pair(struct runner *r, struct cell *ci,
-                                      struct cell *cj, int timer);
-
-void runner_do_sinks_sink_swallow_self(struct runner *r, struct cell *c,
-                                       int timer);
-void runner_do_sinks_sink_swallow_pair(struct runner *r, struct cell *ci,
-                                       struct cell *cj, int timer);
+#define PASTE(x, y) x##_##y
+
+#define _DOSELF1_SINKS(f) PASTE(runner_doself_sinks, f)
+#define DOSELF1_SINKS _DOSELF1_SINKS(FUNCTION)
+
+#define _DO_SYM_PAIR1_SINKS(f) PASTE(runner_do_sym_pair_sinks, f)
+#define DO_SYM_PAIR1_SINKS _DO_SYM_PAIR1_SINKS(FUNCTION)
+
+#define _DO_NONSYM_PAIR1_SINKS_NAIVE(f) \
+  PASTE(runner_do_nonsym_pair_sinks_naive, f)
+#define DO_NONSYM_PAIR1_SINKS_NAIVE _DO_NONSYM_PAIR1_SINKS_NAIVE(FUNCTION)
+
+#define _DOPAIR1_SINKS_NAIVE(f) PASTE(runner_dopair_sinks_naive, f)
+#define DOPAIR1_SINKS_NAIVE _DOPAIR1_SINKS_NAIVE(FUNCTION)
+
+#define _DOPAIR1_SUBSET_SINKS(f) PASTE(runner_dopair_subset_sinks, f)
+#define DOPAIR1_SUBSET_SINKS _DOPAIR1_SUBSET_SINKS(FUNCTION)
+
+#define _DOPAIR1_SUBSET_SINKS_NAIVE(f) \
+  PASTE(runner_dopair_subset_sinks_naive, f)
+#define DOPAIR1_SUBSET_SINKS_NAIVE _DOPAIR1_SUBSET_SINKS_NAIVE(FUNCTION)
+
+#define _DOSELF1_SUBSET_SINKS(f) PASTE(runner_doself_subset_sinks, f)
+#define DOSELF1_SUBSET_SINKS _DOSELF1_SUBSET_SINKS(FUNCTION)
+
+#define _DOSELF1_SUBSET_BRANCH_SINKS(f) \
+  PASTE(runner_doself_subset_branch_sinks, f)
+#define DOSELF1_SUBSET_BRANCH_SINKS _DOSELF1_SUBSET_BRANCH_SINKS(FUNCTION)
+
+#define _DOPAIR1_SUBSET_BRANCH_SINKS(f) \
+  PASTE(runner_dopair_subset_branch_sinks, f)
+#define DOPAIR1_SUBSET_BRANCH_SINKS _DOPAIR1_SUBSET_BRANCH_SINKS(FUNCTION)
+
+#define _DOSUB_SUBSET_SINKS(f) PASTE(runner_dosub_subset_sinks, f)
+#define DOSUB_SUBSET_SINKS _DOSUB_SUBSET_SINKS(FUNCTION)
+
+#define _DOSELF1_BRANCH_SINKS(f) PASTE(runner_doself_branch_sinks, f)
+#define DOSELF1_BRANCH_SINKS _DOSELF1_BRANCH_SINKS(FUNCTION)
+
+#define _DOPAIR1_BRANCH_SINKS(f) PASTE(runner_dopair_branch_sinks, f)
+#define DOPAIR1_BRANCH_SINKS _DOPAIR1_BRANCH_SINKS(FUNCTION)
+
+#define _DOSUB_PAIR1_SINKS(f) PASTE(runner_dosub_pair_sinks, f)
+#define DOSUB_PAIR1_SINKS _DOSUB_PAIR1_SINKS(FUNCTION)
+
+#define _DOSUB_SELF1_SINKS(f) PASTE(runner_dosub_self_sinks, f)
+#define DOSUB_SELF1_SINKS _DOSUB_SELF1_SINKS(FUNCTION)
+
+#define _TIMER_DOSELF_SINKS(f) PASTE(timer_doself_sinks, f)
+#define TIMER_DOSELF_SINKS _TIMER_DOSELF_SINKS(FUNCTION)
+
+#define _TIMER_DOPAIR_SINKS(f) PASTE(timer_dopair_sinks, f)
+#define TIMER_DOPAIR_SINKS _TIMER_DOPAIR_SINKS(FUNCTION)
+
+#define _TIMER_DOSUB_SELF_SINKS(f) PASTE(timer_dosub_self_sinks, f)
+#define TIMER_DOSUB_SELF_SINKS _TIMER_DOSUB_SELF_SINKS(FUNCTION)
+
+#define _TIMER_DOSUB_PAIR_SINKS(f) PASTE(timer_dosub_pair_sinks, f)
+#define TIMER_DOSUB_PAIR_SINKS _TIMER_DOSUB_PAIR_SINKS(FUNCTION)
+
+#define _IACT_SINKS_GAS(f) PASTE(runner_iact_nonsym_sinks_gas, f)
+#define IACT_SINKS_GAS _IACT_SINKS_GAS(FUNCTION)
+
+#define _IACT_SINKS_SINK(f) PASTE(runner_iact_nonsym_sinks_sink, f)
+#define IACT_SINKS_SINK _IACT_SINKS_SINK(FUNCTION)
+
+void DOSELF1_BRANCH_SINKS(struct runner *r, struct cell *c);
+void DOPAIR1_BRANCH_SINKS(struct runner *r, struct cell *ci, struct cell *cj);
+
+void DOSUB_SELF1_SINKS(struct runner *r, struct cell *ci, int gettimer);
+void DOSUB_PAIR1_SINKS(struct runner *r, struct cell *ci, struct cell *cj,
+                       int gettimer);
+
+void DOSELF1_SUBSET_BRANCH_SINKS(struct runner *r, struct cell *restrict ci,
+                                 struct sink *restrict sinks, int *restrict ind,
+                                 const int scount);
+void DOPAIR1_SUBSET_BRANCH_SINKS(struct runner *r, struct cell *restrict ci,
+                                 struct sink *restrict sinks_i,
+                                 int *restrict ind, int const scount,
+                                 struct cell *restrict cj);
+
+void DOSUB_SUBSET_SINKS(struct runner *r, struct cell *ci, struct sink *sinks,
+                        int *ind, const int scount, struct cell *cj,
+                        int gettimer);
\ No newline at end of file
diff --git a/src/runner_doiact_stars.h b/src/runner_doiact_stars.h
index 1bc31f5e92c3f95aa0c9c0ffef6fcf2276eeb12f..4e991c2cdfcb3de4e371d89911f50cbf59cc8fce 100644
--- a/src/runner_doiact_stars.h
+++ b/src/runner_doiact_stars.h
@@ -59,6 +59,15 @@
 #define _DOSUB_SUBSET_STARS(f) PASTE(runner_dosub_subset_stars, f)
 #define DOSUB_SUBSET_STARS _DOSUB_SUBSET_STARS(FUNCTION)
 
+#define _DOSUB_SELF_SUBSET_STARS(f) PASTE(runner_dosub_self_subset_stars, f)
+#define DOSUB_SELF_SUBSET_STARS _DOSUB_SELF_SUBSET_STARS(FUNCTION)
+
+#define _DOSUB_PAIR_SUBSET_STARS(f) PASTE(runner_dosub_pair_subset_stars, f)
+#define DOSUB_PAIR_SUBSET_STARS _DOSUB_PAIR_SUBSET_STARS(FUNCTION)
+
+#define _FIND_SUB_STARS(f) PASTE(runner_find_sub_stars, f)
+#define FIND_SUB_STARS _FIND_SUB_STARS(FUNCTION)
+
 #define _DOSELF1_BRANCH_STARS(f) PASTE(runner_doself_branch_stars, f)
 #define DOSELF1_BRANCH_STARS _DOSELF1_BRANCH_STARS(FUNCTION)
 
@@ -86,21 +95,34 @@
 #define _IACT_STARS(f) PASTE(runner_iact_nonsym_stars, f)
 #define IACT_STARS _IACT_STARS(FUNCTION)
 
-void DOSELF1_BRANCH_STARS(struct runner *r, struct cell *c);
-void DOPAIR1_BRANCH_STARS(struct runner *r, struct cell *ci, struct cell *cj);
+void DOSELF1_BRANCH_STARS(struct runner *r, const struct cell *c,
+                          const int limit_min_h, const int limit_max_h);
+void DOPAIR1_BRANCH_STARS(struct runner *r, struct cell *ci, struct cell *cj,
+                          const int limit_min_h, const int limit_max_h);
 
-void DOSUB_SELF1_STARS(struct runner *r, struct cell *ci, int gettimer);
+void DOSUB_SELF1_STARS(struct runner *r, struct cell *ci,
+                       int recurse_below_h_max, const int gettimer);
 void DOSUB_PAIR1_STARS(struct runner *r, struct cell *ci, struct cell *cj,
-                       int gettimer);
+                       int recurse_below_h_max, const int gettimer);
 
-void DOSELF1_SUBSET_BRANCH_STARS(struct runner *r, struct cell *restrict ci,
-                                 struct spart *restrict sparts,
-                                 int *restrict ind, int scount);
+void DOSELF1_SUBSET_BRANCH_STARS(struct runner *r, const struct cell *ci,
+                                 struct spart *restrict sparts, const int *ind,
+                                 const int scount);
 
-void DOPAIR1_SUBSET_BRANCH_STARS(struct runner *r, struct cell *restrict ci,
+void DOPAIR1_SUBSET_BRANCH_STARS(struct runner *r,
+                                 const struct cell *restrict ci,
                                  struct spart *restrict sparts_i,
-                                 int *restrict ind, int scount,
+                                 const int *ind, const int scount,
                                  struct cell *restrict cj);
 
+void DOSUB_PAIR_SUBSET_STARS(struct runner *r, struct cell *ci,
+                             struct spart *sparts, const int *ind,
+                             const int scount, struct cell *cj,
+                             const int gettimer);
+
+void DOSUB_SELF_SUBSET_STARS(struct runner *r, struct cell *ci,
+                             struct spart *sparts, const int *ind,
+                             const int scount, const int gettimer);
+
 void DOSUB_SUBSET_STARS(struct runner *r, struct cell *ci, struct spart *sparts,
                         int *ind, int scount, struct cell *cj, int gettimer);
diff --git a/src/runner_doiact_undef.h b/src/runner_doiact_undef.h
index 0a76c6d15df5a535a6cf7e93f6b54a9972b60bfa..a4e7f2e6d0d74f3fdc43db6503b595768e85d349 100644
--- a/src/runner_doiact_undef.h
+++ b/src/runner_doiact_undef.h
@@ -30,6 +30,8 @@
 #undef IACT_STARS
 #undef IACT_BH_GAS
 #undef IACT_BH_BH
+#undef IACT_SINKS_GAS
+#undef IACT_SINKS_SINK
 #undef GET_MU0
 #undef FUNCTION
 #undef FUNCTION_TASK_LOOP
diff --git a/src/runner_ghost.c b/src/runner_ghost.c
index 091589f52919b7a370ceaae5809fb16ef47c066e..830298e2cae955fbc4756e922b41e2a00092a94c 100644
--- a/src/runner_ghost.c
+++ b/src/runner_ghost.c
@@ -27,12 +27,14 @@
 
 /* Local headers. */
 #include "active.h"
+#include "adaptive_softening.h"
 #include "black_holes.h"
 #include "cell.h"
 #include "engine.h"
 #include "feedback.h"
 #include "mhd.h"
 #include "rt.h"
+#include "sink.h"
 #include "space_getsid.h"
 #include "star_formation.h"
 #include "stars.h"
@@ -61,6 +63,13 @@
 #undef FUNCTION_TASK_LOOP
 #undef FUNCTION
 
+/* Import the sink density loop functions. */
+#define FUNCTION density
+#define FUNCTION_TASK_LOOP TASK_LOOP_DENSITY
+#include "runner_doiact_sinks.h"
+#undef FUNCTION_TASK_LOOP
+#undef FUNCTION
+
 /**
  * @brief Intermediate task after the density to check that the smoothing
  * lengths are correct.
@@ -90,7 +99,7 @@ void runner_do_stars_ghost(struct runner *r, struct cell *c, int timer) {
 
   /* Running value of the maximal smoothing length */
   float h_max = c->stars.h_max;
-  float h_max_active = c->stars.h_max_active;
+  float h_max_active = 0.f;
 
   TIMER_TIC;
 
@@ -288,6 +297,13 @@ void runner_do_stars_ghost(struct runner *r, struct cell *c, int timer) {
                                                rt_props, phys_const, us);
             }
 
+            if (feedback_is_active(sp, e) || with_rt) {
+
+              /* Check if h_max has increased */
+              h_max = max(h_max, sp->h);
+              h_max_active = max(h_max_active, sp->h);
+            }
+
             /* Ok, we are done with this particle */
             continue;
           }
@@ -381,6 +397,9 @@ void runner_do_stars_ghost(struct runner *r, struct cell *c, int timer) {
 
         /* We now have a particle whose smoothing length has converged */
 
+        /* Set the correct depth */
+        cell_set_spart_h_depth(sp, c);
+
         /* Check if h_max has increased */
         h_max = max(h_max, sp->h);
         h_max_active = max(h_max_active, sp->h);
@@ -498,19 +517,19 @@ void runner_do_stars_ghost(struct runner *r, struct cell *c, int timer) {
 
             /* Otherwise, sub-self interaction? */
             else if (l->t->type == task_type_sub_self)
-              runner_dosub_subset_stars_density(r, finger, sparts, sid, scount,
-                                                NULL, 1);
+              runner_dosub_self_subset_stars_density(r, finger, sparts, sid,
+                                                     scount, 1);
 
             /* Otherwise, sub-pair interaction? */
             else if (l->t->type == task_type_sub_pair) {
 
               /* Left or right? */
               if (l->t->ci == finger)
-                runner_dosub_subset_stars_density(r, finger, sparts, sid,
-                                                  scount, l->t->cj, 1);
+                runner_dosub_pair_subset_stars_density(r, finger, sparts, sid,
+                                                       scount, l->t->cj, 1);
               else
-                runner_dosub_subset_stars_density(r, finger, sparts, sid,
-                                                  scount, l->t->ci, 1);
+                runner_dosub_pair_subset_stars_density(r, finger, sparts, sid,
+                                                       scount, l->t->ci, 1);
             }
           }
         }
@@ -549,7 +568,9 @@ void runner_do_stars_ghost(struct runner *r, struct cell *c, int timer) {
 
     if (h > c->stars.h_max)
       error("Particle has h larger than h_max (id=%lld)", sp->id);
-    if (spart_is_active(sp, e) && h > c->stars.h_max_active)
+
+    if (spart_is_active(sp, e) && (feedback_is_active(sp, e) || with_rt) &&
+        (h > c->stars.h_max_active))
       error("Active particle has h larger than h_max_active (id=%lld)", sp->id);
   }
 #endif
@@ -590,7 +611,7 @@ void runner_do_black_holes_density_ghost(struct runner *r, struct cell *c,
 
   /* Running value of the maximal smoothing length */
   float h_max = c->black_holes.h_max;
-  float h_max_active = c->black_holes.h_max_active;
+  float h_max_active = 0.f;
 
   TIMER_TIC;
 
@@ -709,6 +730,10 @@ void runner_do_black_holes_density_ghost(struct runner *r, struct cell *c,
 
             black_holes_reset_feedback(bp);
 
+            /* Check if h_max has increased */
+            h_max = max(h_max, bp->h);
+            h_max_active = max(h_max_active, bp->h);
+
             /* Ok, we are done with this particle */
             continue;
           }
@@ -803,6 +828,9 @@ void runner_do_black_holes_density_ghost(struct runner *r, struct cell *c,
 
         black_holes_reset_feedback(bp);
 
+        /* Set the correct depth */
+        cell_set_bpart_h_depth(bp, c);
+
         /* Check if h_max has increased */
         h_max = max(h_max, bp->h);
         h_max_active = max(h_max_active, bp->h);
@@ -1110,7 +1138,7 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
 
   /* Running value of the maximal smoothing length */
   float h_max = c->hydro.h_max;
-  float h_max_active = c->hydro.h_max_active;
+  float h_max_active = 0.f;
 
   TIMER_TIC;
 
@@ -1199,6 +1227,7 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
 
           /* Finish the density calculation */
           hydro_end_density(p, cosmo);
+          adaptive_softening_end_density(p, e->gravity_properties);
           mhd_end_density(p, cosmo);
           chemistry_end_density(p, chemistry, cosmo);
           star_formation_end_density(p, xp, star_formation, cosmo);
@@ -1311,6 +1340,10 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
               rt_reset_part(p, cosmo);
             }
 
+            /* Check if h_max has increased */
+            h_max = max(h_max, p->h);
+            h_max_active = max(h_max_active, p->h);
+
             /* Ok, we are done with this particle */
             continue;
           }
@@ -1379,6 +1412,7 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
 
             /* Re-initialise everything */
             hydro_init_part(p, hs);
+            adaptive_softening_init_part(p);
             mhd_init_part(p);
             chemistry_init_part(p, chemistry);
             star_formation_init_part(p, star_formation);
@@ -1420,12 +1454,19 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
 
         /* We now have a particle whose smoothing length has converged */
 
+        /* Set the correct depth */
+        cell_set_part_h_depth(p, c);
+
         /* Check if h_max has increased */
         h_max = max(h_max, p->h);
         h_max_active = max(h_max_active, p->h);
 
         ghost_stats_converged_hydro(&c->ghost_statistics, p);
 
+        /* Update gravitational softening (in adaptive softening case) */
+        if (p->gpart)
+          gravity_update_softening(p->gpart, p, e->gravity_properties);
+
 #ifdef EXTRA_HYDRO_LOOP
 
         /* As of here, particle gradient variables will be set. */
@@ -1528,19 +1569,18 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
 
             /* Otherwise, sub-self interaction? */
             else if (l->t->type == task_type_sub_self)
-              runner_dosub_subset_density(r, finger, parts, pid, count, NULL,
-                                          1);
+              runner_dosub_self_subset_density(r, finger, parts, pid, count, 1);
 
             /* Otherwise, sub-pair interaction? */
             else if (l->t->type == task_type_sub_pair) {
 
               /* Left or right? */
               if (l->t->ci == finger)
-                runner_dosub_subset_density(r, finger, parts, pid, count,
-                                            l->t->cj, 1);
+                runner_dosub_pair_subset_density(r, finger, parts, pid, count,
+                                                 l->t->cj, 1);
               else
-                runner_dosub_subset_density(r, finger, parts, pid, count,
-                                            l->t->ci, 1);
+                runner_dosub_pair_subset_density(r, finger, parts, pid, count,
+                                                 l->t->ci, 1);
             }
           }
         }
@@ -1700,3 +1740,404 @@ void runner_do_rt_ghost2(struct runner *r, struct cell *c, int timer) {
 
   if (timer) TIMER_TOC(timer_do_rt_ghost2);
 }
+
+/**
+ * @brief Intermediate task after the density to check that the smoothing
+ * lengths are correct, finish density calculations
+ * and calculate accretion rates for the particle swallowing step
+ *
+ * @param r The runner thread.
+ * @param c The cell.
+ * @param timer Are we timing this ?
+ */
+void runner_do_sinks_density_ghost(struct runner *r, struct cell *c,
+                                   int timer) {
+
+  struct sink *restrict sinks = c->sinks.parts;
+  const struct engine *e = r->e;
+  const struct cosmology *cosmo = e->cosmology;
+  const int with_cosmology = e->policy & engine_policy_cosmology;
+  const float sinks_h_max = e->hydro_properties->h_max;
+  const float sinks_h_min = e->hydro_properties->h_min;
+  const float eps = e->sink_properties->h_tolerance;
+  const float sinks_eta_dim = pow_dimension(e->sink_properties->eta_neighbours);
+  const int max_smoothing_iter = e->hydro_properties->max_smoothing_iterations;
+  int redo = 0, scount = 0;
+
+  /* Running value of the maximal smoothing length */
+  float h_max = c->sinks.h_max;
+  float h_max_active = 0.f;
+
+  TIMER_TIC;
+
+  /* Anything to do here? */
+  if (c->sinks.count == 0) return;
+  if (!cell_is_active_sinks(c, e)) return;
+
+  /* Recurse? */
+  if (c->split) {
+    for (int k = 0; k < 8; k++) {
+      if (c->progeny[k] != NULL) {
+        runner_do_sinks_density_ghost(r, c->progeny[k], 0);
+
+        /* Update h_max */
+        h_max = max(h_max, c->progeny[k]->sinks.h_max);
+        h_max_active = max(h_max_active, c->progeny[k]->sinks.h_max_active);
+      }
+    }
+  } else {
+
+    /* Init the list of active particles that have to be updated. */
+    int *sid = NULL;
+    float *h_0 = NULL;
+    float *left = NULL;
+    float *right = NULL;
+    if ((sid = (int *)malloc(sizeof(int) * c->sinks.count)) == NULL)
+      error("Can't allocate memory for sid.");
+    if ((h_0 = (float *)malloc(sizeof(float) * c->sinks.count)) == NULL)
+      error("Can't allocate memory for h_0.");
+    if ((left = (float *)malloc(sizeof(float) * c->sinks.count)) == NULL)
+      error("Can't allocate memory for left.");
+    if ((right = (float *)malloc(sizeof(float) * c->sinks.count)) == NULL)
+      error("Can't allocate memory for right.");
+    for (int k = 0; k < c->sinks.count; k++)
+      if (sink_is_active(&sinks[k], e)) {
+        sid[scount] = k;
+        h_0[scount] = sinks[k].h;
+        left[scount] = 0.f;
+        right[scount] = sinks_h_max;
+        ++scount;
+      }
+
+    if (e->sink_properties->use_fixed_r_cut) {
+      /* If we're using a fixed cutoff rather than a smoothing length, just
+       * finish up the density task and leave sp->h untouched. */
+
+      /* Loop over the active sinks in this cell. */
+      for (int i = 0; i < scount; i++) {
+
+        /* Get a direct pointer on the part. */
+        struct sink *sp = &sinks[sid[i]];
+
+#ifdef SWIFT_DEBUG_CHECKS
+        /* Is this part within the timestep? */
+        if (!sink_is_active(sp, e)) error("Ghost applied to inactive particle");
+#endif
+
+        /* Finish the density calculation */
+        sink_end_density(sp, cosmo);
+
+        /* Set these variables to the fixed cutoff radius for the rest of the
+         * ghost task */
+        h_max = sp->h;
+        h_max_active = sp->h;
+      }
+
+    } else {
+      /* Otherwise we need to iterate to update the smoothing lengths */
+
+      /* While there are particles that need to be updated... */
+      for (int num_reruns = 0; scount > 0 && num_reruns < max_smoothing_iter;
+           num_reruns++) {
+
+        /* Reset the redo-count. */
+        redo = 0;
+
+        /* Loop over the remaining active parts in this cell. */
+        for (int i = 0; i < scount; i++) {
+
+          /* Get a direct pointer on the part. */
+          struct sink *sp = &sinks[sid[i]];
+
+#ifdef SWIFT_DEBUG_CHECKS
+          /* Is this part within the timestep? */
+          if (!sink_is_active(sp, e))
+            error("Ghost applied to inactive particle");
+#endif
+
+          /* Get some useful values */
+          const float h_init = h_0[i];
+          const float h_old = sp->h;
+          const float h_old_dim = pow_dimension(h_old);
+          const float h_old_dim_minus_one = pow_dimension_minus_one(h_old);
+
+          float h_new;
+          int has_no_neighbours = 0;
+
+          if (sp->density.wcount <
+              1.e-5 * kernel_root) { /* No neighbours case */
+
+            /* Flag that there were no neighbours */
+            has_no_neighbours = 1;
+
+            /* Double h and try again */
+            h_new = 2.f * h_old;
+
+          } else {
+
+            /* Finish the density calculation */
+            sink_end_density(sp, cosmo);
+
+            /* Compute one step of the Newton-Raphson scheme */
+            const float n_sum = sp->density.wcount * h_old_dim;
+            const float n_target = sinks_eta_dim;
+            const float f = n_sum - n_target;
+            const float f_prime =
+                sp->density.wcount_dh * h_old_dim +
+                hydro_dimension * sp->density.wcount * h_old_dim_minus_one;
+
+            /* Improve the bisection bounds */
+            if (n_sum < n_target)
+              left[i] = max(left[i], h_old);
+            else if (n_sum > n_target)
+              right[i] = min(right[i], h_old);
+
+#ifdef SWIFT_DEBUG_CHECKS
+            /* Check the validity of the left and right bounds */
+            if (left[i] > right[i])
+              error("Invalid left (%e) and right (%e)", left[i], right[i]);
+#endif
+
+            /* Skip if h is already h_max and we don't have enough neighbours
+             */
+            /* Same if we are below h_min */
+            if (((sp->h >= sinks_h_max) && (f < 0.f)) ||
+                ((sp->h <= sinks_h_min) && (f > 0.f))) {
+
+              /* Check if h_max has increased */
+              h_max = max(h_max, sp->h);
+              h_max_active = max(h_max_active, sp->h);
+
+              /* Ok, we are done with this particle */
+              continue;
+            }
+
+            /* Normal case: Use Newton-Raphson to get a better value of h */
+
+            /* Avoid floating point exception from f_prime = 0 */
+            h_new = h_old - f / (f_prime + FLT_MIN);
+
+            /* Be verbose about the particles that struggle to converge */
+            if (num_reruns > max_smoothing_iter - 10) {
+
+              message(
+                  "Smoothing length convergence problem: iter=%d p->id=%lld "
+                  "h_init=%12.8e h_old=%12.8e h_new=%12.8e f=%f f_prime=%f "
+                  "n_sum=%12.8e n_target=%12.8e left=%12.8e right=%12.8e",
+                  num_reruns, sp->id, h_init, h_old, h_new, f, f_prime, n_sum,
+                  n_target, left[i], right[i]);
+            }
+
+            /* Safety check: truncate to the range [ h_old/2 , 2h_old ]. */
+            h_new = min(h_new, 2.f * h_old);
+            h_new = max(h_new, 0.5f * h_old);
+
+            /* Verify that we are actually progrssing towards the answer */
+            h_new = max(h_new, left[i]);
+            h_new = min(h_new, right[i]);
+          }
+
+          /* Check whether the particle has an inappropriate smoothing length
+           */
+          if (fabsf(h_new - h_old) > eps * h_old) {
+
+            /* Ok, correct then */
+
+            /* Case where we have been oscillating around the solution */
+            if ((h_new == left[i] && h_old == right[i]) ||
+                (h_old == left[i] && h_new == right[i])) {
+
+              /* Bisect the remaining interval */
+              sp->h = pow_inv_dimension(
+                  0.5f * (pow_dimension(left[i]) + pow_dimension(right[i])));
+
+            } else {
+
+              /* Normal case */
+              sp->h = h_new;
+            }
+
+            /* If below the absolute maximum, try again */
+            if (sp->h < sinks_h_max && sp->h > sinks_h_min) {
+
+              /* Flag for another round of fun */
+              sid[redo] = sid[i];
+              h_0[redo] = h_0[i];
+              left[redo] = left[i];
+              right[redo] = right[i];
+              redo += 1;
+
+              /* Re-initialise everything */
+              sink_init_sink(sp);
+
+              /* Off we go ! */
+              continue;
+
+            } else if (sp->h <= sinks_h_min) {
+
+              /* Ok, this particle is a lost cause... */
+              sp->h = sinks_h_min;
+
+            } else if (sp->h >= sinks_h_max) {
+
+              /* Ok, this particle is a lost cause... */
+              sp->h = sinks_h_max;
+
+              /* Do some damage control if no neighbours at all were found */
+              if (has_no_neighbours) {
+                sinks_sink_has_no_neighbours(sp, cosmo);
+              }
+
+            } else {
+              error(
+                  "Fundamental problem with the smoothing length iteration "
+                  "logic.");
+            }
+          }
+
+          /* We now have a particle whose smoothing length has converged */
+
+          /* Set the correct depth */
+          cell_set_sink_h_depth(sp, c);
+
+          /* Check if h_max has increased */
+          h_max = max(h_max, sp->h);
+          h_max_active = max(h_max_active, sp->h);
+        }
+
+        /* We now need to treat the particles whose smoothing length had not
+         * converged again */
+
+        /* Re-set the counter for the next loop (potentially). */
+        scount = redo;
+        if (scount > 0) {
+
+          /* Climb up the cell hierarchy. */
+          for (struct cell *finger = c; finger != NULL;
+               finger = finger->parent) {
+
+            /* Run through this cell's density interactions. */
+            for (struct link *l = finger->sinks.density; l != NULL;
+                 l = l->next) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+              if (l->t->ti_run < r->e->ti_current)
+                error("Density task should have been run.");
+#endif
+
+              /* Self-interaction? */
+              if (l->t->type == task_type_self)
+                runner_doself_subset_branch_sinks_density(r, finger, sinks, sid,
+                                                          scount);
+
+              /* Otherwise, pair interaction? */
+              else if (l->t->type == task_type_pair) {
+
+                /* Left or right? */
+                if (l->t->ci == finger)
+                  runner_dopair_subset_branch_sinks_density(
+                      r, finger, sinks, sid, scount, l->t->cj);
+                else
+                  runner_dopair_subset_branch_sinks_density(
+                      r, finger, sinks, sid, scount, l->t->ci);
+              }
+
+              /* Otherwise, sub-self interaction? */
+              else if (l->t->type == task_type_sub_self)
+                runner_dosub_subset_sinks_density(r, finger, sinks, sid, scount,
+                                                  NULL, 1);
+
+              /* Otherwise, sub-pair interaction? */
+              else if (l->t->type == task_type_sub_pair) {
+
+                /* Left or right? */
+                if (l->t->ci == finger)
+                  runner_dosub_subset_sinks_density(r, finger, sinks, sid,
+                                                    scount, l->t->cj, 1);
+                else
+                  runner_dosub_subset_sinks_density(r, finger, sinks, sid,
+                                                    scount, l->t->ci, 1);
+              }
+            }
+          }
+        }
+      }
+
+      if (scount) {
+        warning(
+            "Smoothing length failed to converge for the following sink "
+            "particles:");
+        for (int i = 0; i < scount; i++) {
+          struct sink *sp = &sinks[sid[i]];
+          warning("ID: %lld, h: %g, wcount: %g", sp->id, sp->h,
+                  sp->density.wcount);
+        }
+
+        error("Smoothing length failed to converge on %i particles.", scount);
+      }
+
+      /* Be clean */
+      free(left);
+      free(right);
+      free(sid);
+      free(h_0);
+    }
+
+    /* We need one more quick loop over the sinks to run prepare_swallow */
+    for (int i = 0; i < c->sinks.count; i++) {
+
+      /* Get a direct pointer on the part. */
+      struct sink *sp = &sinks[i];
+
+      if (sink_is_active(sp, e)) {
+
+        /* Get particle time-step */
+        double dt;
+        if (with_cosmology) {
+          const integertime_t ti_step = get_integer_timestep(sp->time_bin);
+          const integertime_t ti_begin =
+              get_integer_time_begin(e->ti_current - 1, sp->time_bin);
+
+          dt = cosmology_get_delta_time(e->cosmology, ti_begin,
+                                        ti_begin + ti_step);
+        } else {
+          dt = get_timestep(sp->time_bin, e->time_base);
+        }
+
+        /* Calculate the accretion rate and accreted mass this timestep, for use
+         * in swallow loop */
+        sink_prepare_swallow(sp, e->sink_properties, e->physical_constants,
+                             e->cosmology, e->cooling_func, e->entropy_floor,
+                             e->time, with_cosmology, dt, e->ti_current);
+      }
+    }
+  }
+
+  /* Update h_max */
+  c->sinks.h_max = h_max;
+  c->sinks.h_max_active = h_max_active;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  for (int i = 0; i < c->sinks.count; ++i) {
+    const struct sink *sp = &c->sinks.parts[i];
+    const float h = c->sinks.parts[i].h;
+    if (sink_is_inhibited(sp, e)) continue;
+
+    if (h > c->sinks.h_max)
+      error("Particle has h larger than h_max (id=%lld)", sp->id);
+    if (sink_is_active(sp, e) && h > c->sinks.h_max_active)
+      error("Active particle has h larger than h_max_active (id=%lld)", sp->id);
+  }
+#endif
+
+  /* The ghost may not always be at the top level.
+   * Therefore we need to update h_max between the super- and top-levels */
+  if (c->sinks.density_ghost) {
+    for (struct cell *tmp = c->parent; tmp != NULL; tmp = tmp->parent) {
+      atomic_max_f(&tmp->sinks.h_max, h_max);
+      atomic_max_f(&tmp->sinks.h_max_active, h_max_active);
+    }
+  }
+
+  if (timer) TIMER_TOC(timer_do_sinks_ghost);
+}
diff --git a/src/runner_main.c b/src/runner_main.c
index 8c32c7d709430ecbbfbe64856db4e287b0620849..e6b3b1ef0510b952164487dba5e511899e836795 100644
--- a/src/runner_main.c
+++ b/src/runner_main.c
@@ -33,7 +33,6 @@
 /* Local headers. */
 #include "engine.h"
 #include "feedback.h"
-#include "runner_doiact_sinks.h"
 #include "scheduler.h"
 #include "space_getsid.h"
 #include "timers.h"
@@ -113,6 +112,18 @@
 #include "runner_doiact_black_holes.h"
 #include "runner_doiact_undef.h"
 
+/* Import the sink density loop functions. */
+#define FUNCTION density
+#define FUNCTION_TASK_LOOP TASK_LOOP_DENSITY
+#include "runner_doiact_sinks.h"
+#include "runner_doiact_undef.h"
+
+/* Import the sink swallow loop functions. */
+#define FUNCTION swallow
+#define FUNCTION_TASK_LOOP TASK_LOOP_SWALLOW
+#include "runner_doiact_sinks.h"
+#include "runner_doiact_undef.h"
+
 /* Import the RT gradient loop functions */
 #define FUNCTION rt_gradient
 #define FUNCTION_TASK_LOOP TASK_LOOP_RT_GRADIENT
@@ -177,7 +188,7 @@ void *runner_main(void *data) {
         struct cell *ci_temp = ci;
         struct cell *cj_temp = cj;
         double shift[3];
-        t->sid = space_getsid(e->s, &ci_temp, &cj_temp, shift);
+        t->sid = space_getsid_and_swap_cells(e->s, &ci_temp, &cj_temp, shift);
       } else {
         t->sid = -1;
       }
@@ -194,30 +205,43 @@ void *runner_main(void *data) {
       /* Different types of tasks... */
       switch (t->type) {
         case task_type_self:
-          if (t->subtype == task_subtype_density)
-            runner_doself1_branch_density(r, ci);
+          if (t->subtype == task_subtype_grav)
+            runner_doself_recursive_grav(r, ci, 1);
+          else if (t->subtype == task_subtype_external_grav)
+            runner_do_grav_external(r, ci, 1);
+          else if (t->subtype == task_subtype_density)
+            runner_doself1_branch_density(r, ci, /*limit_h_min=*/0,
+                                          /*limit_h_max=*/0);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
-            runner_doself1_branch_gradient(r, ci);
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+            runner_doself2_branch_gradient(r, ci, /*limit_h_min=*/0,
+                                           /*limit_h_max=*/0);
+#else
+            runner_doself1_branch_gradient(r, ci, /*limit_h_min=*/0,
+                                           /*limit_h_max=*/0);
+#endif
 #endif
           else if (t->subtype == task_subtype_force)
-            runner_doself2_branch_force(r, ci);
+            runner_doself2_branch_force(r, ci, /*limit_h_min=*/0,
+                                        /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_limiter)
-            runner_doself1_branch_limiter(r, ci);
-          else if (t->subtype == task_subtype_grav)
-            runner_doself_recursive_grav(r, ci, 1);
-          else if (t->subtype == task_subtype_external_grav)
-            runner_do_grav_external(r, ci, 1);
+            runner_doself1_branch_limiter(r, ci, /*limit_h_min=*/0,
+                                          /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_stars_density)
-            runner_doself_branch_stars_density(r, ci);
+            runner_doself_branch_stars_density(r, ci, /*limit_h_min=*/0,
+                                               /*limit_h_max=*/0);
 #ifdef EXTRA_STAR_LOOPS
           else if (t->subtype == task_subtype_stars_prep1)
-            runner_doself_branch_stars_prep1(r, ci);
+            runner_doself_branch_stars_prep1(r, ci, /*limit_h_min=*/0,
+                                             /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_stars_prep2)
-            runner_doself_branch_stars_prep2(r, ci);
+            runner_doself_branch_stars_prep2(r, ci, /*limit_h_min=*/0,
+                                             /*limit_h_max=*/0);
 #endif
           else if (t->subtype == task_subtype_stars_feedback)
-            runner_doself_branch_stars_feedback(r, ci);
+            runner_doself_branch_stars_feedback(r, ci, /*limit_h_min=*/0,
+                                                /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_bh_density)
             runner_doself_branch_bh_density(r, ci);
           else if (t->subtype == task_subtype_bh_swallow)
@@ -229,9 +253,13 @@ void *runner_main(void *data) {
           else if (t->subtype == task_subtype_bh_feedback)
             runner_doself_branch_bh_feedback(r, ci);
           else if (t->subtype == task_subtype_rt_gradient)
-            runner_doself1_branch_rt_gradient(r, ci);
+            runner_doself1_branch_rt_gradient(r, ci, /*limit_h_min=*/0,
+                                              /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_rt_transport)
-            runner_doself2_branch_rt_transport(r, ci);
+            runner_doself2_branch_rt_transport(r, ci, /*limit_h_min=*/0,
+                                               /*limit_h_max=*/0);
+          else if (t->subtype == task_subtype_sink_density)
+            runner_doself_branch_sinks_density(r, ci);
           else if (t->subtype == task_subtype_sink_swallow)
             runner_doself_branch_sinks_swallow(r, ci);
           else if (t->subtype == task_subtype_sink_do_gas_swallow)
@@ -244,28 +272,41 @@ void *runner_main(void *data) {
           break;
 
         case task_type_pair:
-          if (t->subtype == task_subtype_density)
-            runner_dopair1_branch_density(r, ci, cj);
+          if (t->subtype == task_subtype_grav)
+            runner_dopair_recursive_grav(r, ci, cj, 1);
+          else if (t->subtype == task_subtype_density)
+            runner_dopair1_branch_density(r, ci, cj, /*limit_h_min=*/0,
+                                          /*limit_h_max=*/0);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
-            runner_dopair1_branch_gradient(r, ci, cj);
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+            runner_dopair2_branch_gradient(r, ci, cj, /*limit_h_min=*/0,
+                                           /*limit_h_max=*/0);
+#else
+            runner_dopair1_branch_gradient(r, ci, cj, /*limit_h_min=*/0,
+                                           /*limit_h_max=*/0);
+#endif
 #endif
           else if (t->subtype == task_subtype_force)
-            runner_dopair2_branch_force(r, ci, cj);
+            runner_dopair2_branch_force(r, ci, cj, /*limit_h_min=*/0,
+                                        /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_limiter)
-            runner_dopair1_branch_limiter(r, ci, cj);
-          else if (t->subtype == task_subtype_grav)
-            runner_dopair_recursive_grav(r, ci, cj, 1);
+            runner_dopair1_branch_limiter(r, ci, cj, /*limit_h_min=*/0,
+                                          /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_stars_density)
-            runner_dopair_branch_stars_density(r, ci, cj);
+            runner_dopair_branch_stars_density(r, ci, cj, /*limit_h_min=*/0,
+                                               /*limit_h_max=*/0);
 #ifdef EXTRA_STAR_LOOPS
           else if (t->subtype == task_subtype_stars_prep1)
-            runner_dopair_branch_stars_prep1(r, ci, cj);
+            runner_dopair_branch_stars_prep1(r, ci, cj, /*limit_h_min=*/0,
+                                             /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_stars_prep2)
-            runner_dopair_branch_stars_prep2(r, ci, cj);
+            runner_dopair_branch_stars_prep2(r, ci, cj, /*limit_h_min=*/0,
+                                             /*limit_h_max=*/0);
 #endif
           else if (t->subtype == task_subtype_stars_feedback)
-            runner_dopair_branch_stars_feedback(r, ci, cj);
+            runner_dopair_branch_stars_feedback(r, ci, cj, /*limit_h_min=*/0,
+                                                /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_bh_density)
             runner_dopair_branch_bh_density(r, ci, cj);
           else if (t->subtype == task_subtype_bh_swallow)
@@ -277,9 +318,13 @@ void *runner_main(void *data) {
           else if (t->subtype == task_subtype_bh_feedback)
             runner_dopair_branch_bh_feedback(r, ci, cj);
           else if (t->subtype == task_subtype_rt_gradient)
-            runner_dopair1_branch_rt_gradient(r, ci, cj);
+            runner_dopair1_branch_rt_gradient(r, ci, cj, /*limit_h_min=*/0,
+                                              /*limit_h_max=*/0);
           else if (t->subtype == task_subtype_rt_transport)
-            runner_dopair2_branch_rt_transport(r, ci, cj);
+            runner_dopair2_branch_rt_transport(r, ci, cj, /*limit_h_min=*/0,
+                                               /*limit_h_max=*/0);
+          else if (t->subtype == task_subtype_sink_density)
+            runner_dopair_branch_sinks_density(r, ci, cj);
           else if (t->subtype == task_subtype_sink_swallow)
             runner_dopair_branch_sinks_swallow(r, ci, cj);
           else if (t->subtype == task_subtype_sink_do_gas_swallow)
@@ -293,25 +338,29 @@ void *runner_main(void *data) {
 
         case task_type_sub_self:
           if (t->subtype == task_subtype_density)
-            runner_dosub_self1_density(r, ci, 1);
+            runner_dosub_self1_density(r, ci, /*below_h_max=*/0, 1);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
-            runner_dosub_self1_gradient(r, ci, 1);
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+            runner_dosub_self2_gradient(r, ci, /*below_h_max=*/0, 1);
+#else
+            runner_dosub_self1_gradient(r, ci, /*below_h_max=*/0, 1);
+#endif
 #endif
           else if (t->subtype == task_subtype_force)
-            runner_dosub_self2_force(r, ci, 1);
+            runner_dosub_self2_force(r, ci, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_limiter)
-            runner_dosub_self1_limiter(r, ci, 1);
+            runner_dosub_self1_limiter(r, ci, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_stars_density)
-            runner_dosub_self_stars_density(r, ci, 1);
+            runner_dosub_self_stars_density(r, ci, /*below_h_max=*/0, 1);
 #ifdef EXTRA_STAR_LOOPS
           else if (t->subtype == task_subtype_stars_prep1)
-            runner_dosub_self_stars_prep1(r, ci, 1);
+            runner_dosub_self_stars_prep1(r, ci, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_stars_prep2)
-            runner_dosub_self_stars_prep2(r, ci, 1);
+            runner_dosub_self_stars_prep2(r, ci, /*below_h_max=*/0, 1);
 #endif
           else if (t->subtype == task_subtype_stars_feedback)
-            runner_dosub_self_stars_feedback(r, ci, 1);
+            runner_dosub_self_stars_feedback(r, ci, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_bh_density)
             runner_dosub_self_bh_density(r, ci, 1);
           else if (t->subtype == task_subtype_bh_swallow)
@@ -323,9 +372,11 @@ void *runner_main(void *data) {
           else if (t->subtype == task_subtype_bh_feedback)
             runner_dosub_self_bh_feedback(r, ci, 1);
           else if (t->subtype == task_subtype_rt_gradient)
-            runner_dosub_self1_rt_gradient(r, ci, 1);
+            runner_dosub_self1_rt_gradient(r, ci, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_rt_transport)
-            runner_dosub_self2_rt_transport(r, ci, 1);
+            runner_dosub_self2_rt_transport(r, ci, /*below_h_max=*/0, 1);
+          else if (t->subtype == task_subtype_sink_density)
+            runner_dosub_self_sinks_density(r, ci, 1);
           else if (t->subtype == task_subtype_sink_swallow)
             runner_dosub_self_sinks_swallow(r, ci, 1);
           else if (t->subtype == task_subtype_sink_do_gas_swallow)
@@ -339,25 +390,29 @@ void *runner_main(void *data) {
 
         case task_type_sub_pair:
           if (t->subtype == task_subtype_density)
-            runner_dosub_pair1_density(r, ci, cj, 1);
+            runner_dosub_pair1_density(r, ci, cj, /*below_h_max=*/0, 1);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
-            runner_dosub_pair1_gradient(r, ci, cj, 1);
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+            runner_dosub_pair2_gradient(r, ci, cj, /*below_h_max=*/0, 1);
+#else
+            runner_dosub_pair1_gradient(r, ci, cj, /*below_h_max=*/0, 1);
+#endif
 #endif
           else if (t->subtype == task_subtype_force)
-            runner_dosub_pair2_force(r, ci, cj, 1);
+            runner_dosub_pair2_force(r, ci, cj, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_limiter)
-            runner_dosub_pair1_limiter(r, ci, cj, 1);
+            runner_dosub_pair1_limiter(r, ci, cj, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_stars_density)
-            runner_dosub_pair_stars_density(r, ci, cj, 1);
+            runner_dosub_pair_stars_density(r, ci, cj, /*below_h_max=*/0, 1);
 #ifdef EXTRA_STAR_LOOPS
           else if (t->subtype == task_subtype_stars_prep1)
-            runner_dosub_pair_stars_prep1(r, ci, cj, 1);
+            runner_dosub_pair_stars_prep1(r, ci, cj, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_stars_prep2)
-            runner_dosub_pair_stars_prep2(r, ci, cj, 1);
+            runner_dosub_pair_stars_prep2(r, ci, cj, /*below_h_max=*/0, 1);
 #endif
           else if (t->subtype == task_subtype_stars_feedback)
-            runner_dosub_pair_stars_feedback(r, ci, cj, 1);
+            runner_dosub_pair_stars_feedback(r, ci, cj, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_bh_density)
             runner_dosub_pair_bh_density(r, ci, cj, 1);
           else if (t->subtype == task_subtype_bh_swallow)
@@ -369,9 +424,11 @@ void *runner_main(void *data) {
           else if (t->subtype == task_subtype_bh_feedback)
             runner_dosub_pair_bh_feedback(r, ci, cj, 1);
           else if (t->subtype == task_subtype_rt_gradient)
-            runner_dosub_pair1_rt_gradient(r, ci, cj, 1);
+            runner_dosub_pair1_rt_gradient(r, ci, cj, /*below_h_max=*/0, 1);
           else if (t->subtype == task_subtype_rt_transport)
-            runner_dosub_pair2_rt_transport(r, ci, cj, 1);
+            runner_dosub_pair2_rt_transport(r, ci, cj, /*below_h_max=*/0, 1);
+          else if (t->subtype == task_subtype_sink_density)
+            runner_dosub_pair_sinks_density(r, ci, cj, 1);
           else if (t->subtype == task_subtype_sink_swallow)
             runner_dosub_pair_sinks_swallow(r, ci, cj, 1);
           else if (t->subtype == task_subtype_sink_do_gas_swallow)
@@ -388,7 +445,8 @@ void *runner_main(void *data) {
           runner_do_hydro_sort(
               r, ci, t->flags,
               ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin,
-              cell_get_flag(ci, cell_flag_rt_requests_sort), 1);
+              /*lock=*/0, cell_get_flag(ci, cell_flag_rt_requests_sort),
+              /*clock=*/1);
           /* Reset the sort flags as our work here is done. */
           t->flags = 0;
           break;
@@ -399,7 +457,8 @@ void *runner_main(void *data) {
            * don't have rt_sort tasks. */
           runner_do_hydro_sort(
               r, ci, t->flags,
-              ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin, 1, 1);
+              ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin,
+              /*lock=*/0, /*rt_requests_sorts=*/1, /*clock=*/1);
           /* Reset the sort flags as our work here is done. */
           t->flags = 0;
           break;
@@ -431,6 +490,9 @@ void *runner_main(void *data) {
         case task_type_bh_swallow_ghost3:
           runner_do_black_holes_swallow_ghost(r, ci, 1);
           break;
+        case task_type_sink_density_ghost:
+          runner_do_sinks_density_ghost(r, ci, 1);
+          break;
         case task_type_drift_part:
           runner_do_drift_part(r, ci, 1);
           break;
@@ -482,6 +544,8 @@ void *runner_main(void *data) {
             free(t->buff);
           } else if (t->subtype == task_subtype_sf_counts) {
             free(t->buff);
+          } else if (t->subtype == task_subtype_grav_counts) {
+            free(t->buff);
           } else if (t->subtype == task_subtype_part_swallow) {
             free(t->buff);
           } else if (t->subtype == task_subtype_bpart_merger) {
@@ -495,9 +559,12 @@ void *runner_main(void *data) {
             cell_unpack_end_step(ci, (struct pcell_step *)t->buff);
             free(t->buff);
           } else if (t->subtype == task_subtype_sf_counts) {
-            cell_unpack_sf_counts(ci, (struct pcell_sf *)t->buff);
+            cell_unpack_sf_counts(ci, (struct pcell_sf_stars *)t->buff);
             cell_clear_stars_sort_flags(ci, /*clear_unused_flags=*/0);
             free(t->buff);
+          } else if (t->subtype == task_subtype_grav_counts) {
+            cell_unpack_grav_counts(ci, (struct pcell_sf_grav *)t->buff);
+            free(t->buff);
           } else if (t->subtype == task_subtype_xv) {
             runner_do_recv_part(r, ci, 1, 1);
           } else if (t->subtype == task_subtype_rho) {
@@ -568,10 +635,16 @@ void *runner_main(void *data) {
           runner_do_sink_formation(r, t->ci);
           break;
         case task_type_fof_self:
-          runner_do_fof_self(r, t->ci, 1);
+          runner_do_fof_search_self(r, t->ci, 1);
           break;
         case task_type_fof_pair:
-          runner_do_fof_pair(r, t->ci, t->cj, 1);
+          runner_do_fof_search_pair(r, t->ci, t->cj, 1);
+          break;
+        case task_type_fof_attach_self:
+          runner_do_fof_attach_self(r, t->ci, 1);
+          break;
+        case task_type_fof_attach_pair:
+          runner_do_fof_attach_pair(r, t->ci, t->cj, 1);
           break;
         case task_type_neutrino_weight:
           runner_do_neutrino_weighting(r, ci, 1);
diff --git a/src/runner_neutrino.c b/src/runner_neutrino.c
index 22807a870ab3b7e3879a9d2f14398de390944294..785616a31144945d83566057608a1e03e3a69905 100644
--- a/src/runner_neutrino.c
+++ b/src/runner_neutrino.c
@@ -89,5 +89,5 @@ void runner_do_neutrino_weighting(struct runner *r, struct cell *c, int timer) {
     }
   }
 
-  if (timer) TIMER_TOC(timer_weight);
+  if (timer) TIMER_TOC(timer_neutrino_weighting);
 }
diff --git a/src/runner_others.c b/src/runner_others.c
index 2112a7a2b4717fe3bd4661a8f7b7089c8b3637a7..bab876ba8760a2482548890e6373eda22dc753fc 100644
--- a/src/runner_others.c
+++ b/src/runner_others.c
@@ -48,11 +48,13 @@
 #include "error.h"
 #include "feedback.h"
 #include "fof.h"
+#include "forcing.h"
 #include "gravity.h"
 #include "hydro.h"
 #include "potential.h"
 #include "pressure_floor.h"
 #include "rt.h"
+#include "runner_doiact_sinks.h"
 #include "space.h"
 #include "star_formation.h"
 #include "star_formation_logger.h"
@@ -61,6 +63,8 @@
 #include "timestep_limiter.h"
 #include "tracers.h"
 
+extern const int sort_stack_size;
+
 /**
  * @brief Calculate gravity acceleration from external potential
  *
@@ -194,6 +198,9 @@ void runner_do_cooling(struct runner *r, struct cell *c, int timer) {
  */
 void runner_do_star_formation_sink(struct runner *r, struct cell *c,
                                    int timer) {
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
 
   struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -248,11 +255,18 @@ void runner_do_star_formation_sink(struct runner *r, struct cell *c,
         error("TODO");
 #endif
 
-        /* Spawn as many sink as necessary */
-        while (sink_spawn_star(s, e, sink_props, cosmo, with_cosmology,
-                               phys_const, us)) {
+        /* Update the sink properties before spwaning stars */
+        sink_update_sink_properties_before_star_formation(s, e, sink_props,
+                                                          phys_const);
+
+        /* Spawn as many stars as necessary
+           - loop counter for the random seed.
+           - Start by 1 as 0 is used at init (sink_copy_properties) */
+        for (int star_counter = 1; sink_spawn_star(
+                 s, e, sink_props, cosmo, with_cosmology, phys_const, us);
+             star_counter++) {
 
-          /* Create a new star */
+          /* Create a new star with a mass s->target_mass */
           struct spart *sp = cell_spawn_new_spart_from_sink(e, c, s);
           if (sp == NULL)
             error("Run out of available star particles or gparts");
@@ -261,11 +275,30 @@ void runner_do_star_formation_sink(struct runner *r, struct cell *c,
           sink_copy_properties_to_star(s, sp, e, sink_props, cosmo,
                                        with_cosmology, phys_const, us);
 
+          /* Verify that we do not have too many stars in the leaf for
+           * the sort task to be able to act. */
+          if (c->stars.count > (1LL << sort_stack_size))
+            error(
+                "Too many stars in the cell tree leaf! The sorting task will "
+                "not be able to perform its duties. Possible solutions: (1) "
+                "The code need to be run with different star formation "
+                "parameters to reduce the number of star particles created. OR "
+                "(2) The size of the sorting stack must be increased in "
+                "runner_sort.c.");
+
           /* Update the h_max */
           c->stars.h_max = max(c->stars.h_max, sp->h);
           c->stars.h_max_active = max(c->stars.h_max_active, sp->h);
-        }
-      }
+
+          /* Update sink properties */
+          sink_update_sink_properties_during_star_formation(
+              s, sp, e, sink_props, phys_const, star_counter);
+        } /* Loop over the stars to spawn */
+
+        /* Update the sink after star formation */
+        sink_update_sink_properties_after_star_formation(s, e, sink_props,
+                                                         phys_const);
+      } /* if sink_is_active */
     } /* Loop over the particles */
   }
 
@@ -390,93 +423,115 @@ void runner_do_star_formation(struct runner *r, struct cell *c, int timer) {
                                                     dt_star)) {
 
             /* Convert the gas particle to a star particle */
-            struct spart *sp = NULL;
-            const int spawn_spart =
-                star_formation_should_spawn_spart(p, xp, sf_props);
+            const int n_spart_spawn =
+                star_formation_number_spart_to_spawn(p, xp, sf_props);
+            const int n_spart_convert =
+                star_formation_number_spart_to_convert(p, xp, sf_props);
 
-            /* Are we using a model that actually generates star particles? */
-            if (swift_star_formation_model_creates_stars) {
-
-              /* Check if we should create a new particle or transform one */
-              if (spawn_spart) {
-                /* Spawn a new spart (+ gpart) */
-                sp = cell_spawn_new_spart_from_part(e, c, p, xp);
-              } else {
-                /* Convert the gas particle to a star particle */
-                sp = cell_convert_part_to_spart(e, c, p, xp);
-#ifdef WITH_CSDS
-                /* Write the particle */
-                /* Logs all the fields request by the user */
-                // TODO select only the requested fields
-                csds_log_part(e->csds, p, xp, e, /* log_all */ 1,
-                              csds_flag_change_type, swift_type_stars);
+#ifdef SWIFT_DEBUG_CHECKS
+            if (n_spart_convert > 1 || n_spart_convert < 0)
+              error("Invalid number of sparts to convert");
 #endif
-              }
-
-            } else {
 
-              /* We are in a model where spart don't exist
-               * --> convert the part to a DM gpart */
-              cell_convert_part_to_gpart(e, c, p, xp);
-            }
+            int n_spart_to_create = n_spart_spawn + n_spart_convert;
 
-            /* Did we get a star? (Or did we run out of spare ones?) */
-            if (sp != NULL) {
+            while (n_spart_to_create > 0) {
 
-              /* message("We formed a star id=%lld cellID=%lld", sp->id,
-               * c->cellID); */
+              struct spart *sp = NULL;
+              int part_converted;
 
-              /* Copy the properties of the gas particle to the star particle */
-              star_formation_copy_properties(
-                  p, xp, sp, e, sf_props, cosmo, with_cosmology, phys_const,
-                  hydro_props, us, cooling, !spawn_spart);
-
-              /* Update the Star formation history */
-              star_formation_logger_log_new_spart(sp, &c->stars.sfh);
-
-              /* Update the h_max */
-              c->stars.h_max = max(c->stars.h_max, sp->h);
-              c->stars.h_max_active = max(c->stars.h_max_active, sp->h);
-
-              /* Update the displacement information */
-              if (star_formation_need_update_dx_max) {
-                const float dx2_part = xp->x_diff[0] * xp->x_diff[0] +
-                                       xp->x_diff[1] * xp->x_diff[1] +
-                                       xp->x_diff[2] * xp->x_diff[2];
-                const float dx2_sort = xp->x_diff_sort[0] * xp->x_diff_sort[0] +
-                                       xp->x_diff_sort[1] * xp->x_diff_sort[1] +
-                                       xp->x_diff_sort[2] * xp->x_diff_sort[2];
-
-                const float dx_part = sqrtf(dx2_part);
-                const float dx_sort = sqrtf(dx2_sort);
-
-                /* Note: no need to update quantities further up the tree as
-                   this task is always called at the top-level */
-                c->hydro.dx_max_part = max(c->hydro.dx_max_part, dx_part);
-                c->hydro.dx_max_sort = max(c->hydro.dx_max_sort, dx_sort);
-              }
+              /* Are we using a model that actually generates star particles? */
+              if (swift_star_formation_model_creates_stars) {
 
+                /* Check if we should create a new particle or transform one */
+                if (n_spart_to_create == 1 && n_spart_convert == 1) {
+                  /* Convert the gas particle to a star particle */
+                  sp = cell_convert_part_to_spart(e, c, p, xp);
+                  part_converted = 1;
 #ifdef WITH_CSDS
-              if (spawn_spart) {
-                /* Set to zero the csds data. */
-                csds_part_data_init(&sp->csds_data);
+                  /* Write the particle */
+                  /* Logs all the fields request by the user */
+                  // TODO select only the requested fields
+                  csds_log_part(e->csds, p, xp, e, /* log_all */ 1,
+                                csds_flag_change_type, swift_type_stars);
+#endif
+                } else {
+                  /* Spawn a new spart (+ gpart) */
+                  sp = cell_spawn_new_spart_from_part(e, c, p, xp);
+                  part_converted = 0;
+                }
+
               } else {
-                /* Copy the properties back to the stellar particle */
-                sp->csds_data = xp->csds_data;
+
+                /* We are in a model where spart don't exist
+                 * --> convert the part to a DM gpart */
+                cell_convert_part_to_gpart(e, c, p, xp);
+                part_converted = 1;
               }
 
-              /* Write the s-particle */
-              csds_log_spart(e->csds, sp, e, /* log_all */ 1, csds_flag_create,
-                             /* data */ 0);
+              /* Did we get a star? (Or did we run out of spare ones?) */
+              if (sp != NULL) {
+
+                /* Copy the properties of the gas particle to the star particle
+                 */
+                star_formation_copy_properties(
+                    p, xp, sp, e, sf_props, cosmo, with_cosmology, phys_const,
+                    hydro_props, us, cooling, part_converted);
+
+                /* Update the Star formation history */
+                star_formation_logger_log_new_spart(sp, &c->stars.sfh);
+
+                /* Update the h_max */
+                c->stars.h_max = max(c->stars.h_max, sp->h);
+                c->stars.h_max_active = max(c->stars.h_max_active, sp->h);
+
+                /* Update the displacement information */
+                if (star_formation_need_update_dx_max) {
+                  const float dx2_part = xp->x_diff[0] * xp->x_diff[0] +
+                                         xp->x_diff[1] * xp->x_diff[1] +
+                                         xp->x_diff[2] * xp->x_diff[2];
+                  const float dx2_sort =
+                      xp->x_diff_sort[0] * xp->x_diff_sort[0] +
+                      xp->x_diff_sort[1] * xp->x_diff_sort[1] +
+                      xp->x_diff_sort[2] * xp->x_diff_sort[2];
+
+                  const float dx_part = sqrtf(dx2_part);
+                  const float dx_sort = sqrtf(dx2_sort);
+
+                  /* Note: no need to update quantities further up the tree as
+                     this task is always called at the top-level */
+                  c->hydro.dx_max_part = max(c->hydro.dx_max_part, dx_part);
+                  c->hydro.dx_max_sort = max(c->hydro.dx_max_sort, dx_sort);
+                }
+
+#ifdef WITH_CSDS
+                if (spawn_spart) {
+                  /* Set to zero the csds data. */
+                  csds_part_data_init(&sp->csds_data);
+                } else {
+                  /* Copy the properties back to the stellar particle */
+                  sp->csds_data = xp->csds_data;
+                }
+
+                /* Write the s-particle */
+                csds_log_spart(e->csds, sp, e, /* log_all */ 1,
+                               csds_flag_create,
+                               /* data */ 0);
 #endif
-            } else if (swift_star_formation_model_creates_stars) {
+              } else if (swift_star_formation_model_creates_stars) {
 
-              /* Do something about the fact no star could be formed.
-                 Note that in such cases a tree rebuild to create more free
-                 slots has already been triggered by the function
-                 cell_convert_part_to_spart() */
-              star_formation_no_spart_available(e, p, xp);
-            }
+                /* Do something about the fact no star could be formed.
+                   Note that in such cases a tree rebuild to create more free
+                   slots has already been triggered by the function
+                   cell_convert_part_to_spart() */
+                star_formation_no_spart_available(e, p, xp);
+              }
+
+              /* We have spawned a particle, decrease the counter of particles
+               * to create */
+              n_spart_to_create--;
+
+            } /* while n_spart_to_create > 0 */
           }
 
         } else { /* Are we not star-forming? */
@@ -515,6 +570,10 @@ void runner_do_star_formation(struct runner *r, struct cell *c, int timer) {
  */
 void runner_do_sink_formation(struct runner *r, struct cell *c) {
 
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
+
   struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
   const struct sink_props *sink_props = e->sink_properties;
@@ -532,7 +591,7 @@ void runner_do_sink_formation(struct runner *r, struct cell *c) {
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (c->nodeID != e->nodeID)
-    error("Running star formation task on a foreign node!");
+    error("Running sink formation task on a foreign node!");
 #endif
 
   /* Anything to do here? */
@@ -549,6 +608,11 @@ void runner_do_sink_formation(struct runner *r, struct cell *c) {
 
         /* Do the recursion */
         runner_do_sink_formation(r, cp);
+
+        /* Update the h_max */
+        c->sinks.h_max = max(c->sinks.h_max, cp->sinks.h_max);
+        c->sinks.h_max_active =
+            max(c->sinks.h_max_active, cp->sinks.h_max_active);
       }
   } else {
 
@@ -562,6 +626,10 @@ void runner_do_sink_formation(struct runner *r, struct cell *c) {
       /* Only work on active particles */
       if (part_is_active(p, e)) {
 
+        /* Loop over all particles to find the neighbours within r_acc. Then, */
+        /* compute all quantities you need to decide to form a sink or not. */
+        runner_do_prepare_part_sink_formation(r, c, p, xp);
+
         /* Is this particle star forming? */
         if (sink_is_forming(p, xp, sink_props, phys_const, cosmo, hydro_props,
                             us, cooling, entropy_floor)) {
@@ -599,6 +667,10 @@ void runner_do_sink_formation(struct runner *r, struct cell *c) {
               sink_copy_properties(p, xp, sink, e, sink_props, cosmo,
                                    with_cosmology, phys_const, hydro_props, us,
                                    cooling);
+
+              /* Update the cell h_max if necessary */
+              c->sinks.h_max = max(c->sinks.h_max, sink->h);
+              c->sinks.h_max_active = max(c->sinks.h_max_active, sink->h);
             }
           }
         }
@@ -634,12 +706,14 @@ void runner_do_end_hydro_force(struct runner *r, struct cell *c, int timer) {
     const struct cosmology *cosmo = e->cosmology;
     const int count = c->hydro.count;
     struct part *restrict parts = c->hydro.parts;
+    struct xpart *restrict xparts = c->hydro.xparts;
 
     /* Loop over the gas particles in this cell. */
     for (int k = 0; k < count; k++) {
 
       /* Get a handle on the part. */
       struct part *restrict p = &parts[k];
+      struct xpart *restrict xp = &xparts[k];
 
       double dt = 0;
       if (part_is_active(p, e)) {
@@ -661,6 +735,10 @@ void runner_do_end_hydro_force(struct runner *r, struct cell *c, int timer) {
         timestep_limiter_end_force(p);
         chemistry_end_force(p, cosmo, with_cosmology, e->time, dt);
 
+        /* Apply the forcing terms (if any) */
+        forcing_terms_apply(e->time, e->forcing_terms, e->s,
+                            e->physical_constants, p, xp);
+
 #ifdef SWIFT_BOUNDARY_PARTICLES
 
         /* Get the ID of the part */
@@ -677,7 +755,8 @@ void runner_do_end_hydro_force(struct runner *r, struct cell *c, int timer) {
 
           /* Some values need to be reset in the Gizmo case. */
           hydro_prepare_force(p, &c->hydro.xparts[k], cosmo,
-                              e->hydro_properties, 0, 0);
+                              e->hydro_properties, e->pressure_floor_props,
+                              /*dt_alpha=*/0, /*dt_therm=*/0);
           rt_prepare_force(p);
 #endif
         }
@@ -702,6 +781,7 @@ void runner_do_end_grav_force(struct runner *r, struct cell *c, int timer) {
   const struct engine *e = r->e;
   const int with_self_gravity = (e->policy & engine_policy_self_gravity);
   const int with_black_holes = (e->policy & engine_policy_black_holes);
+  const int with_sinks = (e->policy & engine_policy_sinks);
 
   TIMER_TIC;
 
@@ -822,6 +902,12 @@ void runner_do_end_grav_force(struct runner *r, struct cell *c, int timer) {
           black_holes_store_potential_in_part(
               &s->parts[offset].black_holes_data, gp);
         }
+
+        /* Deal with sinks' need of potentials */
+        if (with_sinks && gp->type == swift_type_gas) {
+          const size_t offset = -gp->id_or_neg_offset;
+          sink_store_potential_in_part(&s->parts[offset].sink_data, gp);
+        }
       }
     }
   }
@@ -949,7 +1035,7 @@ void runner_do_csds(struct runner *r, struct cell *c, int timer) {
  * @param c cell
  * @param timer 1 if the time is to be recorded.
  */
-void runner_do_fof_self(struct runner *r, struct cell *c, int timer) {
+void runner_do_fof_search_self(struct runner *r, struct cell *c, int timer) {
 
 #ifdef WITH_FOF
 
@@ -979,8 +1065,8 @@ void runner_do_fof_self(struct runner *r, struct cell *c, int timer) {
  * @param cj cell j
  * @param timer 1 if the time is to be recorded.
  */
-void runner_do_fof_pair(struct runner *r, struct cell *ci, struct cell *cj,
-                        int timer) {
+void runner_do_fof_search_pair(struct runner *r, struct cell *ci,
+                               struct cell *cj, int timer) {
 
 #ifdef WITH_FOF
 
@@ -1006,6 +1092,68 @@ void runner_do_fof_pair(struct runner *r, struct cell *ci, struct cell *cj,
 #endif
 }
 
+/**
+ * @brief Recursively search for FOF groups in a single cell.
+ *
+ * @param r runner task
+ * @param c cell
+ * @param timer 1 if the time is to be recorded.
+ */
+void runner_do_fof_attach_self(struct runner *r, struct cell *c, int timer) {
+
+#ifdef WITH_FOF
+
+  TIMER_TIC;
+
+  const struct engine *e = r->e;
+  struct space *s = e->s;
+  const double dim[3] = {s->dim[0], s->dim[1], s->dim[2]};
+  const int periodic = s->periodic;
+  const struct gpart *const gparts = s->gparts;
+  const double attach_r2 = e->fof_properties->l_x2;
+
+  rec_fof_attach_self(e->fof_properties, dim, attach_r2, periodic, gparts,
+                      s->nr_gparts, c);
+
+  if (timer) TIMER_TOC(timer_fof_self);
+
+#else
+  error("SWIFT was not compiled with FOF enabled!");
+#endif
+}
+
+/**
+ * @brief Recursively search for FOF groups between a pair of cells.
+ *
+ * @param r runner task
+ * @param ci cell i
+ * @param cj cell j
+ * @param timer 1 if the time is to be recorded.
+ */
+void runner_do_fof_attach_pair(struct runner *r, struct cell *ci,
+                               struct cell *cj, int timer) {
+
+#ifdef WITH_FOF
+
+  TIMER_TIC;
+
+  const struct engine *e = r->e;
+  struct space *s = e->s;
+  const double dim[3] = {s->dim[0], s->dim[1], s->dim[2]};
+  const int periodic = s->periodic;
+  const struct gpart *const gparts = s->gparts;
+  const double attach_r2 = e->fof_properties->l_x2;
+
+  rec_fof_attach_pair(e->fof_properties, dim, attach_r2, periodic, gparts,
+                      s->nr_gparts, ci, cj, e->nodeID == ci->nodeID,
+                      e->nodeID == cj->nodeID);
+
+  if (timer) TIMER_TOC(timer_fof_pair);
+#else
+  error("SWIFT was not compiled with FOF enabled!");
+#endif
+}
+
 /**
  * @brief Finish up the transport step and do the thermochemistry
  *        for radiative transfer
@@ -1074,7 +1222,7 @@ void runner_do_rt_tchem(struct runner *r, struct cell *c, int timer) {
         error("Got part with negative time-step: %lld, %.6g", p->id, dt);
 #endif
 
-      rt_finalise_transport(p, dt, cosmo);
+      rt_finalise_transport(p, rt_props, dt, cosmo);
 
       /* And finally do thermochemistry */
       rt_tchem(p, xp, rt_props, cosmo, hydro_props, phys_const, us, dt);
diff --git a/src/runner_recv.c b/src/runner_recv.c
index c36f6d2349c29a6aa042f488ec9d49b524168153..5b019f2d0d04d9601e56f174d503f00436c75013 100644
--- a/src/runner_recv.c
+++ b/src/runner_recv.c
@@ -31,7 +31,9 @@
 #include "runner.h"
 
 /* Local headers. */
+#include "active.h"
 #include "engine.h"
+#include "feedback.h"
 #include "timers.h"
 
 /**
@@ -225,10 +227,11 @@ void runner_do_recv_spart(struct runner *r, struct cell *c, int clear_sorts,
 
 #ifdef WITH_MPI
 
+  const struct engine *e = r->e;
+  const int with_rt = (e->policy & engine_policy_rt);
+  const integertime_t ti_current = e->ti_current;
   struct spart *restrict sparts = c->stars.parts;
   const size_t nr_sparts = c->stars.count;
-  const integertime_t ti_current = r->e->ti_current;
-  const timebin_t max_active_bin = r->e->max_active_bin;
 
   TIMER_TIC;
 
@@ -258,7 +261,8 @@ void runner_do_recv_spart(struct runner *r, struct cell *c, int clear_sorts,
       time_bin_max = max(time_bin_max, sparts[k].time_bin);
       h_max = max(h_max, sparts[k].h);
       sparts[k].gpart = NULL;
-      if (sparts[k].time_bin <= max_active_bin)
+      if (spart_is_active(&sparts[k], e) &&
+          (feedback_is_active(&sparts[k], e) || with_rt))
         h_max_active = max(h_max_active, sparts[k].h);
     }
 
diff --git a/src/runner_sinks.c b/src/runner_sinks.c
index 208a7e99471056dc660e4212078f551c56643ea0..261ed840e9cb8297f7143d1ddc4beffa179f1933 100644
--- a/src/runner_sinks.c
+++ b/src/runner_sinks.c
@@ -28,478 +28,10 @@
 #include "cell.h"
 #include "engine.h"
 #include "sink.h"
+#include "sink_iact.h"
 #include "space_getsid.h"
 #include "timers.h"
 
-/**
- * @brief Calculate gas and sink interaction around #sinks
- *
- * @param r runner task
- * @param c cell
- * @param timer 1 if the time is to be recorded.
- */
-void runner_doself_sinks_swallow(struct runner *r, struct cell *c, int timer) {
-
-#ifdef SWIFT_DEBUG_CHECKS
-  if (c->nodeID != engine_rank) error("Should be run on a different node");
-#endif
-
-  TIMER_TIC;
-
-  const struct engine *e = r->e;
-
-  /* Anything to do here? */
-  if (c->sinks.count == 0) return;
-  if (!cell_is_active_sinks(c, e)) return;
-
-  const int scount = c->sinks.count;
-  const int count = c->hydro.count;
-  struct sink *restrict sinks = c->sinks.parts;
-  struct part *restrict parts = c->hydro.parts;
-
-  /* Do we actually have any gas neighbours? */
-  if (c->hydro.count != 0) {
-
-    /* Loop over the sinks in ci. */
-    for (int sid = 0; sid < scount; sid++) {
-
-      /* Get a hold of the ith sinks in ci. */
-      struct sink *restrict si = &sinks[sid];
-
-      /* Skip inactive particles */
-      if (!sink_is_active(si, e)) continue;
-
-      const float ri = si->r_cut;
-      const float ri2 = ri * ri;
-      const float six[3] = {(float)(si->x[0] - c->loc[0]),
-                            (float)(si->x[1] - c->loc[1]),
-                            (float)(si->x[2] - c->loc[2])};
-
-      /* Loop over the parts (gas) in cj. */
-      for (int pjd = 0; pjd < count; pjd++) {
-
-        /* Get a pointer to the jth particle. */
-        struct part *restrict pj = &parts[pjd];
-        const float hj = pj->h;
-
-        /* Early abort? */
-        if (part_is_inhibited(pj, e)) continue;
-
-        /* Compute the pairwise distance. */
-        const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
-                              (float)(pj->x[1] - c->loc[1]),
-                              (float)(pj->x[2] - c->loc[2])};
-        const float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
-        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
-
-#ifdef SWIFT_DEBUG_CHECKS
-        /* Check that particles have been drifted to the current time */
-        if (si->ti_drift != e->ti_current)
-          error("Particle si not drifted to current time");
-        if (pj->ti_drift != e->ti_current)
-          error("Particle pj not drifted to current time");
-#endif
-
-        if (r2 < ri2) {
-          runner_iact_nonsym_sinks_gas_swallow(r2, dx, ri, hj, si, pj);
-        }
-      } /* loop over the parts in ci. */
-    }   /* loop over the bparts in ci. */
-  }     /* Do we have gas particles in the cell? */
-
-  /* When doing sink swallowing, we need a quick loop also over the sink
-   * neighbours */
-
-  /* Loop over the sinks in ci. */
-  for (int sid = 0; sid < scount; sid++) {
-
-    /* Get a hold of the ith sink in ci. */
-    struct sink *restrict si = &sinks[sid];
-
-    /* Skip inactive particles */
-    if (!sink_is_active(si, e)) continue;
-
-    const float ri = si->r_cut;
-    const float ri2 = ri * ri;
-    const float six[3] = {(float)(si->x[0] - c->loc[0]),
-                          (float)(si->x[1] - c->loc[1]),
-                          (float)(si->x[2] - c->loc[2])};
-
-    /* Loop over the sinks in cj. */
-    for (int sjd = 0; sjd < scount; sjd++) {
-
-      /* Skip self interaction */
-      if (sid == sjd) continue;
-
-      /* Get a pointer to the jth particle. */
-      struct sink *restrict sj = &sinks[sjd];
-      const float rj = sj->r_cut;
-      const float rj2 = rj * rj;
-
-      /* Early abort? */
-      if (sink_is_inhibited(sj, e)) continue;
-
-      /* Compute the pairwise distance. */
-      const float sjx[3] = {(float)(sj->x[0] - c->loc[0]),
-                            (float)(sj->x[1] - c->loc[1]),
-                            (float)(sj->x[2] - c->loc[2])};
-      const float dx[3] = {six[0] - sjx[0], six[1] - sjx[1], six[2] - sjx[2]};
-      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
-
-      if (r2 < ri2 || r2 < rj2) {
-        runner_iact_nonsym_sinks_sink_swallow(r2, dx, ri, rj, si, sj);
-      }
-    } /* loop over the sinks in ci. */
-  }   /* loop over the sinks in ci. */
-
-  if (timer) TIMER_TOC(timer_doself_sink_swallow);
-}
-
-/**
- * @brief Calculate gas and sink interaction around #sinks
- *
- * @param r runner task
- * @param ci The first #cell
- * @param cj The second #cell
- */
-void runner_do_nonsym_pair_sinks_naive_swallow(struct runner *r,
-                                               struct cell *restrict ci,
-                                               struct cell *restrict cj) {
-
-  const struct engine *e = r->e;
-
-  /* Anything to do here? */
-  if (ci->sinks.count == 0) return;
-  if (!cell_is_active_sinks(ci, e)) return;
-
-  const int scount_i = ci->sinks.count;
-  const int count_j = cj->hydro.count;
-  struct sink *restrict sinks_i = ci->sinks.parts;
-  struct part *restrict parts_j = cj->hydro.parts;
-
-  /* Get the relative distance between the pairs, wrapping. */
-  double shift[3] = {0.0, 0.0, 0.0};
-  for (int k = 0; k < 3; k++) {
-    if (cj->loc[k] - ci->loc[k] < -e->s->dim[k] / 2)
-      shift[k] = e->s->dim[k];
-    else if (cj->loc[k] - ci->loc[k] > e->s->dim[k] / 2)
-      shift[k] = -e->s->dim[k];
-  }
-
-  /* Do we actually have any gas neighbours? */
-  if (cj->hydro.count != 0) {
-
-    /* Loop over the sinks in ci. */
-    for (int sid = 0; sid < scount_i; sid++) {
-
-      /* Get a hold of the ith bpart in ci. */
-      struct sink *restrict si = &sinks_i[sid];
-
-      /* Skip inactive particles */
-      if (!sink_is_active(si, e)) continue;
-
-      const float ri = si->r_cut;
-      const float ri2 = ri * ri;
-      const float six[3] = {(float)(si->x[0] - cj->loc[0]),
-                            (float)(si->x[1] - cj->loc[1]),
-                            (float)(si->x[2] - cj->loc[2])};
-
-      /* Loop over the parts (gas) in cj. */
-      for (int pjd = 0; pjd < count_j; pjd++) {
-
-        /* Get a pointer to the jth particle. */
-        struct part *restrict pj = &parts_j[pjd];
-        const float hj = pj->h;
-
-        /* Skip inhibited particles. */
-        if (part_is_inhibited(pj, e)) continue;
-
-        /* Compute the pairwise distance. */
-        const float pjx[3] = {(float)(pj->x[0] - cj->loc[0]),
-                              (float)(pj->x[1] - cj->loc[1]),
-                              (float)(pj->x[2] - cj->loc[2])};
-        const float dx[3] = {six[0] - pjx[0], six[1] - pjx[1], six[2] - pjx[2]};
-        const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
-
-#ifdef SWIFT_DEBUG_CHECKS
-        /* Check that particles have been drifted to the current time */
-        if (si->ti_drift != e->ti_current)
-          error("Particle si not drifted to current time");
-        if (pj->ti_drift != e->ti_current)
-          error("Particle pj not drifted to current time");
-#endif
-
-        if (r2 < ri2) {
-          runner_iact_nonsym_sinks_gas_swallow(r2, dx, ri, hj, si, pj);
-        }
-      } /* loop over the parts in cj. */
-    }   /* loop over the sinks in ci. */
-  }     /* Do we have gas particles in the cell? */
-
-  /* When doing sink swallowing, we need a quick loop also over the sinks
-   * neighbours */
-
-  const int scount_j = cj->sinks.count;
-  struct sink *restrict sinks_j = cj->sinks.parts;
-
-  /* Loop over the sinks in ci. */
-  for (int sid = 0; sid < scount_i; sid++) {
-
-    /* Get a hold of the ith bpart in ci. */
-    struct sink *restrict si = &sinks_i[sid];
-
-    /* Skip inactive particles */
-    if (!sink_is_active(si, e)) continue;
-
-    const float ri = si->r_cut;
-    const float ri2 = ri * ri;
-    const float six[3] = {(float)(si->x[0] - (cj->loc[0] + shift[0])),
-                          (float)(si->x[1] - (cj->loc[1] + shift[1])),
-                          (float)(si->x[2] - (cj->loc[2] + shift[2]))};
-
-    /* Loop over the sinks in cj. */
-    for (int sjd = 0; sjd < scount_j; sjd++) {
-
-      /* Get a pointer to the jth particle. */
-      struct sink *restrict sj = &sinks_j[sjd];
-      const float rj = sj->r_cut;
-      const float rj2 = rj * rj;
-
-      /* Skip inhibited particles. */
-      if (sink_is_inhibited(sj, e)) continue;
-
-      /* Compute the pairwise distance. */
-      const float sjx[3] = {(float)(sj->x[0] - cj->loc[0]),
-                            (float)(sj->x[1] - cj->loc[1]),
-                            (float)(sj->x[2] - cj->loc[2])};
-      const float dx[3] = {six[0] - sjx[0], six[1] - sjx[1], six[2] - sjx[2]};
-      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
-
-#ifdef SWIFT_DEBUG_CHECKS
-      /* Check that particles have been drifted to the current time */
-      if (si->ti_drift != e->ti_current)
-        error("Particle si not drifted to current time");
-      if (sj->ti_drift != e->ti_current)
-        error("Particle sj not drifted to current time");
-#endif
-
-      if (r2 < ri2 || r2 < rj2) {
-        runner_iact_nonsym_sinks_sink_swallow(r2, dx, ri, rj, si, sj);
-      }
-    } /* loop over the sinks in cj. */
-  }   /* loop over the sinks in ci. */
-}
-
-/**
- * @brief Calculate swallow for ci #sinks part around the cj #gas and sinks and
- *                              cj #sinks part around the ci #gas and sinks
- *
- * @param r runner task
- * @param ci The first #cell
- * @param cj The second #cell
- */
-void runner_dopair_sinks_naive_swallow(struct runner *r,
-                                       struct cell *restrict ci,
-                                       struct cell *restrict cj, int timer) {
-
-  TIMER_TIC;
-
-  runner_do_nonsym_pair_sinks_naive_swallow(r, ci, cj);
-  runner_do_nonsym_pair_sinks_naive_swallow(r, cj, ci);
-
-  if (timer) TIMER_TOC(timer_dopair_sink_swallow);
-}
-
-/**
- * @brief Wrapper to runner_doself_sinks_swallow
- *
- * @param r #runner
- * @param c #cell c
- *
- */
-void runner_doself_branch_sinks_swallow(struct runner *r, struct cell *c) {
-
-  const struct engine *restrict e = r->e;
-
-  /* Anything to do here? */
-  if (c->sinks.count == 0) return;
-
-  /* Anything to do here? */
-  if (!cell_is_active_sinks(c, e)) return;
-
-  /* Did we mess up the recursion? */
-  if (c->sinks.r_cut_max_old > c->dmin)
-    error("Cell smaller than the cut off radius");
-
-  runner_doself_sinks_swallow(r, c, 1);
-}
-
-/**
- * @brief Wrapper for runner_dopair_sinks_naive_swallow.
- *
- * @param r #runner
- * @param ci #cell ci
- * @param cj #cell cj
- *
- */
-void runner_dopair_branch_sinks_swallow(struct runner *r, struct cell *ci,
-                                        struct cell *cj) {
-
-  const struct engine *restrict e = r->e;
-
-  const int ci_active = cell_is_active_sinks(ci, e);
-  const int cj_active = cell_is_active_sinks(cj, e);
-
-  const int do_ci = (ci->sinks.count != 0 && cj->hydro.count != 0 && ci_active);
-  const int do_cj = (cj->sinks.count != 0 && ci->hydro.count != 0 && cj_active);
-
-  /* Anything to do here? */
-  if (!do_ci && !do_cj) return;
-
-  /* Check that cells are drifted. */
-  if (do_ci && (!cell_are_sink_drifted(ci, e) || !cell_are_part_drifted(cj, e)))
-    error("Interacting undrifted cells.");
-
-  if (do_cj && (!cell_are_part_drifted(ci, e) || !cell_are_sink_drifted(cj, e)))
-    error("Interacting undrifted cells.");
-
-  /* No sorted interactions here -> use the naive ones */
-  runner_dopair_sinks_naive_swallow(r, ci, cj, 1);
-}
-
-/**
- * @brief Compute grouped sub-cell interactions for pairs
- *
- * @param r The #runner.
- * @param ci The first #cell.
- * @param cj The second #cell.
- * @param gettimer Do we have a timer ?
- *
- * @todo Hard-code the sid on the recursive calls to avoid the
- * redundant computations to find the sid on-the-fly.
- */
-void runner_dosub_pair_sinks_swallow(struct runner *r, struct cell *ci,
-                                     struct cell *cj, int timer) {
-
-  TIMER_TIC;
-
-  struct space *s = r->e->s;
-  const struct engine *e = r->e;
-
-  /* Should we even bother?
-   * In the swallow case we care about sink-sink and sink-gas
-   * interactions. */
-
-  const int should_do_ci = ci->sinks.count != 0 && cell_is_active_sinks(ci, e);
-  const int should_do_cj = cj->sinks.count != 0 && cell_is_active_sinks(cj, e);
-
-  if (!should_do_ci && !should_do_cj) return;
-
-  /* Get the type of pair and flip ci/cj if needed. */
-  double shift[3];
-  const int sid = space_getsid(s, &ci, &cj, shift);
-
-  /* Recurse? */
-  if (cell_can_recurse_in_pair_sinks_task(ci, cj) &&
-      cell_can_recurse_in_pair_sinks_task(cj, ci)) {
-    struct cell_split_pair *csp = &cell_split_pairs[sid];
-    for (int k = 0; k < csp->count; k++) {
-      const int pid = csp->pairs[k].pid;
-      const int pjd = csp->pairs[k].pjd;
-      if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL)
-        runner_dosub_pair_sinks_swallow(r, ci->progeny[pid], cj->progeny[pjd],
-                                        0);
-    }
-  }
-
-  /* Otherwise, compute the pair directly. */
-  else {
-
-    const int do_ci = ci->sinks.count != 0 && cell_is_active_sinks(ci, e);
-    const int do_cj = cj->sinks.count != 0 && cell_is_active_sinks(cj, e);
-
-    if (do_ci) {
-
-      /* Make sure both cells are drifted to the current timestep. */
-      if (!cell_are_sink_drifted(ci, e))
-        error("Interacting undrifted cells (sinks).");
-
-      if (cj->hydro.count != 0 && !cell_are_part_drifted(cj, e))
-        error("Interacting undrifted cells (parts).");
-    }
-
-    if (do_cj) {
-
-      /* Make sure both cells are drifted to the current timestep. */
-      if (ci->hydro.count != 0 && !cell_are_part_drifted(ci, e))
-        error("Interacting undrifted cells (parts).");
-
-      if (!cell_are_sink_drifted(cj, e))
-        error("Interacting undrifted cells (sinks).");
-    }
-
-    if (do_ci || do_cj) runner_dopair_branch_sinks_swallow(r, ci, cj);
-  }
-
-  if (timer) TIMER_TOC(timer_dosub_pair_sink_swallow);
-}
-
-/**
- * @brief Compute grouped sub-cell interactions for self tasks
- *
- * @param r The #runner.
- * @param ci The first #cell.
- * @param gettimer Do we have a timer ?
- */
-void runner_dosub_self_sinks_swallow(struct runner *r, struct cell *ci,
-                                     int timer) {
-
-  TIMER_TIC;
-
-  const struct engine *e = r->e;
-
-#ifdef SWIFT_DEBUG_CHECKS
-  if (ci->nodeID != engine_rank)
-    error("This function should not be called on foreign cells");
-#endif
-
-  /* Should we even bother?
-   * In the swallow case we care about sink-sink and sink-gas
-   * interactions. */
-
-  const int should_do_ci = ci->sinks.count != 0 && cell_is_active_sinks(ci, e);
-
-  if (!should_do_ci) return;
-
-  /* Recurse? */
-  if (cell_can_recurse_in_self_sinks_task(ci)) {
-
-    /* Loop over all progeny. */
-    for (int k = 0; k < 8; k++)
-      if (ci->progeny[k] != NULL) {
-        runner_dosub_self_sinks_swallow(r, ci->progeny[k], 0);
-        for (int j = k + 1; j < 8; j++)
-          if (ci->progeny[j] != NULL)
-            runner_dosub_pair_sinks_swallow(r, ci->progeny[k], ci->progeny[j],
-                                            0);
-      }
-  }
-
-  /* Otherwise, compute self-interaction. */
-  else {
-
-    /* Check we did drift to the current time */
-    if (!cell_are_sink_drifted(ci, e)) error("Interacting undrifted cell.");
-
-    if (ci->hydro.count != 0 && !cell_are_part_drifted(ci, e))
-      error("Interacting undrifted cells (parts).");
-
-    runner_doself_branch_sinks_swallow(r, ci);
-  }
-
-  if (timer) TIMER_TOC(timer_dosub_self_sink_swallow);
-}
-
 /**
  * @brief Process all the gas particles in a cell that have been flagged for
  * swallowing by a sink.
@@ -531,6 +63,9 @@ void runner_do_sinks_gas_swallow(struct runner *r, struct cell *c, int timer) {
   struct part *parts = c->hydro.parts;
   struct xpart *xparts = c->hydro.xparts;
 
+  integertime_t ti_current = e->ti_current;
+  integertime_t ti_beg_max = 0;
+
   /* Early abort?
    * (We only want cells for which we drifted the gas as these are
    * the only ones that could have gas particles that have been flagged
@@ -546,6 +81,10 @@ void runner_do_sinks_gas_swallow(struct runner *r, struct cell *c, int timer) {
         struct cell *restrict cp = c->progeny[k];
 
         runner_do_sinks_gas_swallow(r, cp, 0);
+
+        /* Propagate the ti_beg_max from the leaves to the roots.
+         * See bug fix below. */
+        ti_beg_max = max(cp->hydro.ti_beg_max, ti_beg_max);
       }
     }
   } else {
@@ -601,8 +140,6 @@ void runner_do_sinks_gas_swallow(struct runner *r, struct cell *c, int timer) {
             /* If the gas particle is local, remove it */
             if (c->nodeID == e->nodeID) {
 
-              message("sink %lld removing gas particle %lld", sp->id, p->id);
-
               lock_lock(&e->s->lock);
 
               /* Re-check that the particle has not been removed
@@ -639,8 +176,29 @@ void runner_do_sinks_gas_swallow(struct runner *r, struct cell *c, int timer) {
                 p->id, swallow_id);
         }
       } /* Part was flagged for swallowing */
-    }   /* Loop over the parts */
-  }     /* Cell is not split */
+
+      /* Bug fix : Change the hydro.ti_beg_max when a sink eats the last gas
+       * particle possessing the ti_beg_max of the cell. We set hydro.ti_beg_max
+       * to the max ti_beg of the remaining gas particle. Why this fix ?
+       * Otherwise, we fail the check from cell_check_timesteps. This bug is
+       * rare because it needs that the swallowed gas is the last part with the
+       * ti_beg_max of the cell.
+       * The same is not done for ti_end_min since it may inactivate cells that
+       * need to perform sinks tasks.
+       */
+
+      if (part_is_inhibited(p, e)) continue;
+
+      integertime_t ti_beg =
+          get_integer_time_begin(ti_current + 1, p->time_bin);
+      ti_beg_max = max(ti_beg, ti_beg_max);
+    } /* Loop over the parts */
+  } /* Cell is not split */
+
+  /* Update ti_beg_max. See bug fix above. */
+  if (ti_beg_max != c->hydro.ti_beg_max) {
+    c->hydro.ti_beg_max = ti_beg_max;
+  }
 }
 
 /**
@@ -652,10 +210,13 @@ void runner_do_sinks_gas_swallow(struct runner *r, struct cell *c, int timer) {
  */
 void runner_do_sinks_gas_swallow_self(struct runner *r, struct cell *c,
                                       int timer) {
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (c->nodeID != r->e->nodeID) error("Running self task on foreign node");
-  if (!cell_is_active_sinks(c, r->e))
+  if (!cell_is_active_sinks(c, r->e) && !cell_is_active_hydro(c, r->e))
     error("Running self task on inactive cell");
 #endif
 
@@ -672,6 +233,9 @@ void runner_do_sinks_gas_swallow_self(struct runner *r, struct cell *c,
  */
 void runner_do_sinks_gas_swallow_pair(struct runner *r, struct cell *ci,
                                       struct cell *cj, int timer) {
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
 
   const struct engine *e = r->e;
 
@@ -801,9 +365,6 @@ void runner_do_sinks_sink_swallow(struct runner *r, struct cell *c, int timer) {
             /* If the sink particle is local, remove it */
             if (c->nodeID == e->nodeID) {
 
-              message("sink %lld removing sink particle %lld", sp->id,
-                      cell_sp->id);
-
               /* Finally, remove the sink particle from the system
                * Recall that the gpart associated with it is also removed
                * at the same time. */
@@ -831,8 +392,8 @@ void runner_do_sinks_sink_swallow(struct runner *r, struct cell *c, int timer) {
         }
 
       } /* Part was flagged for swallowing */
-    }   /* Loop over the parts */
-  }     /* Cell is not split */
+    } /* Loop over the parts */
+  } /* Cell is not split */
 }
 
 /**
@@ -844,10 +405,13 @@ void runner_do_sinks_sink_swallow(struct runner *r, struct cell *c, int timer) {
  */
 void runner_do_sinks_sink_swallow_self(struct runner *r, struct cell *c,
                                        int timer) {
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
 
 #ifdef SWIFT_DEBUG_CHECKS
   if (c->nodeID != r->e->nodeID) error("Running self task on foreign node");
-  if (!cell_is_active_sinks(c, r->e))
+  if (!cell_is_active_sinks(c, r->e) && !cell_is_active_hydro(c, r->e))
     error("Running self task on inactive cell");
 #endif
 
@@ -864,6 +428,9 @@ void runner_do_sinks_sink_swallow_self(struct runner *r, struct cell *c,
  */
 void runner_do_sinks_sink_swallow_pair(struct runner *r, struct cell *ci,
                                        struct cell *cj, int timer) {
+#ifdef SWIFT_DEBUG_CHECKS_MPI_DOMAIN_DECOMPOSITION
+  return;
+#endif
 
   const struct engine *e = r->e;
 
@@ -877,3 +444,55 @@ void runner_do_sinks_sink_swallow_pair(struct runner *r, struct cell *ci,
   if (cell_is_active_sinks(cj, e)) runner_do_sinks_sink_swallow(r, ci, timer);
   if (cell_is_active_sinks(ci, e)) runner_do_sinks_sink_swallow(r, cj, timer);
 }
+
+/**
+ * @brief Compute the energies (kinetic, potential, etc ) of the gas particle
+ * p and all quantities required for the formation of a sink.
+ *
+ * Note: This function iterates over gas particles and sink particles.
+ *
+ * @param e The #engine.
+ * @param c The #cell.
+ * @param p The #part.
+ * @param xp The #xpart data of the particle p.
+ */
+void runner_do_prepare_part_sink_formation(struct runner *r, struct cell *c,
+                                           struct part *restrict p,
+                                           struct xpart *restrict xp) {
+  struct engine *e = r->e;
+  const struct cosmology *cosmo = e->cosmology;
+  const int with_cosmology = e->policy & engine_policy_cosmology;
+  const struct sink_props *sink_props = e->sink_properties;
+  const int count = c->hydro.count;
+  struct part *restrict parts = c->hydro.parts;
+  struct xpart *restrict xparts = c->hydro.xparts;
+
+  /* Loop over all particles to find the neighbours within r_acc. Then,
+     compute all quantities you need.  */
+  for (int i = 0; i < count; i++) {
+
+    /*Get a handle on the part */
+    struct part *restrict pi = &parts[i];
+    struct xpart *restrict xpi = &xparts[i];
+
+    /* Compute the quantities required to later decide to form a sink or not. */
+    sink_prepare_part_sink_formation_gas_criteria(e, p, xp, pi, xpi, cosmo,
+                                                  sink_props);
+  } /* End of gas neighbour loop */
+
+  /* Check that we are not forming a sink in the accretion radius of another
+     one. The new sink may be swallowed by the older one.) */
+  const int scount = c->sinks.count;
+  struct sink *restrict sinks = c->sinks.parts;
+
+  for (int i = 0; i < scount; i++) {
+
+    /* Get a hold of the ith sinks in ci. */
+    struct sink *restrict si = &sinks[i];
+
+    /* Compute the quantities required to later decide to form a sink or not. */
+    sink_prepare_part_sink_formation_sink_criteria(e, p, xp, si, with_cosmology,
+                                                   cosmo, sink_props, e->time);
+
+  } /* End of sink neighbour loop */
+}
diff --git a/src/runner_sort.c b/src/runner_sort.c
index 550626517f861769bd38fa64db04a2b0270b81fe..500c1878c52943d49ce43a6710c4484059ee974e 100644
--- a/src/runner_sort.c
+++ b/src/runner_sort.c
@@ -31,6 +31,9 @@
 #include "engine.h"
 #include "timers.h"
 
+/*! The size of the sorting stack used at the leaf level */
+const int sort_stack_size = 10;
+
 /**
  * @brief Sorts again all the stars in a given cell hierarchy.
  *
@@ -65,16 +68,15 @@ void runner_do_stars_resort(struct runner *r, struct cell *c, const int timer) {
  * @param N The number of entries.
  */
 void runner_do_sort_ascending(struct sort_entry *sort, int N) {
-  const int stack_size = 10;
 
   struct {
     short int lo, hi;
-  } qstack[stack_size];
+  } qstack[sort_stack_size];
   int qpos, i, j, lo, hi, imin;
   struct sort_entry temp;
   float pivot;
 
-  if (N >= (1LL << stack_size)) {
+  if (N >= (1LL << sort_stack_size)) {
     error(
         "The stack size for sorting is too small."
         "Either increase it or reduce the number of parts per cell.");
@@ -199,7 +201,8 @@ RUNNER_CHECK_SORTS(stars)
  *      for recursive calls.
  */
 void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
-                          int cleanup, int rt_requests_sort, int clock) {
+                          const int cleanup, const int lock,
+                          const int rt_requests_sort, const int clock) {
 
   struct sort_entry *fingers[8];
   const int count = c->hydro.count;
@@ -213,6 +216,9 @@ void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
   if (c->hydro.super == NULL) error("Task called above the super level!!!");
 #endif
 
+  /* Should we lock the cell once more? */
+  if (lock) lock_lock(&c->hydro.extra_sort_lock);
+
   /* We need to do the local sorts plus whatever was requested further up. */
   flags |= c->hydro.do_sort;
   if (cleanup) {
@@ -221,8 +227,14 @@ void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
     flags &= ~c->hydro.sorted;
   }
   if (flags == 0 && !cell_get_flag(c, cell_flag_do_hydro_sub_sort) &&
-      !cell_get_flag(c, cell_flag_do_rt_sub_sort))
+      !cell_get_flag(c, cell_flag_do_rt_sub_sort)) {
+
+    /* Unlock before returning */
+    if (lock && lock_unlock(&c->hydro.extra_sort_lock) != 0)
+      error("Impossible to unlock the cell!");
+
     return;
+  }
 
   /* Check that the particles have been moved to the current time */
   if (flags && !cell_are_part_drifted(c, r->e)) {
@@ -266,7 +278,7 @@ void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
               r, c->progeny[k], flags,
               cleanup && (c->progeny[k]->hydro.dx_max_sort_old >
                           space_maxreldx * c->progeny[k]->dmin),
-              rt_requests_sort, 0);
+              lock, rt_requests_sort, /*clock=*/0);
           dx_max_sort = max(dx_max_sort, c->progeny[k]->hydro.dx_max_sort);
           dx_max_sort_old =
               max(dx_max_sort_old, c->progeny[k]->hydro.dx_max_sort_old);
@@ -427,6 +439,10 @@ void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
   cell_clear_flag(c, cell_flag_rt_requests_sort);
   c->hydro.requires_sorts = 0;
 
+  /* Make sure we unlock if necessary */
+  if (lock && lock_unlock(&c->hydro.extra_sort_lock) != 0)
+    error("Impossible to unlock the cell!");
+
   if (clock) TIMER_TOC(timer_dosort);
 }
 
@@ -682,8 +698,8 @@ void runner_do_all_hydro_sort(struct runner *r, struct cell *c) {
   if (c->hydro.super == c) {
 
     /* Sort everything */
-    runner_do_hydro_sort(r, c, 0x1FFF, /*cleanup=*/0, /*rt_requests_sort=*/0,
-                         /*timer=*/0);
+    runner_do_hydro_sort(r, c, 0x1FFF, /*cleanup=*/0, /* lock=*/0,
+                         /*rt_requests_sort=*/0, /*timer=*/0);
 
   } else {
 
diff --git a/src/runner_time_integration.c b/src/runner_time_integration.c
index 152fc8ae342e6b2bdf27b486443e0eb0f07cf152..802ff912e16bb44e54f6dd4628ec148174d18841 100644
--- a/src/runner_time_integration.c
+++ b/src/runner_time_integration.c
@@ -676,18 +676,14 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
 
   int updated = 0, g_updated = 0, s_updated = 0, sink_updated = 0,
       b_updated = 0;
-  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_end_max = 0,
-                ti_hydro_beg_max = 0;
+  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_beg_max = 0;
   integertime_t ti_rt_end_min = max_nr_timesteps, ti_rt_beg_max = 0;
   integertime_t ti_rt_min_step_size = max_nr_timesteps;
-  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_end_max = 0,
-                ti_gravity_beg_max = 0;
-  integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_end_max = 0,
-                ti_stars_beg_max = 0;
-  integertime_t ti_sinks_end_min = max_nr_timesteps, ti_sinks_end_max = 0,
-                ti_sinks_beg_max = 0;
+  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_beg_max = 0;
+  integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_beg_max = 0;
+  integertime_t ti_sinks_end_min = max_nr_timesteps, ti_sinks_beg_max = 0;
   integertime_t ti_black_holes_end_min = max_nr_timesteps,
-                ti_black_holes_end_max = 0, ti_black_holes_beg_max = 0;
+                ti_black_holes_beg_max = 0;
 
   /* No children? */
   if (!c->split) {
@@ -740,6 +736,10 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
          * debugging/consistency check. */
         rt_debugging_check_timestep(p, &ti_rt_new_step, &ti_new_step,
                                     e->max_nr_rt_subcycles, e->time_base);
+
+        if (ti_rt_new_step <= 0LL)
+          error("Got integer time step <= 0? %lld %lld",
+                get_part_rt_timestep(p, xp, e), ti_new_step);
 #endif
 
         /* Update particle */
@@ -758,7 +758,6 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
 
         /* What is the next sync-point ? */
         ti_hydro_end_min = min(ti_current + ti_new_step, ti_hydro_end_min);
-        ti_hydro_end_max = max(ti_current + ti_new_step, ti_hydro_end_max);
 
         /* What is the next starting point for this cell ? */
         ti_hydro_beg_max = max(ti_current, ti_hydro_beg_max);
@@ -768,8 +767,6 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
           /* What is the next sync-point ? */
           ti_gravity_end_min =
               min(ti_current + ti_new_step, ti_gravity_end_min);
-          ti_gravity_end_max =
-              max(ti_current + ti_new_step, ti_gravity_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_gravity_beg_max = max(ti_current, ti_gravity_beg_max);
@@ -798,7 +795,6 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
 
           /* What is the next sync-point ? */
           ti_hydro_end_min = min(ti_end, ti_hydro_end_min);
-          ti_hydro_end_max = max(ti_end, ti_hydro_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_hydro_beg_max = max(ti_beg, ti_hydro_beg_max);
@@ -828,7 +824,6 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
 
             /* What is the next sync-point ? */
             ti_gravity_end_min = min(ti_end, ti_gravity_end_min);
-            ti_gravity_end_max = max(ti_end, ti_gravity_end_max);
 
             /* What is the next starting point for this cell ? */
             ti_gravity_beg_max = max(ti_beg, ti_gravity_beg_max);
@@ -872,8 +867,6 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
           /* What is the next sync-point ? */
           ti_gravity_end_min =
               min(ti_current + ti_new_step, ti_gravity_end_min);
-          ti_gravity_end_max =
-              max(ti_current + ti_new_step, ti_gravity_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_gravity_beg_max = max(ti_current, ti_gravity_beg_max);
@@ -887,7 +880,6 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
 
             /* What is the next sync-point ? */
             ti_gravity_end_min = min(ti_end, ti_gravity_end_min);
-            ti_gravity_end_max = max(ti_end, ti_gravity_end_max);
 
             const integertime_t ti_beg =
                 get_integer_time_begin(ti_current + 1, gp->time_bin);
@@ -952,9 +944,7 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
         g_updated++;
 
         ti_stars_end_min = min(ti_current + ti_new_step, ti_stars_end_min);
-        ti_stars_end_max = max(ti_current + ti_new_step, ti_stars_end_max);
         ti_gravity_end_min = min(ti_current + ti_new_step, ti_gravity_end_min);
-        ti_gravity_end_max = max(ti_current + ti_new_step, ti_gravity_end_max);
 
         /* What is the next starting point for this cell ? */
         ti_stars_beg_max = max(ti_current, ti_stars_beg_max);
@@ -972,9 +962,7 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
               get_integer_time_begin(ti_current + 1, sp->time_bin);
 
           ti_stars_end_min = min(ti_end, ti_stars_end_min);
-          ti_stars_end_max = max(ti_end, ti_stars_end_max);
           ti_gravity_end_min = min(ti_end, ti_gravity_end_min);
-          ti_gravity_end_max = max(ti_end, ti_gravity_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_stars_beg_max = max(ti_beg, ti_stars_beg_max);
@@ -1012,9 +1000,7 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
         g_updated++;
 
         ti_sinks_end_min = min(ti_current + ti_new_step, ti_sinks_end_min);
-        ti_sinks_end_max = max(ti_current + ti_new_step, ti_sinks_end_max);
         ti_gravity_end_min = min(ti_current + ti_new_step, ti_gravity_end_min);
-        ti_gravity_end_max = max(ti_current + ti_new_step, ti_gravity_end_max);
 
         /* What is the next starting point for this cell ? */
         ti_sinks_beg_max = max(ti_current, ti_sinks_beg_max);
@@ -1032,9 +1018,7 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
               get_integer_time_begin(ti_current + 1, sink->time_bin);
 
           ti_sinks_end_min = min(ti_end, ti_sinks_end_min);
-          ti_sinks_end_max = max(ti_end, ti_sinks_end_max);
           ti_gravity_end_min = min(ti_end, ti_gravity_end_min);
-          ti_gravity_end_max = max(ti_end, ti_gravity_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_sinks_beg_max = max(ti_beg, ti_sinks_beg_max);
@@ -1089,10 +1073,7 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
 
         ti_black_holes_end_min =
             min(ti_current + ti_new_step, ti_black_holes_end_min);
-        ti_black_holes_end_max =
-            max(ti_current + ti_new_step, ti_black_holes_end_max);
         ti_gravity_end_min = min(ti_current + ti_new_step, ti_gravity_end_min);
-        ti_gravity_end_max = max(ti_current + ti_new_step, ti_gravity_end_max);
 
         /* What is the next starting point for this cell ? */
         ti_black_holes_beg_max = max(ti_current, ti_black_holes_beg_max);
@@ -1110,9 +1091,7 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
               get_integer_time_begin(ti_current + 1, bp->time_bin);
 
           ti_black_holes_end_min = min(ti_end, ti_black_holes_end_min);
-          ti_black_holes_end_max = max(ti_end, ti_black_holes_end_max);
           ti_gravity_end_min = min(ti_end, ti_gravity_end_min);
-          ti_gravity_end_max = max(ti_end, ti_gravity_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_black_holes_beg_max = max(ti_beg, ti_black_holes_beg_max);
@@ -1163,6 +1142,9 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
     }
   }
 
+  /* Flag something may have changed */
+  if (c->top == c) space_mark_cell_as_updated(r->e->s, c);
+
   /* Store the values. */
   c->hydro.updated = updated;
   c->grav.updated = g_updated;
@@ -1290,6 +1272,9 @@ void runner_do_timestep_collect(struct runner *r, struct cell *c,
     }
   }
 
+  /* Flag something may have changed */
+  if (c->top == c) space_mark_cell_as_updated(r->e->s, c);
+
   /* Store the collected values in the cell. */
   c->hydro.ti_end_min = ti_hydro_end_min;
   c->hydro.ti_beg_max = ti_hydro_beg_max;
@@ -1336,10 +1321,8 @@ void runner_do_limiter(struct runner *r, struct cell *c, int force,
   if (c->nodeID != engine_rank) error("Limiting dt of a foreign cell is nope.");
 #endif
 
-  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_end_max = 0,
-                ti_hydro_beg_max = 0;
-  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_end_max = 0,
-                ti_gravity_beg_max = 0;
+  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_beg_max = 0;
+  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_beg_max = 0;
 
   /* Limit irrespective of cell flags? */
   force = (force || cell_get_flag(c, cell_flag_do_hydro_limiter));
@@ -1370,6 +1353,9 @@ void runner_do_limiter(struct runner *r, struct cell *c, int force,
       }
     }
 
+    /* Flag something may have changed */
+    if (c->top == c) space_mark_cell_as_updated(r->e->s, c);
+
     /* Store the updated values */
     c->hydro.ti_end_min = min(c->hydro.ti_end_min, ti_hydro_end_min);
     c->hydro.ti_beg_max = max(c->hydro.ti_beg_max, ti_hydro_beg_max);
@@ -1431,7 +1417,6 @@ void runner_do_limiter(struct runner *r, struct cell *c, int force,
 
         /* What is the next sync-point ? */
         ti_hydro_end_min = min(ti_end_new, ti_hydro_end_min);
-        ti_hydro_end_max = max(ti_end_new, ti_hydro_end_max);
 
         /* What is the next starting point for this cell ? */
         ti_hydro_beg_max = max(ti_beg_new, ti_hydro_beg_max);
@@ -1444,7 +1429,6 @@ void runner_do_limiter(struct runner *r, struct cell *c, int force,
 
           /* What is the next sync-point ? */
           ti_gravity_end_min = min(ti_end_new, ti_gravity_end_min);
-          ti_gravity_end_max = max(ti_end_new, ti_gravity_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_gravity_beg_max = max(ti_beg_new, ti_gravity_beg_max);
@@ -1452,6 +1436,9 @@ void runner_do_limiter(struct runner *r, struct cell *c, int force,
       }
     }
 
+    /* Flag something may have changed */
+    if (c->top == c) space_mark_cell_as_updated(r->e->s, c);
+
     /* Store the updated values */
     c->hydro.ti_end_min = min(c->hydro.ti_end_min, ti_hydro_end_min);
     c->hydro.ti_beg_max = max(c->hydro.ti_beg_max, ti_hydro_beg_max);
@@ -1493,10 +1480,8 @@ void runner_do_sync(struct runner *r, struct cell *c, int force,
   if (c->nodeID != engine_rank) error("Syncing of a foreign cell is nope.");
 #endif
 
-  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_end_max = 0,
-                ti_hydro_beg_max = 0;
-  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_end_max = 0,
-                ti_gravity_beg_max = 0;
+  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_beg_max = 0;
+  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_beg_max = 0;
 
   /* Limit irrespective of cell flags? */
   force = (force || cell_get_flag(c, cell_flag_do_hydro_sync));
@@ -1526,6 +1511,9 @@ void runner_do_sync(struct runner *r, struct cell *c, int force,
       }
     }
 
+    /* Flag something may have changed */
+    if (c->top == c) space_mark_cell_as_updated(r->e->s, c);
+
     /* Store the updated values */
     c->hydro.ti_end_min = min(c->hydro.ti_end_min, ti_hydro_end_min);
     c->hydro.ti_beg_max = max(c->hydro.ti_beg_max, ti_hydro_beg_max);
@@ -1606,7 +1594,6 @@ void runner_do_sync(struct runner *r, struct cell *c, int force,
 
         /* What is the next sync-point ? */
         ti_hydro_end_min = min(ti_current + ti_new_step, ti_hydro_end_min);
-        ti_hydro_end_max = max(ti_current + ti_new_step, ti_hydro_end_max);
 
         /* What is the next starting point for this cell ? */
         ti_hydro_beg_max = max(ti_current, ti_hydro_beg_max);
@@ -1620,8 +1607,6 @@ void runner_do_sync(struct runner *r, struct cell *c, int force,
           /* What is the next sync-point ? */
           ti_gravity_end_min =
               min(ti_current + ti_new_step, ti_gravity_end_min);
-          ti_gravity_end_max =
-              max(ti_current + ti_new_step, ti_gravity_end_max);
 
           /* What is the next starting point for this cell ? */
           ti_gravity_beg_max = max(ti_current, ti_gravity_beg_max);
@@ -1629,6 +1614,9 @@ void runner_do_sync(struct runner *r, struct cell *c, int force,
       }
     }
 
+    /* Flag something may have changed */
+    if (c->top == c) space_mark_cell_as_updated(r->e->s, c);
+
     /* Store the updated values */
     c->hydro.ti_end_min = min(c->hydro.ti_end_min, ti_hydro_end_min);
     c->hydro.ti_beg_max = max(c->hydro.ti_beg_max, ti_hydro_beg_max);
@@ -1736,6 +1724,8 @@ void runner_do_rt_advance_cell_time(struct runner *r, struct cell *c,
 void runner_do_collect_rt_times(struct runner *r, struct cell *c,
                                 const int timer) {
 
+  TIMER_TIC;
+
   const struct engine *e = r->e;
   size_t rt_updated = 0;
 
diff --git a/src/scheduler.c b/src/scheduler.c
index 4f468d935a446178ec8bc6671790145977af6034..17578f104e8db0568156c96af503824c527d19f2 100644
--- a/src/scheduler.c
+++ b/src/scheduler.c
@@ -44,7 +44,6 @@
 #include "cycle.h"
 #include "engine.h"
 #include "error.h"
-#include "intrinsics.h"
 #include "kernel_hydro.h"
 #include "memuse.h"
 #include "mpiuse.h"
@@ -52,6 +51,7 @@
 #include "sort_part.h"
 #include "space.h"
 #include "space_getsid.h"
+#include "swift_intrinsics.h"
 #include "task.h"
 #include "threadpool.h"
 #include "timers.h"
@@ -77,8 +77,9 @@ static void scheduler_extend_unlocks(struct scheduler *s) {
     error("Failed to re-allocate unlocks.");
 
   /* Wait for all writes to the old buffer to complete. */
-  while (s->completed_unlock_writes < s->size_unlocks)
-    ;
+  while (s->completed_unlock_writes < s->size_unlocks) {
+    /* Nothing to do here. */
+  }
 
   /* Copy the buffers. */
   memcpy(unlocks_new, s->unlocks, sizeof(struct task *) * s->size_unlocks);
@@ -119,8 +120,9 @@ void scheduler_addunlock(struct scheduler *s, struct task *ta,
 #endif
 
   /* Wait for there to actually be space at my index. */
-  while (ind > s->size_unlocks)
-    ;
+  while (ind > s->size_unlocks) {
+    /* Nothing to do here. */
+  }
 
   /* Guard against case when more than (old) s->size_unlocks unlocks
    * are now pending. */
@@ -1277,10 +1279,10 @@ static void scheduler_splittask_hydro(struct task *t, struct scheduler *s) {
         break;
       }
 
-      /* Get the sort ID, use space_getsid and not t->flags
+      /* Get the sort ID, use space_getsid_and_swap_cells and not t->flags
          to make sure we get ci and cj swapped if needed. */
       double shift[3];
-      const int sid = space_getsid(s->space, &ci, &cj, shift);
+      const int sid = space_getsid_and_swap_cells(s->space, &ci, &cj, shift);
 
 #ifdef SWIFT_DEBUG_CHECKS
       if (sid != t->flags)
@@ -1370,11 +1372,12 @@ static void scheduler_splittask_hydro(struct task *t, struct scheduler *s) {
                     scheduler_addtask(s, task_type_pair, t->subtype, 0, 0,
                                       ci->progeny[j], cj->progeny[k]);
                 scheduler_splittask_hydro(tl, s);
-                tl->flags = space_getsid(s->space, &t->ci, &t->cj, shift);
+                tl->flags = space_getsid_and_swap_cells(s->space, &t->ci,
+                                                        &t->cj, shift);
               }
       }
     } /* pair interaction? */
-  }   /* iterate over the current task. */
+  } /* iterate over the current task. */
 }
 
 /**
@@ -1449,9 +1452,9 @@ static void scheduler_splittask_gravity(struct task *t, struct scheduler *s) {
                         s);
 
           } /* Self-gravity only */
-        }   /* Make tasks explicitly */
-      }     /* Cell is split */
-    }       /* Self interaction */
+        } /* Make tasks explicitly */
+      } /* Cell is split */
+    } /* Self interaction */
 
     /* Pair interaction? */
     else if (t->type == task_type_pair) {
@@ -1524,7 +1527,7 @@ static void scheduler_splittask_gravity(struct task *t, struct scheduler *s) {
         } /* Split the pair */
       }
     } /* pair interaction? */
-  }   /* iterate over the current task. */
+  } /* iterate over the current task. */
 }
 
 /**
@@ -1746,33 +1749,92 @@ struct task *scheduler_addtask(struct scheduler *s, enum task_types type,
   return t;
 }
 
+struct unlock_extra_data {
+  struct scheduler *s;
+  int *counts;
+  int *offsets;
+  struct task **unlocks;
+  struct task **scheduler_unlocks;
+};
+
 /**
- * @brief Set the unlock pointers in each task.
+ * @brief Cound the number of tasks unlocking each task
  *
- * @param s The #scheduler.
+ * @param map_data the index of unlocks in this pool thread.
+ * @param num_elements the number of indexes in this pool thread
+ * @param extra_data The scheduler and the count array.
  */
-void scheduler_set_unlocks(struct scheduler *s) {
-  /* Store the counts for each task. */
-  int *counts;
-  if ((counts = (int *)swift_malloc("counts", sizeof(int) * s->nr_tasks)) ==
-      NULL)
-    error("Failed to allocate temporary counts array.");
-  bzero(counts, sizeof(int) * s->nr_tasks);
-  for (int k = 0; k < s->nr_unlocks; k++) {
-    counts[s->unlock_ind[k]] += 1;
+void scheduler_set_unlock_counts_mapper(void *map_data, int num_elements,
+                                        void *extra_data) {
+
+  struct unlock_extra_data *data = (struct unlock_extra_data *)extra_data;
+  int *counts = (int *)data->counts;
+  struct scheduler *s = data->s;
+  int *volatile unlock_ind = (int *)map_data;
+
+  for (int k = 0; k < num_elements; k++) {
+    atomic_inc(&counts[unlock_ind[k]]);
 
     /* Check that we are not overflowing */
-    if (counts[s->unlock_ind[k]] < 0)
+    if (counts[unlock_ind[k]] < 0)
       error(
           "Task (type=%s/%s) unlocking more than %lld other tasks!\n"
           "This likely a result of having tasks at vastly different levels"
           "in the tree.\nYou may want to play with the 'Scheduler' "
           "parameters to modify the task splitting strategy and reduce"
           "the difference in task depths.",
-          taskID_names[s->tasks[s->unlock_ind[k]].type],
-          subtaskID_names[s->tasks[s->unlock_ind[k]].subtype],
+          taskID_names[s->tasks[unlock_ind[k]].type],
+          subtaskID_names[s->tasks[unlock_ind[k]].subtype],
           (1LL << (8 * sizeof(int) - 1)) - 1);
   }
+}
+
+/**
+ * @brief Cound the number of tasks unlocking each task
+ *
+ * @param map_data the index of unlocks in this pool thread.
+ * @param num_elements the number of indexes in this pool thread
+ * @param extra_data The scheduler and the list of offsets
+ */
+void scheduler_set_unlock_sorts_mapper(void *map_data, int num_elements,
+                                       void *extra_data) {
+
+  struct unlock_extra_data *data = (struct unlock_extra_data *)extra_data;
+  const struct scheduler *s = data->s;
+  struct task **unlocks = data->unlocks;
+  int *volatile offsets = data->offsets;
+  int *unlock_ind = (int *)map_data;
+  const size_t delta = unlock_ind - s->unlock_ind;
+
+  for (int k = 0; k < num_elements; k++) {
+    const int ind = unlock_ind[k];
+    unlocks[atomic_inc(&offsets[ind])] = s->unlocks[k + delta];
+  }
+}
+
+/**
+ * @brief Set the unlock pointers in each task.
+ *
+ * @param s The #scheduler.
+ * @param tp the #threadpool.
+ */
+void scheduler_set_unlocks(struct scheduler *s, struct threadpool *tp) {
+
+  /* Temporary extra data for the threadpool */
+  struct unlock_extra_data extra_data;
+
+  /* Store the counts for each task. */
+  int *counts;
+  if ((counts = (int *)swift_malloc("counts", sizeof(int) * s->nr_tasks)) ==
+      NULL)
+    error("Failed to allocate temporary counts array.");
+  bzero(counts, sizeof(int) * s->nr_tasks);
+
+  extra_data.s = s;
+  extra_data.counts = counts;
+  threadpool_map(tp, scheduler_set_unlock_counts_mapper, s->unlock_ind,
+                 s->nr_unlocks, sizeof(int), threadpool_auto_chunk_size,
+                 &extra_data);
 
   /* Compute the offset for each unlock block. */
   int *offsets;
@@ -1794,11 +1856,12 @@ void scheduler_set_unlocks(struct scheduler *s) {
   if ((unlocks = (struct task **)swift_malloc(
            "unlocks", sizeof(struct task *) * s->size_unlocks)) == NULL)
     error("Failed to allocate temporary unlocks array.");
-  for (int k = 0; k < s->nr_unlocks; k++) {
-    const int ind = s->unlock_ind[k];
-    unlocks[offsets[ind]] = s->unlocks[k];
-    offsets[ind] += 1;
-  }
+
+  extra_data.offsets = offsets;
+  extra_data.unlocks = unlocks;
+  threadpool_map(tp, scheduler_set_unlock_sorts_mapper, s->unlock_ind,
+                 s->nr_unlocks, sizeof(int), threadpool_auto_chunk_size,
+                 &extra_data);
 
   /* Swap the unlocks. */
   swift_free("unlocks", s->unlocks);
@@ -2001,7 +2064,8 @@ void scheduler_reweight(struct scheduler *s, int verbose) {
                  t->subtype == task_subtype_stars_prep2 ||
                  t->subtype == task_subtype_stars_feedback)
           cost = 1.f * wscale * scount_i * count_i;
-        else if (t->subtype == task_subtype_sink_swallow ||
+        else if (t->subtype == task_subtype_sink_density ||
+                 t->subtype == task_subtype_sink_swallow ||
                  t->subtype == task_subtype_sink_do_gas_swallow)
           cost = 1.f * wscale * count_i * sink_count_i;
         else if (t->subtype == task_subtype_sink_do_sink_swallow)
@@ -2047,7 +2111,8 @@ void scheduler_reweight(struct scheduler *s, int verbose) {
             cost = 2.f * wscale * (scount_i * count_j + scount_j * count_i) *
                    sid_scale[t->flags];
 
-        } else if (t->subtype == task_subtype_sink_swallow ||
+        } else if (t->subtype == task_subtype_sink_density ||
+                   t->subtype == task_subtype_sink_swallow ||
                    t->subtype == task_subtype_sink_do_gas_swallow) {
           if (t->ci->nodeID != nodeID)
             cost = 3.f * wscale * count_i * sink_count_j * sid_scale[t->flags];
@@ -2123,7 +2188,8 @@ void scheduler_reweight(struct scheduler *s, int verbose) {
                    sid_scale[t->flags];
           }
 
-        } else if (t->subtype == task_subtype_sink_swallow ||
+        } else if (t->subtype == task_subtype_sink_density ||
+                   t->subtype == task_subtype_sink_swallow ||
                    t->subtype == task_subtype_sink_do_gas_swallow) {
           if (t->ci->nodeID != nodeID) {
             cost =
@@ -2192,7 +2258,8 @@ void scheduler_reweight(struct scheduler *s, int verbose) {
             t->subtype == task_subtype_stars_prep2 ||
             t->subtype == task_subtype_stars_feedback) {
           cost = 1.f * (wscale * scount_i) * count_i;
-        } else if (t->subtype == task_subtype_sink_swallow ||
+        } else if (t->subtype == task_subtype_sink_density ||
+                   t->subtype == task_subtype_sink_swallow ||
                    t->subtype == task_subtype_sink_do_gas_swallow) {
           cost = 1.f * (wscale * sink_count_i) * count_i;
         } else if (t->subtype == task_subtype_sink_do_sink_swallow) {
@@ -2234,6 +2301,9 @@ void scheduler_reweight(struct scheduler *s, int verbose) {
       case task_type_bh_swallow_ghost2:
         if (t->ci == t->ci->hydro.super) cost = wscale * bcount_i;
         break;
+      case task_type_sink_density_ghost:
+        if (t->ci == t->ci->hydro.super) cost = wscale * sink_count_i;
+        break;
       case task_type_drift_part:
         cost = wscale * count_i;
         break;
@@ -2581,7 +2651,12 @@ void scheduler_enqueue(struct scheduler *s, struct task *t) {
 
         } else if (t->subtype == task_subtype_sf_counts) {
 
-          count = size = t->ci->mpi.pcell_size * sizeof(struct pcell_sf);
+          count = size = t->ci->mpi.pcell_size * sizeof(struct pcell_sf_stars);
+          buff = t->buff = malloc(count);
+
+        } else if (t->subtype == task_subtype_grav_counts) {
+
+          count = size = t->ci->mpi.pcell_size * sizeof(struct pcell_sf_grav);
           buff = t->buff = malloc(count);
 
         } else {
@@ -2677,9 +2752,15 @@ void scheduler_enqueue(struct scheduler *s, struct task *t) {
 
         } else if (t->subtype == task_subtype_sf_counts) {
 
-          size = count = t->ci->mpi.pcell_size * sizeof(struct pcell_sf);
+          size = count = t->ci->mpi.pcell_size * sizeof(struct pcell_sf_stars);
+          buff = t->buff = malloc(size);
+          cell_pack_sf_counts(t->ci, (struct pcell_sf_stars *)t->buff);
+
+        } else if (t->subtype == task_subtype_grav_counts) {
+
+          size = count = t->ci->mpi.pcell_size * sizeof(struct pcell_sf_grav);
           buff = t->buff = malloc(size);
-          cell_pack_sf_counts(t->ci, (struct pcell_sf *)t->buff);
+          cell_pack_grav_counts(t->ci, (struct pcell_sf_grav *)t->buff);
 
         } else {
           error("Unknown communication sub-type");
@@ -2811,6 +2892,77 @@ struct task *scheduler_unlock(struct scheduler *s, struct task *t) {
   return NULL;
 }
 
+/**
+ * Take note of the time at which a task was successfully fetched from the
+ * queue.
+ *
+ * @param s The #scheduler.
+ */
+void scheduler_mark_last_fetch(struct scheduler *s) {
+
+#if defined(SWIFT_DEBUG_CHECKS)
+  if (s->deadlock_waiting_time_ms <= 0.f) return;
+
+  ticks now = getticks();
+  ticks last = s->last_successful_task_fetch;
+  while (atomic_cas(&s->last_successful_task_fetch, last, now) != last) {
+    now = getticks();
+    last = s->last_successful_task_fetch;
+  }
+#endif
+}
+
+/**
+ * Abort the run if you're stuck doing nothing for too long.
+ * This function is intended to abort the mission if you're
+ * deadlocked somewhere and somehow. You might get core dumps
+ * this way. Alternatively, you might manually set a breakpoint
+ * with gdb when this function is called.
+ *
+ * @param s The #scheduler.
+ */
+void scheduler_check_deadlock(struct scheduler *s) {
+
+#if defined(SWIFT_DEBUG_CHECKS)
+  if (s->deadlock_waiting_time_ms <= 0.f) return;
+
+  /* lock_lock(&s->last_task_fetch_lock); */
+  ticks now = getticks();
+  ticks last = s->last_successful_task_fetch;
+
+  if (last == 0LL) {
+    /* Ensure that the first check each engine_launch doesn't fail. There is no
+     * guarantee how long it will take from the point where
+     * last_successful_task_fetch was reset to get to this point. A poorly
+     * chosen scheduler->deadlock_waiting_time_ms may abort a big run in places
+     * where there is no deadlock. Better safe than sorry, so at start-up, the
+     * last successful task fetch time is marked as 0. So we just exit without
+     * checking the time. */
+    while (atomic_cas(&s->last_successful_task_fetch, last, now) != last) {
+      now = getticks();
+      last = s->last_successful_task_fetch;
+    }
+    return;
+  }
+
+  /* ticks on different CPUs may disagree a bit. So we may end up
+   * with last > now, and consequently negative idle time, which
+   * then overflows unsigned long longs and gives false positives. */
+  const ticks big = max(now, last);
+  const ticks small = min(now, last);
+  const double idle_time = clocks_diff_ticks(big, small);
+
+  if (idle_time > s->deadlock_waiting_time_ms) {
+    message(
+        "Detected what looks like a deadlock after %g ms of no new task being "
+        "fetched from queues. Dumping diagnostic data.",
+        idle_time);
+    engine_dump_diagnostic_data(s->e);
+    error("Aborting now.");
+  }
+#endif
+}
+
 /**
  * @brief Get a task, preferably from the given queue.
  *
@@ -2854,10 +3006,11 @@ struct task *scheduler_gettask(struct scheduler *s, int qid,
           TIMER_TIC
           res = queue_gettask(&s->queues[qids[ind]], prev, 0);
           TIMER_TOC(timer_qsteal);
-          if (res != NULL)
+          if (res != NULL) {
             break;
-          else
+          } else {
             qids[ind] = qids[--count];
+          }
         }
         if (res != NULL) break;
       }
@@ -2877,10 +3030,13 @@ struct task *scheduler_gettask(struct scheduler *s, int qid,
       }
       pthread_mutex_unlock(&s->sleep_mutex);
     }
+
+    scheduler_check_deadlock(s);
   }
 
-  /* Start the timer on this task, if we got one. */
   if (res != NULL) {
+    scheduler_mark_last_fetch(s);
+    /* Start the timer on this task, if we got one. */
     res->tic = getticks();
 #ifdef SWIFT_DEBUG_TASKS
     res->rid = qid;
@@ -2943,6 +3099,11 @@ void scheduler_init(struct scheduler *s, struct space *space, int nr_tasks,
   s->tasks = NULL;
   s->tasks_ind = NULL;
   scheduler_reset(s, nr_tasks);
+
+#if defined(SWIFT_DEBUG_CHECKS)
+  s->e = space->e;
+  s->last_successful_task_fetch = 0LL;
+#endif
 }
 
 /**
diff --git a/src/scheduler.h b/src/scheduler.h
index 349cdc38a3c770d0418e43f00167a4e76edb8723..e8a245c71ede84c30e1b11bb3015c651f29b2f50 100644
--- a/src/scheduler.h
+++ b/src/scheduler.h
@@ -125,6 +125,20 @@ struct scheduler {
 
   /* Frequency of the task levels dumping. */
   int frequency_task_levels;
+
+#if defined(SWIFT_DEBUG_CHECKS)
+  /* Stuff for the deadlock detector */
+
+  /* How long to wait (in ms) before assuming we're in a deadlock */
+  float deadlock_waiting_time_ms;
+
+  /* Time at which last task was successfully retrieved from a queue */
+  ticks last_successful_task_fetch;
+
+  /* needed to dump queues on deadlock detection */
+  struct engine *e;
+
+#endif /* SWIFT_DEBUG_CHECKS */
 };
 
 /* Inlined functions (for speed). */
@@ -178,8 +192,10 @@ scheduler_activate_send(struct scheduler *s, struct link *link,
   struct link *l = NULL;
   for (l = link;
        l != NULL && !(l->t->cj->nodeID == nodeID && l->t->subtype == subtype);
-       l = l->next)
-    ;
+       l = l->next) {
+    /* Nothing to do here */
+  }
+
   if (l == NULL) {
     error("Missing link to send task.");
   }
@@ -201,8 +217,10 @@ __attribute__((always_inline)) INLINE static struct link *
 scheduler_activate_recv(struct scheduler *s, struct link *link,
                         const enum task_subtypes subtype) {
   struct link *l = NULL;
-  for (l = link; l != NULL && l->t->subtype != subtype; l = l->next)
-    ;
+  for (l = link; l != NULL && l->t->subtype != subtype; l = l->next) {
+    /* Nothing to do here */
+  }
+
   if (l == NULL) {
     error("Missing link to recv task.");
   }
@@ -227,8 +245,10 @@ scheduler_activate_pack(struct scheduler *s, struct link *link,
   struct link *l = NULL;
   for (l = link;
        l != NULL && !(l->t->cj->nodeID == nodeID && l->t->subtype == subtype);
-       l = l->next)
-    ;
+       l = l->next) {
+    /* Nothing to do here */
+  }
+
   if (l == NULL) {
     error("Missing link to pack task.");
   }
@@ -250,8 +270,10 @@ __attribute__((always_inline)) INLINE static struct link *
 scheduler_activate_unpack(struct scheduler *s, struct link *link,
                           enum task_subtypes subtype) {
   struct link *l = NULL;
-  for (l = link; l != NULL && l->t->subtype != subtype; l = l->next)
-    ;
+  for (l = link; l != NULL && l->t->subtype != subtype; l = l->next) {
+    /* Nothing to do here */
+  }
+
   if (l == NULL) {
     error("Missing link to unpack task.");
   }
@@ -279,7 +301,7 @@ void scheduler_splittasks(struct scheduler *s, const int fof_tasks,
 struct task *scheduler_done(struct scheduler *s, struct task *t);
 struct task *scheduler_unlock(struct scheduler *s, struct task *t);
 void scheduler_addunlock(struct scheduler *s, struct task *ta, struct task *tb);
-void scheduler_set_unlocks(struct scheduler *s);
+void scheduler_set_unlocks(struct scheduler *s, struct threadpool *tp);
 void scheduler_dump_queue(struct scheduler *s);
 void scheduler_print_tasks(const struct scheduler *s, const char *fileName);
 void scheduler_clean(struct scheduler *s);
diff --git a/src/serial_io.c b/src/serial_io.c
index 2e39dc4d2dc53707cb2a792c945d3b579a04fc51..6fd377250d28930a67cc1a1bc3ac2e5f22cc811b 100644
--- a/src/serial_io.c
+++ b/src/serial_io.c
@@ -63,6 +63,9 @@
 #include "version.h"
 #include "xmf.h"
 
+/* Max number of entries that can be written for a given particle type */
+static const int io_max_size_output_list = 100;
+
 /**
  * @brief Reads a data array from a given HDF5 group.
  *
@@ -299,7 +302,8 @@ void prepare_array_serial(
     h_err = H5Pset_chunk(h_prop, rank, chunk_shape);
     if (h_err < 0)
       error("Error while setting chunk size (%llu, %llu) for field '%s'.",
-            chunk_shape[0], chunk_shape[1], props.name);
+            (unsigned long long)chunk_shape[0],
+            (unsigned long long)chunk_shape[1], props.name);
   }
 
   /* Are we imposing some form of lossy compression filter? */
@@ -353,6 +357,9 @@ void prepare_array_serial(
   io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
   io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
   io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -775,7 +782,14 @@ void read_ic_serial(char* fileName, const struct unit_system* internal_units,
     /* Is it this rank's turn to read ? */
     if (rank == mpi_rank) {
 
-      h_file = H5Fopen(fileName, H5F_ACC_RDONLY, H5P_DEFAULT);
+      /* Set the minimal API version to avoid issues with advanced features */
+      hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+      herr_t err =
+          H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                               HDF5_HIGHEST_FILE_FORMAT_VERSION);
+      if (err < 0) error("Error setting the hdf5 API version");
+
+      h_file = H5Fopen(fileName, H5F_ACC_RDONLY, h_props);
       if (h_file < 0)
         error("Error while opening file '%s' on rank %d.", fileName, mpi_rank);
 
@@ -794,8 +808,8 @@ void read_ic_serial(char* fileName, const struct unit_system* internal_units,
           error("Error while opening particle group %s.", partTypeGroupName);
 
         int num_fields = 0;
-        struct io_props list[100];
-        bzero(list, 100 * sizeof(struct io_props));
+        struct io_props list[io_max_size_output_list];
+        bzero(list, io_max_size_output_list * sizeof(struct io_props));
         size_t Nparticles = 0;
 
         /* Read particle fields into the particle structure */
@@ -882,6 +896,7 @@ void read_ic_serial(char* fileName, const struct unit_system* internal_units,
 
       /* Close file */
       H5Fclose(h_file);
+      H5Pclose(h_props);
     }
 
     /* Wait for the read of the reading to complete */
@@ -945,6 +960,7 @@ void read_ic_serial(char* fileName, const struct unit_system* internal_units,
  * @param e The engine containing all the system.
  * @param internal_units The #unit_system used internally
  * @param snapshot_units The #unit_system used in the snapshots
+ * @param fof Is this a snapshot related to a stand-alone FOF call?
  * @param mpi_rank The MPI rank of this node.
  * @param mpi_size The number of MPI ranks.
  * @param comm The MPI communicator.
@@ -961,10 +977,10 @@ void read_ic_serial(char* fileName, const struct unit_system* internal_units,
 void write_output_serial(struct engine* e,
                          const struct unit_system* internal_units,
                          const struct unit_system* snapshot_units,
-                         const int mpi_rank, const int mpi_size, MPI_Comm comm,
-                         MPI_Info info) {
+                         const int fof, const int mpi_rank, const int mpi_size,
+                         MPI_Comm comm, MPI_Info info) {
 
-  hid_t h_file = 0, h_grp = 0;
+  hid_t h_file = 0, h_grp = 0, h_props = 0;
   int numFiles = 1;
   const struct part* parts = e->s->parts;
   const struct xpart* xparts = e->s->xparts;
@@ -1157,9 +1173,15 @@ void write_output_serial(struct engine* e,
     /* Write the part corresponding to this specific output */
     xmf_write_outputheader(xmfFile, fileName, e->time);
 
+    /* Set the minimal API version to avoid issues with advanced features */
+    h_props = H5Pcreate(H5P_FILE_ACCESS);
+    herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                      HDF5_HIGHEST_FILE_FORMAT_VERSION);
+    if (err < 0) error("Error setting the hdf5 API version");
+
     /* Open file */
     /* message("Opening file '%s'.", fileName); */
-    h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+    h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
     if (h_file < 0) error("Error while opening file '%s'.", fileName);
 
     /* Open header to write simulation properties */
@@ -1263,7 +1285,7 @@ void write_output_serial(struct engine* e,
     ic_info_write_hdf5(e->ics_metadata, h_file);
 
     /* Write all the meta-data */
-    io_write_meta_data(h_file, e, internal_units, snapshot_units);
+    io_write_meta_data(h_file, e, internal_units, snapshot_units, fof);
 
     /* Loop over all particle types */
     for (int ptype = 0; ptype < swift_type_count; ptype++) {
@@ -1299,14 +1321,22 @@ void write_output_serial(struct engine* e,
 
     /* Close file */
     H5Fclose(h_file);
+    H5Pclose(h_props);
   }
 
   /* Now write the top-level cell structure */
-  hid_t h_file_cells = 0, h_grp_cells = 0;
+  hid_t h_file_cells = 0, h_grp_cells = 0, h_props_cells = 0;
   if (mpi_rank == 0) {
 
+    /* Set the minimal API version to avoid issues with advanced features */
+    h_props_cells = H5Pcreate(H5P_FILE_ACCESS);
+    herr_t err =
+        H5Pset_libver_bounds(h_props_cells, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                             HDF5_HIGHEST_FILE_FORMAT_VERSION);
+    if (err < 0) error("Error setting the hdf5 API version");
+
     /* Open the snapshot on rank 0 */
-    h_file_cells = H5Fopen(fileName, H5F_ACC_RDWR, H5P_DEFAULT);
+    h_file_cells = H5Fopen(fileName, H5F_ACC_RDWR, h_props_cells);
     if (h_file_cells < 0)
       error("Error while opening file '%s' on rank %d.", fileName, mpi_rank);
 
@@ -1327,6 +1357,7 @@ void write_output_serial(struct engine* e,
   if (mpi_rank == 0) {
     H5Gclose(h_grp_cells);
     H5Fclose(h_file_cells);
+    H5Pclose(h_props_cells);
   }
 
   /* Now loop over ranks and write the data */
@@ -1335,7 +1366,14 @@ void write_output_serial(struct engine* e,
     /* Is it this rank's turn to write ? */
     if (rank == mpi_rank) {
 
-      h_file = H5Fopen(fileName, H5F_ACC_RDWR, H5P_DEFAULT);
+      /* Set the minimal API version to avoid issues with advanced features */
+      h_props = H5Pcreate(H5P_FILE_ACCESS);
+      herr_t err =
+          H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                               HDF5_HIGHEST_FILE_FORMAT_VERSION);
+      if (err < 0) error("Error setting the hdf5 API version");
+
+      h_file = H5Fopen(fileName, H5F_ACC_RDWR, h_props);
       if (h_file < 0)
         error("Error while opening file '%s' on rank %d.", fileName, mpi_rank);
 
@@ -1362,8 +1400,8 @@ void write_output_serial(struct engine* e,
           error("Error while opening particle group %s.", partTypeGroupName);
 
         int num_fields = 0;
-        struct io_props list[100];
-        bzero(list, 100 * sizeof(struct io_props));
+        struct io_props list[io_max_size_output_list];
+        bzero(list, io_max_size_output_list * sizeof(struct io_props));
         size_t Nparticles = 0;
 
         struct part* parts_written = NULL;
@@ -1636,6 +1674,15 @@ void write_output_serial(struct engine* e,
             error("Particle Type %d not yet supported. Aborting", ptype);
         }
 
+        /* Verify we are not going to crash when writing below */
+        if (num_fields >= io_max_size_output_list)
+          error("Too many fields to write for particle type %d", ptype);
+        for (int i = 0; i < num_fields; ++i) {
+          if (!list[i].is_used) error("List of field contains an empty entry!");
+          if (!list[i].dimension)
+            error("Dimension of field '%s' is <= 1!", list[i].name);
+        }
+
         /* Did the user specify a non-standard default for the entire particle
          * type? */
         const enum lossy_compression_schemes compression_level_current_default =
@@ -1689,6 +1736,7 @@ void write_output_serial(struct engine* e,
 
       /* Close file */
       H5Fclose(h_file);
+      H5Pclose(h_props);
     }
 
     /* Wait for the read of the reading to complete */
diff --git a/src/serial_io.h b/src/serial_io.h
index 350da844aa034575c04f7efa432e84abb933c5ef..2a323f543f53bad03e3e661d6d8189b69d907f68 100644
--- a/src/serial_io.h
+++ b/src/serial_io.h
@@ -54,8 +54,8 @@ void read_ic_serial(char* fileName, const struct unit_system* internal_units,
 void write_output_serial(struct engine* e,
                          const struct unit_system* internal_units,
                          const struct unit_system* snapshot_units,
-                         const int mpi_rank, const int mpi_size, MPI_Comm comm,
-                         MPI_Info info);
+                         const int fof, const int mpi_rank, const int mpi_size,
+                         MPI_Comm comm, MPI_Info info);
 
 #endif /* HAVE_HDF5 && WITH_MPI && !HAVE_PARALLEL_HDF5 */
 
diff --git a/src/shadowswift/voronoi.h b/src/shadowswift/voronoi.h
new file mode 100644
index 0000000000000000000000000000000000000000..bf8be746009ede022270b5e319387874e83eea7e
--- /dev/null
+++ b/src/shadowswift/voronoi.h
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *                             Yolan Uyttenhove (Yolan.Uyttenhove@UGent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFTSIM_SHADOWSWIFT_VORONOI_H
+#define SWIFTSIM_SHADOWSWIFT_VORONOI_H
+
+/* Local includes */
+#include "inline.h"
+
+struct voronoi {
+  int pair_count[27];
+};
+
+struct voronoi_pair {};
+
+__attribute__((always_inline)) INLINE static void voronoi_destroy(
+    struct voronoi* v) {}
+
+#endif  // SWIFTSIM_SHADOWSWIFT_VORONOI_H
diff --git a/src/single_io.c b/src/single_io.c
index 4fffaee3ab77b9cb5d2f65333e3b41ae7ae95b33..5ea174ae970026669d0109ab93e1db3c835712b9 100644
--- a/src/single_io.c
+++ b/src/single_io.c
@@ -63,6 +63,9 @@
 #include "version.h"
 #include "xmf.h"
 
+/* Max number of entries that can be written for a given particle type */
+static const int io_max_size_output_list = 100;
+
 /**
  * @brief Reads a data array from a given HDF5 group.
  *
@@ -297,36 +300,36 @@ void write_array_single(const struct engine* e, hid_t grp, const char* fileName,
   /* Dataset properties */
   hid_t h_prop = H5Pcreate(H5P_DATASET_CREATE);
 
-  /* Set chunk size if have some particles */
+  /* Create filters and set compression level if we have something to write */
+  char comp_buffer[32] = "None";
   if (N > 0) {
+
+    /* Set chunk size */
     h_err = H5Pset_chunk(h_prop, rank, chunk_shape);
     if (h_err < 0)
       error("Error while setting chunk size (%llu, %llu) for field '%s'.",
             (unsigned long long)chunk_shape[0],
             (unsigned long long)chunk_shape[1], props.name);
-  }
-
-  /* Are we imposing some form of lossy compression filter? */
-  char comp_buffer[32] = "None";
-  if (lossy_compression != compression_write_lossless)
-    set_hdf5_lossy_compression(&h_prop, &h_type, lossy_compression, props.name,
-                               comp_buffer);
-
-  /* Impose GZIP and shuffle data compression */
-  if (e->snapshot_compression > 0 && N > 0) {
-    h_err = H5Pset_shuffle(h_prop);
-    if (h_err < 0)
-      error("Error while setting shuffling options for field '%s'.",
-            props.name);
 
-    h_err = H5Pset_deflate(h_prop, e->snapshot_compression);
-    if (h_err < 0)
-      error("Error while setting compression options for field '%s'.",
-            props.name);
-  }
+    /* Are we imposing some form of lossy compression filter? */
+    if (lossy_compression != compression_write_lossless)
+      set_hdf5_lossy_compression(&h_prop, &h_type, lossy_compression,
+                                 props.name, comp_buffer);
+
+    /* Impose GZIP and shuffle data compression */
+    if (e->snapshot_compression > 0) {
+      h_err = H5Pset_shuffle(h_prop);
+      if (h_err < 0)
+        error("Error while setting shuffling options for field '%s'.",
+              props.name);
+
+      h_err = H5Pset_deflate(h_prop, e->snapshot_compression);
+      if (h_err < 0)
+        error("Error while setting compression options for field '%s'.",
+              props.name);
+    }
 
-  /* Impose check-sum to verify data corruption */
-  if (N > 0) {
+    /* Impose check-sum to verify data corruption */
     h_err = H5Pset_fletcher32(h_prop);
     if (h_err < 0)
       error("Error while setting checksum options for field '%s'.", props.name);
@@ -362,6 +365,9 @@ void write_array_single(const struct engine* e, hid_t grp, const char* fileName,
   io_write_attribute_f(h_data, "a-scale exponent", props.scale_factor_exponent);
   io_write_attribute_s(h_data, "Expression for physical CGS units", buffer);
   io_write_attribute_s(h_data, "Lossy compression filter", comp_buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write the actual number this conversion factor corresponds to */
   const double factor =
@@ -657,8 +663,8 @@ void read_ic_single(
       error("Error while opening particle group %s.", partTypeGroupName);
 
     int num_fields = 0;
-    struct io_props list[100];
-    bzero(list, 100 * sizeof(struct io_props));
+    struct io_props list[io_max_size_output_list];
+    bzero(list, io_max_size_output_list * sizeof(struct io_props));
     size_t Nparticles = 0;
 
     /* Read particle fields into the structure */
@@ -799,6 +805,7 @@ void read_ic_single(
  * @param e The engine containing all the system.
  * @param internal_units The #unit_system used internally
  * @param snapshot_units The #unit_system used in the snapshots
+ * @param fof Is this a snapshot related to a stand-alone FOF call?
  *
  * Creates an HDF5 output file and writes the particles contained
  * in the engine. If such a file already exists, it is erased and replaced
@@ -810,7 +817,8 @@ void read_ic_single(
  */
 void write_output_single(struct engine* e,
                          const struct unit_system* internal_units,
-                         const struct unit_system* snapshot_units) {
+                         const struct unit_system* snapshot_units,
+                         const int fof) {
 
   hid_t h_file = 0, h_grp = 0;
   int numFiles = 1;
@@ -982,9 +990,15 @@ void write_output_single(struct engine* e,
 
   };
 
+  /* Set the minimal API version to avoid issues with advanced features */
+  hid_t h_props = H5Pcreate(H5P_FILE_ACCESS);
+  herr_t err = H5Pset_libver_bounds(h_props, HDF5_LOWEST_FILE_FORMAT_VERSION,
+                                    HDF5_HIGHEST_FILE_FORMAT_VERSION);
+  if (err < 0) error("Error setting the hdf5 API version");
+
   /* Open file */
   /* message("Opening file '%s'.", fileName); */
-  h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+  h_file = H5Fcreate(fileName, H5F_ACC_TRUNC, H5P_DEFAULT, h_props);
   if (h_file < 0) error("Error while opening file '%s'.", fileName);
 
   /* Open header to write simulation properties */
@@ -1093,7 +1107,7 @@ void write_output_single(struct engine* e,
   ic_info_write_hdf5(e->ics_metadata, h_file);
 
   /* Write all the meta-data */
-  io_write_meta_data(h_file, e, internal_units, snapshot_units);
+  io_write_meta_data(h_file, e, internal_units, snapshot_units, fof);
 
   /* Now write the top-level cell structure */
   long long global_offsets[swift_type_count] = {0};
@@ -1141,8 +1155,8 @@ void write_output_single(struct engine* e,
     io_write_attribute_ll(h_grp, "TotalNumberOfParticles", N_total[ptype]);
 
     int num_fields = 0;
-    struct io_props list[100];
-    bzero(list, 100 * sizeof(struct io_props));
+    struct io_props list[io_max_size_output_list];
+    bzero(list, io_max_size_output_list * sizeof(struct io_props));
     size_t N = 0;
 
     struct part* parts_written = NULL;
@@ -1411,6 +1425,15 @@ void write_output_single(struct engine* e,
         error("Particle Type %d not yet supported. Aborting", ptype);
     }
 
+    /* Verify we are not going to crash when writing below */
+    if (num_fields >= io_max_size_output_list)
+      error("Too many fields to write for particle type %d", ptype);
+    for (int i = 0; i < num_fields; ++i) {
+      if (!list[i].is_used) error("List of field contains an empty entry!");
+      if (!list[i].dimension)
+        error("Dimension of field '%s' is <= 1!", list[i].name);
+    }
+
     /* Did the user specify a non-standard default for the entire particle
      * type? */
     const enum lossy_compression_schemes compression_level_current_default =
@@ -1464,6 +1487,7 @@ void write_output_single(struct engine* e,
 
   /* Close file */
   H5Fclose(h_file);
+  H5Pclose(h_props);
 
   e->snapshot_output_count++;
   if (e->snapshot_invoke_stf) e->stf_output_count++;
diff --git a/src/single_io.h b/src/single_io.h
index 13a1e1be9c292aafc3fd8c0c5754fa32b7e53512..7d473d863b3ed0b8e73b8af1f437840adf982741 100644
--- a/src/single_io.h
+++ b/src/single_io.h
@@ -45,7 +45,8 @@ void read_ic_single(
 
 void write_output_single(struct engine* e,
                          const struct unit_system* internal_units,
-                         const struct unit_system* snapshot_units);
+                         const struct unit_system* snapshot_units,
+                         const int fof);
 
 #endif /* HAVE_HDF5 && !WITH_MPI */
 
diff --git a/src/sink.c b/src/sink.c
new file mode 100644
index 0000000000000000000000000000000000000000..0d1b79e35b6f9fb1dacbbee9a1acb3b877b89987
--- /dev/null
+++ b/src/sink.c
@@ -0,0 +1,341 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+/* Config parameters. */
+#include <config.h>
+
+/* Some standard headers. */
+#include <float.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/* Local headers. */
+#include "active.h"
+#include "error.h"
+#include "sink_properties.h"
+#include "version.h"
+
+struct exact_density_data {
+  const struct engine *e;
+  const struct space *s;
+  int counter_global;
+};
+
+/**
+ * @brief Mapper function for the exact sink checks.
+ *
+ * @brief map_data The #sink.
+ * @brief nr_sinks The number of star particles.
+ * @brief extra_data Pointers to the structure containing global interaction
+ * counters.
+ */
+void sink_exact_density_compute_mapper(void *map_data, int nr_sinks,
+                                       void *extra_data) {
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+
+  /* Unpack the data */
+  struct sink *restrict sinks = (struct sink *)map_data;
+  struct exact_density_data *data = (struct exact_density_data *)extra_data;
+  const struct space *s = data->s;
+  const struct engine *e = data->e;
+  const int periodic = s->periodic;
+  const double dim[3] = {s->dim[0], s->dim[1], s->dim[2]};
+  int counter = 0;
+
+  for (int i = 0; i < nr_sinks; ++i) {
+
+    struct sink *si = &sinks[i];
+    const long long id = si->id;
+
+    /* Is the particle active and part of the subset to be tested ? */
+    if (id % SWIFT_SINK_DENSITY_CHECKS == 0 && sink_is_starting(si, e)) {
+
+      /* Get some information about the particle */
+      const double pix[3] = {si->x[0], si->x[1], si->x[2]};
+      const double hi = si->h;
+      const float hi_inv = 1.f / hi;
+      const float hig2 = hi * hi * kernel_gamma2;
+
+      /* Be ready for the calculation */
+      int N_density_exact = 0;
+      double rho_exact = 0.;
+      double n_exact = 0.;
+
+      /* Interact it with all other particles in the space.*/
+      for (int j = 0; j < (int)s->nr_parts; ++j) {
+
+        const struct part *pj = &s->parts[j];
+
+        /* Compute the pairwise distance. */
+        double dx = pj->x[0] - pix[0];
+        double dy = pj->x[1] - pix[1];
+        double dz = pj->x[2] - pix[2];
+
+        /* Now apply periodic BC */
+        if (periodic) {
+          dx = nearest(dx, dim[0]);
+          dy = nearest(dy, dim[1]);
+          dz = nearest(dz, dim[2]);
+        }
+
+        const double r2 = dx * dx + dy * dy + dz * dz;
+
+        /* Interact loop of type 1? */
+        if (r2 < hig2) {
+
+          const float mj = pj->mass;
+
+          float wi, wi_dx;
+
+          /* Kernel function */
+          const float r = sqrtf(r2);
+          const float ui = r * hi_inv;
+          kernel_deval(ui, &wi, &wi_dx);
+
+          /* Flag that we found an inhibited neighbour */
+          if (part_is_inhibited(pj, e)) {
+            si->inhibited_check_exact = 1;
+          } else {
+
+            /* Density */
+            rho_exact += mj * wi;
+
+            /* Number density */
+            n_exact += wi;
+
+            /* Number of neighbours */
+            N_density_exact++;
+          }
+        }
+      }
+
+      /* Store the exact answer */
+      si->N_check_density_exact = N_density_exact;
+      si->rho_check_exact = rho_exact * pow_dimension(hi_inv);
+      si->n_check_exact = n_exact * pow_dimension(hi_inv);
+
+      counter++;
+    }
+  }
+  atomic_add(&data->counter_global, counter);
+
+#else
+  error("Sink checking function called without the corresponding flag.");
+#endif
+}
+
+/**
+ * @brief Compute the exact interactions for a selection of star particles
+ * by running a brute force loop over all the particles in the simulation.
+ *
+ * Will be incorrect over MPI.
+ *
+ * @param s The #space.
+ * @param e The #engine.
+ */
+void sink_exact_density_compute(struct space *s, const struct engine *e) {
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+
+  const ticks tic = getticks();
+
+  struct exact_density_data data;
+  data.e = e;
+  data.s = s;
+  data.counter_global = 0;
+
+  threadpool_map(&s->e->threadpool, sink_exact_density_compute_mapper, s->sinks,
+                 s->nr_sinks, sizeof(struct sink), 0, &data);
+
+  if (e->verbose)
+    message("Computed exact densities for %d sinks (took %.3f %s). ",
+            data.counter_global, clocks_from_ticks(getticks() - tic),
+            clocks_getunit());
+#else
+  error("Sink checking function called without the corresponding flag.");
+#endif
+}
+
+/**
+ * @brief Check the star particles' density and force calculations against the
+ * values obtained via the brute-force summation.
+ *
+ * @param s The #space.
+ * @param e The #engine.
+ * @param rel_tol Relative tolerance for the checks
+ */
+void sink_exact_density_check(struct space *s, const struct engine *e,
+                              const double rel_tol) {
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+
+  const ticks tic = getticks();
+
+  const struct sink *sinks = s->sinks;
+  const size_t nr_sinks = s->nr_sinks;
+
+  const double eta = e->sink_properties->eta_neighbours;
+  const double N_ngb_target =
+      (4. / 3.) * M_PI * pow_dimension(kernel_gamma * eta);
+  const double N_ngb_max =
+      N_ngb_target + 2. * e->sink_properties->delta_neighbours;
+  const double N_ngb_min =
+      N_ngb_target - 2. * e->sink_properties->delta_neighbours;
+
+  /* File name */
+  char file_name_swift[100];
+  sprintf(file_name_swift, "sink_checks_swift_step%.4d.dat", e->step);
+
+  /* Creare files and write header */
+  FILE *file_swift = fopen(file_name_swift, "w");
+  if (file_swift == NULL) error("Could not create file '%s'.", file_name_swift);
+  fprintf(file_swift, "# Sink accuracy test - SWIFT DENSITIES\n");
+  fprintf(file_swift, "# N= %d\n", SWIFT_SINK_DENSITY_CHECKS);
+  fprintf(file_swift, "# periodic= %d\n", s->periodic);
+  fprintf(file_swift, "# N_ngb_target= %f +/- %f\n", N_ngb_target,
+          e->sink_properties->delta_neighbours);
+  fprintf(file_swift, "# Git Branch: %s\n", git_branch());
+  fprintf(file_swift, "# Git Revision: %s\n", git_revision());
+  fprintf(file_swift, "# %16s %16s %16s %16s %16s %7s %7s %16s %16s %16s\n",
+          "id", "pos[0]", "pos[1]", "pos[2]", "h", "Nd", "Nf", "rho", "n_rho",
+          "N_ngb");
+
+  /* Output particle SWIFT densities */
+  for (size_t i = 0; i < nr_sinks; ++i) {
+
+    const struct sink *si = &sinks[i];
+    const long long id = si->id;
+
+    const double N_ngb = (4. / 3.) * M_PI * kernel_gamma * kernel_gamma *
+                         kernel_gamma * si->h * si->h * si->h * si->n_check;
+
+    if (id % SWIFT_SINK_DENSITY_CHECKS == 0 && sink_is_starting(si, e)) {
+
+      fprintf(
+          file_swift,
+          "%18lld %16.8e %16.8e %16.8e %16.8e %7d %7d %16.8e %16.8e %16.8e\n",
+          id, si->x[0], si->x[1], si->x[2], si->h, si->N_check_density, 0,
+          si->rho_check, si->n_check, N_ngb);
+    }
+  }
+
+  if (e->verbose)
+    message("Written SWIFT densities in file '%s'.", file_name_swift);
+
+  /* Be nice */
+  fclose(file_swift);
+
+  /* File name */
+  char file_name_exact[100];
+  sprintf(file_name_exact, "sink_checks_exact_step%.4d.dat", e->step);
+
+  /* Creare files and write header */
+  FILE *file_exact = fopen(file_name_exact, "w");
+  if (file_exact == NULL) error("Could not create file '%s'.", file_name_exact);
+  fprintf(file_exact, "# Sink accuracy test - EXACT DENSITIES\n");
+  fprintf(file_exact, "# N= %d\n", SWIFT_SINK_DENSITY_CHECKS);
+  fprintf(file_exact, "# periodic= %d\n", s->periodic);
+  fprintf(file_exact, "# N_ngb_target= %f +/- %f\n", N_ngb_target,
+          e->sink_properties->delta_neighbours);
+  fprintf(file_exact, "# Git Branch: %s\n", git_branch());
+  fprintf(file_exact, "# Git Revision: %s\n", git_revision());
+  fprintf(file_exact, "# %16s %16s %16s %16s %16s %7s %7s %16s %16s %16s\n",
+          "id", "pos[0]", "pos[1]", "pos[2]", "h", "Nd", "Nf", "rho_exact",
+          "n_rho_exact", "N_ngb");
+
+  int wrong_rho = 0;
+  int wrong_n_ngb = 0;
+  int counter = 0;
+
+  /* Output particle SWIFT densities */
+  for (size_t i = 0; i < nr_sinks; ++i) {
+
+    const struct sink *si = &sinks[i];
+    const long long id = si->id;
+    const int found_inhibited = si->inhibited_check_exact;
+
+    const double N_ngb = (4. / 3.) * M_PI * kernel_gamma * kernel_gamma *
+                         kernel_gamma * si->h * si->h * si->h *
+                         si->n_check_exact;
+
+    if (id % SWIFT_SINK_DENSITY_CHECKS == 0 && sink_is_starting(si, e)) {
+
+      counter++;
+
+      fprintf(
+          file_exact,
+          "%18lld %16.8e %16.8e %16.8e %16.8e %7d %7d %16.8e %16.8e %16.8e\n",
+          id, si->x[0], si->x[1], si->x[2], si->h, si->N_check_density_exact, 0,
+          si->rho_check_exact, si->n_check_exact, N_ngb);
+
+      /* Check that we did not go above the threshold.
+       * Note that we ignore particles that saw an inhibted particle as a
+       * neighbour as we don't know whether that neighbour became inhibited in
+       * that step or not. */
+      if (!found_inhibited &&
+          si->N_check_density_exact != si->N_check_density &&
+          (fabsf(si->rho_check / si->rho_check_exact - 1.f) > rel_tol ||
+           fabsf(si->rho_check_exact / si->rho_check - 1.f) > rel_tol)) {
+        message("RHO: id=%lld swift=%e exact=%e N_swift=%d N_true=%d", id,
+                si->rho_check, si->rho_check_exact, si->N_check_density,
+                si->N_check_density_exact);
+        wrong_rho++;
+      }
+
+      if (!found_inhibited && (N_ngb > N_ngb_max || N_ngb < N_ngb_min)) {
+
+        message("N_NGB: id=%lld exact=%f N_true=%d N_swift=%d", id, N_ngb,
+                si->N_check_density_exact, si->N_check_density);
+
+        wrong_n_ngb++;
+      }
+    }
+  }
+
+  if (e->verbose)
+    message("Written exact densities in file '%s'.", file_name_exact);
+
+  /* Be nice */
+  fclose(file_exact);
+
+  if (wrong_rho)
+    error(
+        "Density difference larger than the allowed tolerance for %d "
+        "sink particles! (out of %d particles)",
+        wrong_rho, counter);
+  else
+    message("Verified %d sink particles", counter);
+
+  /* if (wrong_n_ngb) */
+  /*   error( */
+  /*       "N_ngb difference larger than the allowed tolerance for %d " */
+  /*       "star particles! (out of %d particles)", */
+  /*       wrong_n_ngb, counter); */
+  /* else */
+  /*   message("Verified %d star particles", counter); */
+
+  if (e->verbose)
+    message("Writting brute-force density files took %.3f %s. ",
+            clocks_from_ticks(getticks() - tic), clocks_getunit());
+
+#else
+  error("Sink checking function called without the corresponding flag.");
+#endif
+}
diff --git a/src/sink.h b/src/sink.h
index e5d3a78aa2a35e6604136194f68bc496cd088f05..e891283826c4ab84117bf082048666ba0edc7b3f 100644
--- a/src/sink.h
+++ b/src/sink.h
@@ -25,12 +25,19 @@
 /* Select the correct sink model */
 #if defined(SINK_NONE)
 #include "./sink/Default/sink.h"
-#include "./sink/Default/sink_iact.h"
+#elif defined(SINK_BASIC)
+#include "./sink/Basic/sink.h"
 #elif defined(SINK_GEAR)
 #include "./sink/GEAR/sink.h"
-#include "./sink/GEAR/sink_iact.h"
 #else
 #error "Invalid choice of sink model"
 #endif
 
-#endif
+struct engine;
+struct space;
+
+void sink_exact_density_compute(struct space *s, const struct engine *e);
+void sink_exact_density_check(struct space *s, const struct engine *e,
+                              const double rel_tol);
+
+#endif /* SWIFT_SINK_H */
diff --git a/src/sink/Basic/sink.h b/src/sink/Basic/sink.h
new file mode 100644
index 0000000000000000000000000000000000000000..238a2bf477e7556d0ddf8bd8f895881e7838cb15
--- /dev/null
+++ b/src/sink/Basic/sink.h
@@ -0,0 +1,644 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_BASIC_SINK_H
+#define SWIFT_BASIC_SINK_H
+
+#include <float.h>
+
+/* Put pragma if gsl around here */
+#ifdef HAVE_LIBGSL
+#include <gsl/gsl_cdf.h>
+#endif
+
+/* Local includes */
+#include "active.h"
+#include "chemistry.h"
+#include "cooling.h"
+#include "feedback.h"
+#include "minmax.h"
+#include "random.h"
+#include "sink_part.h"
+#include "sink_properties.h"
+#include "star_formation.h"
+
+/**
+ * @brief Computes the time-step of a given sink particle.
+ *
+ * @param sp Pointer to the sink-particle data.
+ */
+__attribute__((always_inline)) INLINE static float sink_compute_timestep(
+    const struct sink* const sink, const struct sink_props* sink_properties,
+    const int with_cosmology, const struct cosmology* cosmo,
+    const struct gravity_props* grav_props, const double time,
+    const double time_base) {
+
+  return FLT_MAX;
+}
+
+/**
+ * @brief Initialises the sink-particles for the first time
+ *
+ * This function is called only once just after the ICs have been
+ * read in to do some conversions.
+ *
+ * @param sp The #sink particle to act upon.
+ * @param sink_props The properties of the sink particles scheme.
+ * @param e The #engine
+ */
+__attribute__((always_inline)) INLINE static void sink_first_init_sink(
+    struct sink* sp, const struct sink_props* sink_props,
+    const struct engine* e) {
+
+  sp->time_bin = 0;
+
+  sp->number_of_gas_swallows = 0;
+  sp->number_of_direct_gas_swallows = 0;
+  sp->number_of_sink_swallows = 0;
+  sp->number_of_direct_sink_swallows = 0;
+  sp->swallowed_angular_momentum[0] = 0.f;
+  sp->swallowed_angular_momentum[1] = 0.f;
+  sp->swallowed_angular_momentum[2] = 0.f;
+
+  /* Initially set the subgrid mass equal to the dynamical mass read from the
+   * ICs. */
+  sp->subgrid_mass = sp->mass;
+
+  sink_mark_sink_as_not_swallowed(&sp->merger_data);
+}
+
+/**
+ * @brief Initialisation of particle data before the hydro density loop.
+ * Note: during initalisation (space_init)
+ *
+ * @param p The #part to act upon.
+ * @param sink_props The properties of the sink particles scheme.
+ */
+__attribute__((always_inline)) INLINE static void sink_init_part(
+    struct part* restrict p, const struct sink_props* sink_props) {}
+
+/**
+ * @brief Initialisation of sink particle data before sink loops.
+ * Note: during initalisation (space_init_sinks)
+ *
+ * @param sp The #sink particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void sink_init_sink(
+    struct sink* sp) {
+
+  sp->density.wcount = 0.f;
+  sp->density.wcount_dh = 0.f;
+  sp->rho_gas = 0.f;
+  sp->sound_speed_gas = 0.f;
+  sp->velocity_gas[0] = 0.f;
+  sp->velocity_gas[1] = 0.f;
+  sp->velocity_gas[2] = 0.f;
+  sp->ngb_mass = 0.f;
+  sp->num_ngbs = 0;
+  sp->accretion_rate = 0.f;
+  sp->mass_at_start_of_step = sp->mass; /* sp->mass may grow in nibbling mode */
+
+#ifdef DEBUG_INTERACTIONS_SINKS
+  for (int i = 0; i < MAX_NUM_OF_NEIGHBOURS_SINKS; ++i)
+    sp->ids_ngbs_accretion[i] = -1;
+  sp->num_ngb_accretion = 0;
+
+  for (int i = 0; i < MAX_NUM_OF_NEIGHBOURS_SINKS; ++i)
+    sp->ids_ngbs_merger[i] = -1;
+  sp->num_ngb_merger = 0;
+
+  for (int i = 0; i < MAX_NUM_OF_NEIGHBOURS_SINKS; ++i)
+    sp->ids_ngbs_formation[i] = -1;
+  sp->num_ngb_formation = 0;
+#endif
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  sp->N_check_density = 0;
+  sp->N_check_density_exact = 0;
+  sp->rho_check = 0.f;
+  sp->rho_check_exact = 0.f;
+  sp->n_check = 0.f;
+  sp->n_check_exact = 0.f;
+  sp->inhibited_check_exact = 0;
+#endif
+}
+
+/**
+ * @brief Predict additional particle fields forward in time when drifting
+ *
+ * @param sp The #sink.
+ * @param dt_drift The drift time-step for positions.
+ */
+__attribute__((always_inline)) INLINE static void sink_predict_extra(
+    struct sink* restrict sp, float dt_drift) {}
+
+/**
+ * @brief Sets the values to be predicted in the drifts to their values at a
+ * kick time
+ *
+ * @param sp The #sink particle.
+ */
+__attribute__((always_inline)) INLINE static void sink_reset_predicted_values(
+    struct sink* restrict sp) {}
+
+/**
+ * @brief Kick the additional variables
+ *
+ * @param sp The #sink particle to act upon
+ * @param dt The time-step for this kick
+ */
+__attribute__((always_inline)) INLINE static void sink_kick_extra(
+    struct sink* sp, float dt) {}
+
+/**
+ * @brief Finishes the calculation of density on sinks
+ *
+ * @param si The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sink_end_density(
+    struct sink* si, const struct cosmology* cosmo) {
+
+  /* Some smoothing length multiples. */
+  const float h = si->h;
+  const float h_inv = 1.0f / h;                       /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv);       /* 1/h^d */
+  const float h_inv_dim_plus_one = h_inv_dim * h_inv; /* 1/h^(d+1) */
+
+  /* Finish the calculation by inserting the missing h-factors */
+  si->density.wcount *= h_inv_dim;
+  si->density.wcount_dh *= h_inv_dim_plus_one;
+
+  /* Finish the density calculation */
+  si->rho_gas *= h_inv_dim;
+
+  /* For the following, we also have to undo the mass smoothing
+   * (N.B.: bp->velocity_gas is in BH frame, in internal units). */
+  const float rho_inv = 1.f / si->rho_gas;
+  si->sound_speed_gas *= h_inv_dim * rho_inv;
+  si->velocity_gas[0] *= h_inv_dim * rho_inv;
+  si->velocity_gas[1] *= h_inv_dim * rho_inv;
+  si->velocity_gas[2] *= h_inv_dim * rho_inv;
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  si->rho_check *= h_inv_dim;
+  si->n_check *= h_inv_dim;
+#endif
+}
+
+/**
+ * @brief Sets all particle fields to sensible values when the #sink has 0
+ * ngbs.
+ *
+ * @param sp The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sinks_sink_has_no_neighbours(
+    struct sink* restrict sp, const struct cosmology* cosmo) {
+
+  warning(
+      "Sink particle with ID %lld treated as having no neighbours (h: %g, "
+      "wcount: %g).",
+      sp->id, sp->h, sp->density.wcount);
+
+  /* Some smoothing length multiples. */
+  const float h = sp->h;
+  const float h_inv = 1.0f / h;                 /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
+
+  /* Re-set problematic values */
+  sp->density.wcount = kernel_root * h_inv_dim;
+  sp->density.wcount_dh = 0.f;
+}
+
+/**
+ * @brief Compute the accretion rate of the sink and any quantities
+ * required swallowing based on an accretion rate
+ *
+ * Adapted from black_holes_prepare_feedback
+ *
+ * @param si The sink particle.
+ * @param props The properties of the sink scheme.
+ * @param constants The physical constants (in internal units).
+ * @param cosmo The cosmological model.
+ * @param cooling Properties of the cooling model.
+ * @param floor_props Properties of the entropy floor.
+ * @param time Time since the start of the simulation (non-cosmo mode).
+ * @param with_cosmology Are we running with cosmology?
+ * @param dt The time-step size (in physical internal units).
+ * @param ti_begin Integer time value at the beginning of timestep
+ */
+__attribute__((always_inline)) INLINE static void sink_prepare_swallow(
+    struct sink* restrict si, const struct sink_props* props,
+    const struct phys_const* constants, const struct cosmology* cosmo,
+    const struct cooling_function_data* cooling,
+    const struct entropy_floor_properties* floor_props, const double time,
+    const int with_cosmology, const double dt, const integertime_t ti_begin) {
+
+  if (dt == 0. || si->rho_gas == 0.) return;
+
+  /* Gather some physical constants (all in internal units) */
+  const double G = constants->const_newton_G;
+
+  /* (Subgrid) mass of the sink (internal units) */
+  const double sink_mass = si->subgrid_mass;
+
+  /* We can now compute the accretion rate (internal units) */
+  /* Standard approach: compute accretion rate for all gas simultaneously.
+   *
+   * Convert the quantities we gathered to physical frame (all internal
+   * units). Note: velocities are already in black hole frame. */
+  const double gas_rho_phys = si->rho_gas * cosmo->a3_inv;
+  const double gas_c_phys = si->sound_speed_gas * cosmo->a_factor_sound_speed;
+  const double gas_c_phys2 = gas_c_phys * gas_c_phys;
+  const double gas_v_phys[3] = {si->velocity_gas[0] * cosmo->a_inv,
+                                si->velocity_gas[1] * cosmo->a_inv,
+                                si->velocity_gas[2] * cosmo->a_inv};
+  const double gas_v_norm2 = gas_v_phys[0] * gas_v_phys[0] +
+                             gas_v_phys[1] * gas_v_phys[1] +
+                             gas_v_phys[2] * gas_v_phys[2];
+
+  const double denominator2 = gas_v_norm2 + gas_c_phys2;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Make sure that the denominator is strictly positive */
+  if (denominator2 <= 0)
+    error(
+        "Invalid denominator for sink particle %lld in Bondi rate "
+        "calculation.",
+        si->id);
+#endif
+
+  const double denominator_inv = 1. / sqrt(denominator2);
+
+  double accr_rate = 4. * M_PI * G * G * sink_mass * sink_mass * gas_rho_phys *
+                     denominator_inv * denominator_inv * denominator_inv;
+
+  /* Integrate forward in time */
+  si->subgrid_mass += accr_rate * dt;
+}
+
+/**
+ * @brief Calculate if the gas has the potential of becoming
+ * a sink.
+ *
+ * Return 0 if no sink formation should occur.
+ * Note: called in runner_do_sink_formation
+ *
+ * @param p the gas particles.
+ * @param xp the additional properties of the gas particles.
+ * @param sink_props the sink properties to use.
+ * @param phys_const the physical constants in internal units.
+ * @param cosmo the cosmological parameters and properties.
+ * @param hydro_props The properties of the hydro scheme.
+ * @param us The internal system of units.
+ * @param cooling The cooling data struct.
+ * @param entropy_floor The entropy_floor properties.
+ *
+ */
+INLINE static int sink_is_forming(
+    const struct part* restrict p, const struct xpart* restrict xp,
+    const struct sink_props* sink_props, const struct phys_const* phys_const,
+    const struct cosmology* cosmo,
+    const struct hydro_props* restrict hydro_props,
+    const struct unit_system* restrict us,
+    const struct cooling_function_data* restrict cooling,
+    const struct entropy_floor_properties* restrict entropy_floor) {
+
+  /* Sink formation is not implemented in this model. */
+  return 0;
+}
+
+/**
+ * @brief Decides whether a particle should be converted into a
+ * sink or not.
+ *
+ * No SF should occur, so return 0.
+ * Note: called in runner_do_sink_formation
+ *
+ * @param p The #part.
+ * @param xp The #xpart.
+ * @param sink_props The properties of the sink model.
+ * @param e The #engine (for random numbers).
+ * @param dt_sink The time-step of this particle
+ * @return 1 if a conversion should be done, 0 otherwise.
+ */
+INLINE static int sink_should_convert_to_sink(
+    const struct part* p, const struct xpart* xp,
+    const struct sink_props* sink_props, const struct engine* e,
+    const double dt_sink) {
+
+  return 0;
+}
+
+/**
+ * @brief Copies the properties of the gas particle over to the
+ * sink particle.
+ *
+ * @param p The gas particles.
+ * @param xp The additional properties of the gas particles.
+ * @param sink the new created #sink particle.
+ * @param e The #engine.
+ * @param sink_props The sink properties to use.
+ * @param cosmo the cosmological parameters and properties.
+ * @param with_cosmology if we run with cosmology.
+ * @param phys_const The physical constants in internal units.
+ * @param hydro_props The hydro properties to use.
+ * @param us The internal unit system.
+ * @param cooling The cooling function to use.
+ */
+INLINE static void sink_copy_properties(
+    const struct part* p, const struct xpart* xp, struct sink* sink,
+    const struct engine* e, const struct sink_props* sink_props,
+    const struct cosmology* cosmo, const int with_cosmology,
+    const struct phys_const* phys_const,
+    const struct hydro_props* restrict hydro_props,
+    const struct unit_system* restrict us,
+    const struct cooling_function_data* restrict cooling) {}
+
+/**
+ * @brief Update the properties of a sink particles by swallowing
+ * a gas particle.
+ *
+ * @param sp The #sink to update.
+ * @param p The #part that is swallowed.
+ * @param xp The #xpart that is swallowed.
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sink_swallow_part(
+    struct sink* sp, const struct part* p, const struct xpart* xp,
+    const struct cosmology* cosmo) {
+
+  /* Get the current dynamical masses */
+  const float gas_mass = hydro_get_mass(p);
+  const float sink_mass = sp->mass;
+
+  /* Increase the dynamical mass of the sink. */
+  sp->mass += gas_mass;
+  sp->gpart->mass += gas_mass;
+
+  /* Comoving and physical distance between the particles */
+  const float dx[3] = {sp->x[0] - p->x[0], sp->x[1] - p->x[1],
+                       sp->x[2] - p->x[2]};
+  const float dx_physical[3] = {dx[0] * cosmo->a, dx[1] * cosmo->a,
+                                dx[2] * cosmo->a};
+
+  /* Relative velocity between the sink and the part */
+  const float dv[3] = {sp->v[0] - p->v[0], sp->v[1] - p->v[1],
+                       sp->v[2] - p->v[2]};
+
+  const float a = cosmo->a;
+  const float H = cosmo->H;
+  const float a2H = a * a * H;
+
+  /* Calculate the velocity with the Hubble flow */
+  const float v_plus_H_flow[3] = {a2H * dx[0] + dv[0], a2H * dx[1] + dv[1],
+                                  a2H * dx[2] + dv[2]};
+
+  /* Compute the physical relative velocity between the particles */
+  const float dv_physical[3] = {v_plus_H_flow[0] * cosmo->a_inv,
+                                v_plus_H_flow[1] * cosmo->a_inv,
+                                v_plus_H_flow[2] * cosmo->a_inv};
+
+  /* Collect the swallowed angular momentum */
+  sp->swallowed_angular_momentum[0] +=
+      gas_mass *
+      (dx_physical[1] * dv_physical[2] - dx_physical[2] * dv_physical[1]);
+  sp->swallowed_angular_momentum[1] +=
+      gas_mass *
+      (dx_physical[2] * dv_physical[0] - dx_physical[0] * dv_physical[2]);
+  sp->swallowed_angular_momentum[2] +=
+      gas_mass *
+      (dx_physical[0] * dv_physical[1] - dx_physical[1] * dv_physical[0]);
+
+  /* Update the sink momentum */
+  const float sink_mom[3] = {sink_mass * sp->v[0] + gas_mass * p->v[0],
+                             sink_mass * sp->v[1] + gas_mass * p->v[1],
+                             sink_mass * sp->v[2] + gas_mass * p->v[2]};
+
+  sp->v[0] = sink_mom[0] / sp->mass;
+  sp->v[1] = sink_mom[1] / sp->mass;
+  sp->v[2] = sink_mom[2] / sp->mass;
+  sp->gpart->v_full[0] = sp->v[0];
+  sp->gpart->v_full[1] = sp->v[1];
+  sp->gpart->v_full[2] = sp->v[2];
+
+  /* This sink swallowed a gas particle */
+  sp->number_of_gas_swallows++;
+  sp->number_of_direct_gas_swallows++;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  const float dr = sqrt(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
+  message(
+      "sink %lld swallow gas particle %lld. "
+      "(Mass = %e, "
+      "Delta_v = [%f, %f, %f] U_V, "
+      "Delta_x = [%f, %f, %f] U_L, "
+      "Delta_v_rad = %f)",
+      sp->id, p->id, sp->mass, -dv[0], -dv[1], -dv[2], -dx[0], -dx[1], -dx[2],
+      (dv[0] * dx[0] + dv[1] * dx[1] + dv[2] * dx[2]) / dr);
+#endif
+}
+
+/**
+ * @brief Update the properties of a sink particles by swallowing
+ * a sink particle.
+ *
+ * @param spi The #sink to update.
+ * @param spj The #sink that is swallowed.
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sink_swallow_sink(
+    struct sink* spi, const struct sink* spj, const struct cosmology* cosmo) {
+
+  /* Get the current dynamical masses */
+  const float spi_dyn_mass = spi->mass;
+  const float spj_dyn_mass = spj->mass;
+
+  /* Increase the masses of the sink. */
+  spi->mass += spj->mass;
+  spi->gpart->mass += spj->mass;
+  spi->subgrid_mass += spj->subgrid_mass;
+
+  /* Collect the swallowed angular momentum */
+  spi->swallowed_angular_momentum[0] += spj->swallowed_angular_momentum[0];
+  spi->swallowed_angular_momentum[1] += spj->swallowed_angular_momentum[1];
+  spi->swallowed_angular_momentum[2] += spj->swallowed_angular_momentum[2];
+
+  /* Update the sink momentum */
+  const float sink_mom[3] = {
+      spi_dyn_mass * spi->v[0] + spj_dyn_mass * spj->v[0],
+      spi_dyn_mass * spi->v[1] + spj_dyn_mass * spj->v[1],
+      spi_dyn_mass * spi->v[2] + spj_dyn_mass * spj->v[2]};
+
+  spi->v[0] = sink_mom[0] / spi->mass;
+  spi->v[1] = sink_mom[1] / spi->mass;
+  spi->v[2] = sink_mom[2] / spi->mass;
+  spi->gpart->v_full[0] = spi->v[0];
+  spi->gpart->v_full[1] = spi->v[1];
+  spi->gpart->v_full[2] = spi->v[2];
+
+  /* This sink swallowed a sink particle */
+  spi->number_of_sink_swallows++;
+  spi->number_of_direct_sink_swallows++;
+
+  /* Add all other swallowed particles swallowed by the swallowed sink */
+  spi->number_of_sink_swallows += spj->number_of_sink_swallows;
+  spi->number_of_gas_swallows += spj->number_of_gas_swallows;
+
+  message("sink %lld swallow sink particle %lld. New mass: %e.", spi->id,
+          spj->id, spi->mass);
+}
+
+/**
+ * @brief Should the sink spawn a star particle?
+ *
+ * @param sink the sink particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param cosmo The cosmological parameters and properties.
+ * @param with_cosmology If we run with cosmology.
+ * @param phys_const The physical constants in internal units.
+ * @param us The internal unit system.
+ */
+INLINE static int sink_spawn_star(struct sink* sink, const struct engine* e,
+                                  const struct sink_props* sink_props,
+                                  const struct cosmology* cosmo,
+                                  const int with_cosmology,
+                                  const struct phys_const* phys_const,
+                                  const struct unit_system* restrict us) {
+
+  /* Star formation from sinks is disabled in this model. */
+  return 0;
+}
+
+/**
+ * @brief Copy the properties of the sink particle towards the new star. Also,
+ * give the stars some properties such as position and velocity.
+ *
+ * This function also needs to update the sink particle.
+ *
+ * @param sink The #sink particle.
+ * @param sp The star particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param cosmo The cosmological parameters and properties.
+ * @param with_cosmology If we run with cosmology.
+ * @param phys_const The physical constants in internal units.
+ * @param us The internal unit system.
+ */
+INLINE static void sink_copy_properties_to_star(
+    struct sink* sink, struct spart* sp, const struct engine* e,
+    const struct sink_props* sink_props, const struct cosmology* cosmo,
+    const int with_cosmology, const struct phys_const* phys_const,
+    const struct unit_system* restrict us) {}
+
+/**
+ * @brief Update the #sink particle properties before spawning a star.
+ *
+ * In GEAR, we check if the sink had an IMF change from pop III to pop II
+ * during the last gas/sink accretion loops. If so, we draw a new target mass
+ * with the correct IMF so that stars have the right metallicities.
+ *
+ * @param sink The #sink particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param phys_const The physical constants in internal units.
+ */
+INLINE static void sink_update_sink_properties_before_star_formation(
+    struct sink* sink, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const) {}
+
+/**
+ * @brief Update the #sink particle properties right after spawning a star.
+ *
+ * In GEAR: Important properties that are updated are the sink mass and the
+ * sink->target_mass_Msun to draw the next star mass.
+ *
+ * @param sink The #sink particle that spawed stars.
+ * @param sp The #spart particle spawned.
+ * @param e The #engine
+ * @param sink_props the sink properties to use.
+ * @param phys_const the physical constants in internal units.
+ * @param star_counter The star loop counter.
+ */
+INLINE static void sink_update_sink_properties_during_star_formation(
+    struct sink* sink, const struct spart* sp, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const,
+    int star_counter) {}
+
+/**
+ * @brief Update the #sink particle properties after star formation.
+ *
+ * In GEAR, this is unused.
+ *
+ * @param sink The #sink particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param phys_const The physical constants in internal units.
+ */
+INLINE static void sink_update_sink_properties_after_star_formation(
+    struct sink* sink, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const) {}
+
+/**
+ * @brief Store the gravitational potential of a particle by copying it from
+ * its #gpart friend.
+ *
+ * @param p_data The sink data of a gas particle.
+ * @param gp The part's #gpart.
+ */
+__attribute__((always_inline)) INLINE static void sink_store_potential_in_part(
+    struct sink_part_data* p_data, const struct gpart* gp) {}
+
+/**
+ * @brief Compute all quantities required for the formation of a sink such as
+ * kinetic energy, potential energy, etc. This function works on the
+ * neighbouring gas particles.
+ *
+ * @param e The #engine.
+ * @param p The #part for which we compute the quantities.
+ * @param xp The #xpart data of the particle #p.
+ * @param pi A neighbouring #part of #p.
+ * @param xpi The #xpart data of the particle #pi.
+ * @param cosmo The cosmological parameters and properties.
+ * @param sink_props The sink properties to use.
+ */
+INLINE static void sink_prepare_part_sink_formation_gas_criteria(
+    struct engine* e, struct part* restrict p, struct xpart* restrict xp,
+    struct part* restrict pi, struct xpart* restrict xpi,
+    const struct cosmology* cosmo, const struct sink_props* sink_props) {}
+
+/**
+ * @brief Compute all quantities required for the formation of a sink. This
+ * function works on the neighbouring sink particles.
+ *
+ * @param e The #engine.
+ * @param p The #part for which we compute the quantities.
+ * @param xp The #xpart data of the particle #p.
+ * @param si A neighbouring #sink of #p.
+ * @param cosmo The cosmological parameters and properties.
+ * @param sink_props The sink properties to use.
+ */
+INLINE static void sink_prepare_part_sink_formation_sink_criteria(
+    struct engine* e, struct part* restrict p, struct xpart* restrict xp,
+    struct sink* restrict si, const int with_cosmology,
+    const struct cosmology* cosmo, const struct sink_props* sink_props,
+    const double time) {}
+
+#endif /* SWIFT_BASIC_SINK_H */
diff --git a/src/hydro/Shadowswift/voronoi_cell.h b/src/sink/Basic/sink_debug.h
similarity index 67%
rename from src/hydro/Shadowswift/voronoi_cell.h
rename to src/sink/Basic/sink_debug.h
index 30d3e17fdfa76448773c0e45834ddf732989a3a4..441af55c3ecf4083b5c5cb3b563b706e13ea59ac 100644
--- a/src/hydro/Shadowswift/voronoi_cell.h
+++ b/src/sink/Basic/sink_debug.h
@@ -1,6 +1,6 @@
 /*******************************************************************************
  * This file is part of SWIFT.
- * Copyright (c) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,18 +16,14 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
+#ifndef SWIFT_SINK_BASIC_DEBUG_H
+#define SWIFT_SINK_BASIC_DEBUG_H
 
-#ifndef SWIFT_VORONOI_CELL_H
-#define SWIFT_VORONOI_CELL_H
+__attribute__((always_inline)) INLINE static void sink_debug_particle(
+    const struct part* p, const struct xpart* xp) {
 
-#if defined(HYDRO_DIMENSION_1D)
-#include "voronoi1d_cell.h"
-#elif defined(HYDRO_DIMENSION_2D)
-#include "voronoi2d_cell.h"
-#elif defined(HYDRO_DIMENSION_3D)
-#include "voronoi3d_cell.h"
-#else
-#error "You have to select a dimension for the hydro!"
-#endif
+  warning("[PID%lld] sink_part_data:", p->id);
+  warning("[PID%lld] swallow_id = %lld", p->id, p->sink_data.swallow_id);
+}
 
-#endif  // SWIFT_VORONOI_CELL_H
+#endif /* SWIFT_SINK_BASIC_DEBUG_H */
diff --git a/src/sink/Basic/sink_iact.h b/src/sink/Basic/sink_iact.h
new file mode 100644
index 0000000000000000000000000000000000000000..f6cdf999dd31efc31bce9e954503a854bb1771fc
--- /dev/null
+++ b/src/sink/Basic/sink_iact.h
@@ -0,0 +1,373 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_BASIC_SINKS_IACT_H
+#define SWIFT_BASIC_SINKS_IACT_H
+
+/* Local includes */
+#include "gravity.h"
+#include "gravity_iact.h"
+#include "random.h"
+#include "sink_properties.h"
+
+/**
+ * @brief Gas particle interactions relevant for sinks, to run in hydro density
+ * interaction
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle.
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_sink(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {}
+
+/**
+ * @brief Gas particle interactions relevant for sinks, to run in hydro density
+ * interaction (non symmetric version)
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing-length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param pi First particle.
+ * @param pj Second particle (not updated).
+ * @param a Current scale factor.
+ * @param H Current Hubble parameter.
+ */
+__attribute__((always_inline)) INLINE static void runner_iact_nonsym_sink(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, const struct part *restrict pj, const float a,
+    const float H) {}
+
+/**
+ * @brief Density interaction between sinks and gas (non-symmetric).
+ *
+ * The gas particle cannot be touched.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing length or cut off radius of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param si First particle (sink).
+ * @param pj Second particle (gas, not updated).
+ * @param with_cosmology Are we doing a cosmological run?
+ * @param cosmo The cosmological model.
+ * @param grav_props The properties of the gravity scheme (softening, G, ...).
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
+ */
+__attribute__((always_inline)) INLINE static void
+runner_iact_nonsym_sinks_gas_density(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *si, const struct part *pj, const int with_cosmology,
+    const struct cosmology *cosmo, const struct gravity_props *grav_props,
+    const struct sink_props *sink_props, const integertime_t ti_current,
+    const double time) {
+
+  float wi, wi_dx;
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  /* Compute the kernel function */
+  const float hi_inv = 1.0f / hi;
+  const float ui = r * hi_inv;
+  kernel_deval(ui, &wi, &wi_dx);
+
+  /* Compute contribution to the number of neighbours */
+  si->density.wcount += wi;
+  si->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+
+  /* Contribution to the number of neighbours */
+  si->num_ngbs++;
+
+  /* Neighbour gas mass */
+  const float mj = hydro_get_mass(pj);
+
+  /* Contribution to the BH gas density */
+  si->rho_gas += mj * wi;
+
+  /* Contribution to the total neighbour mass */
+  si->ngb_mass += mj;
+
+  /* Neighbour's sound speed */
+  const float cj = hydro_get_comoving_soundspeed(pj);
+
+  /* Contribution to the smoothed sound speed */
+  si->sound_speed_gas += mj * cj * wi;
+
+  /* Neighbour's (drifted) velocity in the frame of the black hole
+   * (we do include a Hubble term) */
+  const float dv[3] = {pj->v[0] - si->v[0], pj->v[1] - si->v[1],
+                       pj->v[2] - si->v[2]};
+
+  /* Contribution to the smoothed velocity (gas w.r.t. black hole) */
+  si->velocity_gas[0] += mj * dv[0] * wi;
+  si->velocity_gas[1] += mj * dv[1] * wi;
+  si->velocity_gas[2] += mj * dv[2] * wi;
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  si->rho_check += mj * wi;
+  si->n_check += wi;
+  si->N_check_density++;
+#endif
+}
+
+/**
+ * @brief Compute sink-sink swallow interaction (non-symmetric).
+ *
+ * Note: Energies are computed with physical quantities, not the comoving ones.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing length or cut off radius of particle i.
+ * @param hj Comoving smoothing length or cut off radius of particle j.
+ * @param si First sink particle.
+ * @param sj Second sink particle.
+ * @param with_cosmology if we run with cosmology.
+ * @param cosmo The cosmological parameters and properties.
+ * @param grav_props The gravity scheme parameters and properties.
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
+ */
+__attribute__((always_inline)) INLINE static void
+runner_iact_nonsym_sinks_sink_swallow(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *restrict si, struct sink *restrict sj,
+    const int with_cosmology, const struct cosmology *cosmo,
+    const struct gravity_props *grav_props,
+    const struct sink_props *sink_properties, const integertime_t ti_current,
+    const double time) {
+
+  /* Simpler version of GEAR as a placeholder. Sinks bound to each other are
+   * merged. */
+
+  /* Relative velocity between the sinks */
+  const float dv[3] = {sj->v[0] - si->v[0], sj->v[1] - si->v[1],
+                       sj->v[2] - si->v[2]};
+
+  const float a = cosmo->a;
+  const float H = cosmo->H;
+  const float a2H = a * a * H;
+
+  /* Calculate the velocity with the Hubble flow */
+  const float v_plus_H_flow[3] = {a2H * dx[0] + dv[0], a2H * dx[1] + dv[1],
+                                  a2H * dx[2] + dv[2]};
+
+  /* Binding energy check */
+  /* Compute the physical relative velocity between the particles */
+  const float dv_physical[3] = {v_plus_H_flow[0] * cosmo->a_inv,
+                                v_plus_H_flow[1] * cosmo->a_inv,
+                                v_plus_H_flow[2] * cosmo->a_inv};
+
+  const float dv_physical_squared = dv_physical[0] * dv_physical[0] +
+                                    dv_physical[1] * dv_physical[1] +
+                                    dv_physical[2] * dv_physical[2];
+
+  /* Kinetic energy per unit mass of the gas */
+  const float E_kin_rel = 0.5f * dv_physical_squared;
+
+  /* Compute the Newtonian or softened potential the sink exherts onto the
+      gas particle */
+  /* TODO: needs updating for MPI safety. We don't have access to foreign gparts
+   * here. */
+  const float eps = gravity_get_softening(si->gpart, grav_props);
+  const float eps2 = eps * eps;
+  const float eps_inv = 1.f / eps;
+  const float eps_inv3 = eps_inv * eps_inv * eps_inv;
+  const float si_mass = si->mass;
+  const float sj_mass = sj->mass;
+
+  float dummy, pot_ij, pot_ji;
+  runner_iact_grav_pp_full(r2, eps2, eps_inv, eps_inv3, si_mass, &dummy,
+                           &pot_ij);
+  runner_iact_grav_pp_full(r2, eps2, eps_inv, eps_inv3, sj_mass, &dummy,
+                           &pot_ji);
+
+  /* Compute the physical potential energies per unit mass :
+                          E_pot_phys = G*pot_grav*a^(-1) + c(a).
+      The normalization is c(a) = 0. */
+  const float E_pot_ij = grav_props->G_Newton * pot_ij * cosmo->a_inv;
+  const float E_pot_ji = grav_props->G_Newton * pot_ji * cosmo->a_inv;
+
+  /* Mechanical energy per unit mass of the pair i-j and j-i */
+  const float E_mec_si = E_kin_rel + E_pot_ij;
+  const float E_mec_sj = E_kin_rel + E_pot_ji;
+
+  /* Now, check if one is bound to the other */
+  if ((E_mec_si > 0) || (E_mec_sj > 0)) {
+    return;
+  }
+
+  /* The sink with the smaller mass will be merged onto the one with the
+   * larger mass.
+   * To avoid rounding issues, we additionally check for IDs if the sink
+   * have the exact same mass. */
+  if ((sj->mass < si->mass) || (sj->mass == si->mass && sj->id < si->id)) {
+    /* This particle is swallowed by the sink with the largest mass of all the
+     * candidates wanting to swallow it (we use IDs to break ties)*/
+    if ((sj->merger_data.swallow_mass < si->mass) ||
+        (sj->merger_data.swallow_mass == si->mass &&
+         sj->merger_data.swallow_id < si->id)) {
+      sj->merger_data.swallow_id = si->id;
+      sj->merger_data.swallow_mass = si->mass;
+    }
+  }
+
+#ifdef DEBUG_INTERACTIONS_SINKS
+  /* Update ngb counters */
+  if (si->num_ngb_formation < MAX_NUM_OF_NEIGHBOURS_SINKS)
+    si->ids_ngbs_formation[si->num_ngb_formation] = pj->id;
+
+  /* Update ngb counters */
+  ++si->num_ngb_formation;
+#endif
+}
+
+/**
+ * @brief Compute sink-gas swallow interaction (non-symmetric).
+ *
+ * Note: Energies are computed with physical quantities, not the comoving ones.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing length or cut off radius of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param si First sink particle.
+ * @param pj Second particle.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
+ */
+__attribute__((always_inline)) INLINE static void
+runner_iact_nonsym_sinks_gas_swallow(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *restrict si, struct part *restrict pj,
+    const int with_cosmology, const struct cosmology *cosmo,
+    const struct gravity_props *grav_props,
+    const struct sink_props *sink_properties, const integertime_t ti_current,
+    const double time) {
+
+  float wi;
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  /* Compute the kernel function */
+  const float hi_inv = 1.0f / hi;
+  const float hi_inv_dim = pow_dimension(hi_inv);
+  const float ui = r * hi_inv;
+  kernel_eval(ui, &wi);
+
+  /* Check if the sink needs to be fed. If not, we're done here */
+  const float sink_mass_deficit = si->subgrid_mass - si->mass_at_start_of_step;
+  if (sink_mass_deficit <= 0) return;
+
+  if (sink_properties->use_nibbling) {
+
+    /* If we do nibbling, things are quite straightforward. We transfer
+     * the mass and all associated quantities right here. */
+
+    const float si_mass_orig = si->mass;
+    const float pj_mass_orig = hydro_get_mass(pj);
+
+    /* Don't nibble from particles that are too small already */
+    if (pj_mass_orig < sink_properties->min_gas_mass_for_nibbling) return;
+
+    /* Next line is equivalent to w_ij * m_j / Sum_j (w_ij * m_j) */
+    const float particle_weight = hi_inv_dim * wi * pj_mass_orig / si->rho_gas;
+    float nibble_mass = sink_mass_deficit * particle_weight;
+
+    /* Need to check whether nibbling would push gas mass below minimum
+     * allowed mass */
+    float new_gas_mass = pj_mass_orig - nibble_mass;
+    if (new_gas_mass < sink_properties->min_gas_mass_for_nibbling) {
+      new_gas_mass = sink_properties->min_gas_mass_for_nibbling;
+      nibble_mass = pj_mass_orig - sink_properties->min_gas_mass_for_nibbling;
+    }
+
+    /* Transfer (dynamical) mass from the gas particle to the sink */
+    si->mass += nibble_mass;
+    hydro_set_mass(pj, new_gas_mass);
+
+    /* Add the angular momentum of the accreted gas to the sink total.
+     * Note no change to gas here. The cosmological conversion factors for
+     * velocity (a^-1) and distance (a) cancel out, so the angular momentum
+     * is already in physical units. */
+    const float dv[3] = {si->v[0] - pj->v[0], si->v[1] - pj->v[1],
+                         si->v[2] - pj->v[2]};
+    si->swallowed_angular_momentum[0] +=
+        nibble_mass * (dx[1] * dv[2] - dx[2] * dv[1]);
+    si->swallowed_angular_momentum[1] +=
+        nibble_mass * (dx[2] * dv[0] - dx[0] * dv[2]);
+    si->swallowed_angular_momentum[2] +=
+        nibble_mass * (dx[0] * dv[1] - dx[1] * dv[0]);
+
+    /* Update the BH momentum and velocity. Again, no change to gas here. */
+    const float si_mom[3] = {si_mass_orig * si->v[0] + nibble_mass * pj->v[0],
+                             si_mass_orig * si->v[1] + nibble_mass * pj->v[1],
+                             si_mass_orig * si->v[2] + nibble_mass * pj->v[2]};
+
+    si->v[0] = si_mom[0] / si->mass;
+    si->v[1] = si_mom[1] / si->mass;
+    si->v[2] = si_mom[2] / si->mass;
+
+  } else { /* ends nibbling section, below comes swallowing */
+
+    /* Probability to swallow this particle
+     * Recall that in SWIFT the SPH kernel is recovered by computing
+     * kernel_eval() and muliplying by (1/h^d) */
+
+    const float prob =
+        (si->subgrid_mass - si->mass) * hi_inv_dim * wi / si->rho_gas;
+
+    /* Draw a random number (Note mixing both IDs) */
+    const float rand = random_unit_interval_two_IDs(si->id, pj->id, ti_current,
+                                                    random_number_sink_swallow);
+
+    /* Are we lucky? */
+    if (rand < prob) {
+
+      /* This particle is swallowed by the BH with the largest ID of all the
+       * candidates wanting to swallow it */
+      if (pj->sink_data.swallow_id < si->id) {
+
+        message("Sink %lld wants to swallow gas particle %lld", si->id, pj->id);
+
+        pj->sink_data.swallow_id = si->id;
+
+      } else {
+
+        message(
+            "Sink %lld wants to swallow gas particle %lld BUT CANNOT (old "
+            "swallow id=%lld)",
+            si->id, pj->id, pj->sink_data.swallow_id);
+      }
+    }
+  } /* ends section for swallowing */
+}
+
+#endif
diff --git a/src/sink/Basic/sink_io.h b/src/sink/Basic/sink_io.h
new file mode 100644
index 0000000000000000000000000000000000000000..0e0cabc6958a47adb1d83bb79f612302b3d3eea3
--- /dev/null
+++ b/src/sink/Basic/sink_io.h
@@ -0,0 +1,219 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_BASIC_SINK_IO_H
+#define SWIFT_BASIC_SINK_IO_H
+
+#include "io_properties.h"
+#include "sink_part.h"
+
+/**
+ * @brief Specifies which sink-particle fields to read from a dataset
+ *
+ * @param sinks The sink-particle array.
+ * @param list The list of i/o properties to read.
+ * @param num_fields The number of i/o fields to read.
+ */
+INLINE static void sink_read_particles(struct sink* sinks,
+                                       struct io_props* list, int* num_fields) {
+
+  /* Say how much we want to read */
+  *num_fields = 5;
+
+  /* List what we want to read */
+  list[0] = io_make_input_field("Coordinates", DOUBLE, 3, COMPULSORY,
+                                UNIT_CONV_LENGTH, sinks, x);
+  list[1] = io_make_input_field("Velocities", FLOAT, 3, COMPULSORY,
+                                UNIT_CONV_SPEED, sinks, v);
+  list[2] = io_make_input_field("Masses", FLOAT, 1, COMPULSORY, UNIT_CONV_MASS,
+                                sinks, mass);
+  list[3] = io_make_input_field("ParticleIDs", LONGLONG, 1, COMPULSORY,
+                                UNIT_CONV_NO_UNITS, sinks, id);
+  list[4] = io_make_input_field("SmoothingLength", FLOAT, 1, OPTIONAL,
+                                UNIT_CONV_LENGTH, sinks, h);
+}
+
+INLINE static void convert_sink_pos(const struct engine* e,
+                                    const struct sink* sp, double* ret) {
+
+  const struct space* s = e->s;
+  if (s->periodic) {
+    ret[0] = box_wrap(sp->x[0], 0.0, s->dim[0]);
+    ret[1] = box_wrap(sp->x[1], 0.0, s->dim[1]);
+    ret[2] = box_wrap(sp->x[2], 0.0, s->dim[2]);
+  } else {
+    ret[0] = sp->x[0];
+    ret[1] = sp->x[1];
+    ret[2] = sp->x[2];
+  }
+}
+
+INLINE static void convert_sink_vel(const struct engine* e,
+                                    const struct sink* sp, float* ret) {
+
+  const int with_cosmology = (e->policy & engine_policy_cosmology);
+  const struct cosmology* cosmo = e->cosmology;
+  const integertime_t ti_current = e->ti_current;
+  const double time_base = e->time_base;
+
+  const integertime_t ti_beg = get_integer_time_begin(ti_current, sp->time_bin);
+  const integertime_t ti_end = get_integer_time_end(ti_current, sp->time_bin);
+
+  /* Get time-step since the last kick */
+  float dt_kick_grav;
+  if (with_cosmology) {
+    dt_kick_grav = cosmology_get_grav_kick_factor(cosmo, ti_beg, ti_current);
+    dt_kick_grav -=
+        cosmology_get_grav_kick_factor(cosmo, ti_beg, (ti_beg + ti_end) / 2);
+  } else {
+    dt_kick_grav = (ti_current - ((ti_beg + ti_end) / 2)) * time_base;
+  }
+
+  /* Extrapolate the velocites to the current time */
+  const struct gpart* gp = sp->gpart;
+  ret[0] = gp->v_full[0] + gp->a_grav[0] * dt_kick_grav;
+  ret[1] = gp->v_full[1] + gp->a_grav[1] * dt_kick_grav;
+  ret[2] = gp->v_full[2] + gp->a_grav[2] * dt_kick_grav;
+
+  /* Conversion from internal units to peculiar velocities */
+  ret[0] *= cosmo->a_inv;
+  ret[1] *= cosmo->a_inv;
+  ret[2] *= cosmo->a_inv;
+}
+
+INLINE static void convert_sink_gas_vel(const struct engine* e,
+                                        const struct sink* sink, float* ret) {
+  const struct cosmology* cosmo = e->cosmology;
+  ret[0] = sink->velocity_gas[0] * cosmo->a_inv;
+  ret[1] = sink->velocity_gas[1] * cosmo->a_inv;
+  ret[2] = sink->velocity_gas[2] * cosmo->a_inv;
+}
+
+INLINE static void convert_sink_gas_sound_speed(const struct engine* e,
+                                                const struct sink* sink,
+                                                double* ret) {
+  const struct cosmology* cosmo = e->cosmology;
+  ret[0] = sink->sound_speed_gas * cosmo->a_factor_sound_speed;
+}
+
+INLINE static void convert_sink_swallowed_angular_momentum(
+    const struct engine* e, const struct sink* sink, float* ret) {
+  ret[0] = sink->swallowed_angular_momentum[0];
+  ret[1] = sink->swallowed_angular_momentum[1];
+  ret[2] = sink->swallowed_angular_momentum[2];
+}
+
+/**
+ * @brief Specifies which sink-particle fields to write to a dataset
+ *
+ * @param sinks The sink-particle array.
+ * @param list The list of i/o properties to write.
+ * @param num_fields The number of i/o fields to write.
+ * @param with_cosmology Are we running a cosmological simulation?
+ */
+INLINE static void sink_write_particles(const struct sink* sinks,
+                                        struct io_props* list, int* num_fields,
+                                        int with_cosmology) {
+
+  /* Say how much we want to write */
+  *num_fields = 12;
+
+  /* List what we want to write */
+  list[0] = io_make_output_field_convert_sink(
+      "Coordinates", DOUBLE, 3, UNIT_CONV_LENGTH, 1.f, sinks, convert_sink_pos,
+      "Co-moving position of the particles");
+
+  list[1] = io_make_output_field_convert_sink(
+      "Velocities", FLOAT, 3, UNIT_CONV_SPEED, 0.f, sinks, convert_sink_vel,
+      "Peculiar velocities of the particles. This is a * dx/dt where x is the "
+      "co-moving position of the particles.");
+
+  list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f, sinks,
+                                 mass, "Dynamical masses of the sinks.");
+
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, sinks, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
+
+  list[4] = io_make_output_field(
+      "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, sinks, h,
+      "Co-moving smoothing lengths (FWHM of the kernel) of the particles");
+
+  list[5] = io_make_physical_output_field(
+      "NumberOfSinkSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, sinks,
+      number_of_sink_swallows, /*can convert to comoving=*/0,
+      "Total number of sink merger events");
+
+  list[6] = io_make_physical_output_field(
+      "NumberOfGasSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, sinks,
+      number_of_gas_swallows, /*can convert to comoving=*/0,
+      "Total number of gas merger events");
+
+  /* Note: Since the swallowed momentum is computed with the physical velocity,
+     i.e. including the Hubble flow term, it is not convertible to comoving
+     frame. */
+  list[7] = io_make_physical_output_field_convert_sink(
+      "SwallowedAngularMomentum", FLOAT, 3, UNIT_CONV_ANGULAR_MOMENTUM, 0.f,
+      sinks,
+      /*can convert to comoving=*/0, convert_sink_swallowed_angular_momentum,
+      "Physical swallowed angular momentum of the particles");
+
+  list[8] = io_make_output_field(
+      "SubgridMasses", FLOAT, 1, UNIT_CONV_MASS, 0.f, sinks, subgrid_mass,
+      "Subgrid mass of the sink. Summed on sink-sink mergers");
+
+  list[9] = io_make_physical_output_field(
+      "GasDensities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f, sinks, rho_gas,
+      /*can convert to comoving=*/1, "Gas density at the location of the sink");
+
+  list[10] = io_make_output_field_convert_sink(
+      "GasVelocities", FLOAT, 3, UNIT_CONV_SPEED, 0.f, sinks,
+      convert_sink_gas_vel,
+      "Gas velocity at the location of the sink. Velocities are peculiar, i.e. "
+      "a * dx/dt where x is the "
+      "co-moving position of the gas.");
+
+  list[11] = io_make_output_field_convert_sink(
+      "GasSoundSpeeds", DOUBLE, 1, UNIT_CONV_SPEED, 0.f, sinks,
+      convert_sink_gas_sound_speed,
+      "Gas sound speed at the location of the sink (physical units)");
+
+#ifdef DEBUG_INTERACTIONS_SINKS
+
+  list += *num_fields;
+  *num_fields += 4;
+
+  list[0] =
+      io_make_output_field("Num_ngb_formation", INT, 1, UNIT_CONV_NO_UNITS, 0.f,
+                           sinks, num_ngb_formation, "Number of neighbors");
+  list[1] =
+      io_make_output_field("Ids_ngb_formation", LONGLONG,
+                           MAX_NUM_OF_NEIGHBOURS_SINKS, UNIT_CONV_NO_UNITS, 0.f,
+                           sinks, ids_ngbs_formation, "IDs of the neighbors");
+
+  list[2] =
+      io_make_output_field("Num_ngb_merger", INT, 1, UNIT_CONV_NO_UNITS, 0.f,
+                           sinks, num_ngb_merger, "Number of merger");
+  list[3] = io_make_output_field(
+      "Ids_ngb_merger", LONGLONG, MAX_NUM_OF_NEIGHBOURS_SINKS,
+      UNIT_CONV_NO_UNITS, 0.f, sinks, ids_ngbs_merger, "IDs of the neighbors");
+
+#endif
+}
+
+#endif /* SWIFT_BASIC_SINK_IO_H */
diff --git a/src/sink/Basic/sink_part.h b/src/sink/Basic/sink_part.h
new file mode 100644
index 0000000000000000000000000000000000000000..a3477c90ffcff81280108c08756e7f3d2c48aebd
--- /dev/null
+++ b/src/sink/Basic/sink_part.h
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_BASIC_SINK_PART_H
+#define SWIFT_BASIC_SINK_PART_H
+
+#include "timeline.h"
+
+#define sink_need_unique_id 1
+
+/**
+ * @brief Particle fields for the sink particles.
+ *
+ * All quantities related to gravity are stored in the associate #gpart.
+ */
+struct sink {
+
+  /*! Particle ID. */
+  long long id;
+
+  /*! Pointer to corresponding gravity part. */
+  struct gpart* gpart;
+
+  /*! Particle position. */
+  double x[3];
+
+  /* Offset between current position and position at last tree rebuild. */
+  float x_diff[3];
+
+  /*! Particle velocity. */
+  float v[3];
+
+  /* Particle smoothing length */
+  float h;
+
+  struct {
+
+    /* Number of neighbours. */
+    float wcount;
+
+    /* Number of neighbours spatial derivative. */
+    float wcount_dh;
+
+  } density;
+
+  /*! Sink particle mass */
+  float mass;
+
+  /*! Total mass of the gas neighbours. */
+  float ngb_mass;
+
+  /*! Integer number of neighbours */
+  int num_ngbs;
+
+  /*! Instantaneous accretion rate */
+  float accretion_rate;
+
+  /*! Subgrid mass of the sink */
+  float subgrid_mass;
+
+  /*! Sink mass at the start of each step, prior to any nibbling */
+  float mass_at_start_of_step;
+
+  /*! Density of the gas surrounding the black hole. */
+  float rho_gas;
+
+  /*! Smoothed sound speed of the gas surrounding the black hole. */
+  float sound_speed_gas;
+
+  /*! Smoothed velocity of the gas surrounding the black hole,
+   * in the frame of the black hole (internal units) */
+  float velocity_gas[3];
+
+  /*! Particle time bin */
+  timebin_t time_bin;
+
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
+  /*! Total (physical) angular momentum accumulated by swallowing particles */
+  float swallowed_angular_momentum[3];
+
+  /*! Total number of sink merger events (including sink swallowed
+   * by merged-in sinks) */
+  int number_of_sink_swallows;
+
+  /*! Total number of sink merger events (excluding sink swallowed
+   * by merged-in sinks) */
+  int number_of_direct_sink_swallows;
+
+  /*! Total number of gas particles swallowed (including particles swallowed
+   * by merged-in sinks) */
+  int number_of_gas_swallows;
+
+  /*! Total number of gas particles swallowed (excluding particles swallowed
+   * by merged-in sinks) */
+  int number_of_direct_gas_swallows;
+
+  /*! sink merger information (e.g. merging ID) */
+  struct sink_sink_data merger_data;
+
+#ifdef SWIFT_DEBUG_CHECKS
+
+  /* Time of the last drift */
+  integertime_t ti_drift;
+
+  /* Time of the last kick */
+  integertime_t ti_kick;
+
+#endif
+
+#ifdef DEBUG_INTERACTIONS_SINKS
+  /*! Number of interactions in merger SELF and PAIR */
+  int num_ngb_merger;
+
+  /*! List of interacting particles in merger SELF and PAIR */
+  long long ids_ngbs_merger[MAX_NUM_OF_NEIGHBOURS_SINKS];
+
+  /*! Number of interactions in compute formation SELF and PAIR */
+  int num_ngb_formation;
+
+  /*! List of interacting particles in compute formation SELF and PAIR */
+  long long ids_ngbs_formation[MAX_NUM_OF_NEIGHBOURS_SINKS];
+
+  /*! Number of interactions in compute formation SELF and PAIR */
+  int num_ngb_accretion;
+
+  /*! List of interacting particles in compute formation SELF and PAIR */
+  long long ids_ngbs_accretion[MAX_NUM_OF_NEIGHBOURS_SINKS];
+#endif
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+
+  /* Integer number of neighbours in the density loop */
+  int N_check_density;
+
+  /* Exact integer number of neighbours in the density loop */
+  int N_check_density_exact;
+
+  /*! Has this particle interacted with any unhibited neighbour? */
+  char inhibited_check_exact;
+
+  float n_check;
+
+  float n_check_exact;
+
+  float rho_check;
+
+  /*! Exact value of the density field obtained via brute-force loop */
+  float rho_check_exact;
+
+#endif
+
+} SWIFT_STRUCT_ALIGN;
+
+#endif /* SWIFT_BASIC_SINK_PART_H */
diff --git a/src/sink/Basic/sink_properties.h b/src/sink/Basic/sink_properties.h
new file mode 100644
index 0000000000000000000000000000000000000000..12a4d21c4023b239eea4bff0c90621fa822d2f8b
--- /dev/null
+++ b/src/sink/Basic/sink_properties.h
@@ -0,0 +1,148 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_BASIC_SINK_PROPERTIES_H
+#define SWIFT_BASIC_SINK_PROPERTIES_H
+
+/* Local header */
+#include "feedback_properties.h"
+#include "parser.h"
+
+/**
+ * @brief Properties of sink in the Default model.
+ */
+struct sink_props {
+
+  /* ----- Basic neighbour search properties ------ */
+
+  /*! Resolution parameter */
+  float eta_neighbours;
+
+  /*! Target weightd number of neighbours (for info only)*/
+  float target_neighbours;
+
+  /*! Smoothing length tolerance */
+  float h_tolerance;
+
+  /*! Tolerance on neighbour number  (for info only)*/
+  float delta_neighbours;
+
+  /*! Maximal number of iterations to converge h */
+  int max_smoothing_iterations;
+
+  /*! Maximal change of h over one time-step */
+  float log_max_h_change;
+
+  /*! Are we using a fixed cutoff radius? (all smoothing length calculations are
+   * disabled if so) */
+  char use_fixed_r_cut;
+
+  /* Use nibbling rather than swallowing for gas? */
+  float use_nibbling;
+
+  /* Gas mass below which sinks will not nibble. */
+  float min_gas_mass_for_nibbling;
+};
+
+/**
+ * @brief Initialise the sink properties from the parameter file.
+ *
+ * @param sp The #sink_props.
+ * @param phys_const The physical constants in the internal unit system.
+ * @param us The internal unit system.
+ * @param params The parsed parameters.
+ * @param cosmo The cosmological model.
+ * @param with_feedback Are we running with feedback?
+ */
+INLINE static void sink_props_init(
+    struct sink_props *sp, struct feedback_props *fp,
+    const struct phys_const *phys_const, const struct unit_system *us,
+    struct swift_params *params, const struct hydro_props *hydro_props,
+    const struct cosmology *cosmo, const int with_feedback) {
+
+  /* We don't use a fixed cutoff radius in this model. This property must always
+   * be present, as we use it to skip smoothing length iterations in
+   * runner_ghost if a fixed cutoff is being used. */
+  sp->use_fixed_r_cut = 0;
+
+  /* Read in the basic neighbour search properties or default to the hydro
+     ones if the user did not provide any different values */
+
+  /* Kernel properties */
+  sp->eta_neighbours = parser_get_opt_param_float(
+      params, "BasicSink:resolution_eta", hydro_props->eta_neighbours);
+
+  /* Tolerance for the smoothing length Newton-Raphson scheme */
+  sp->h_tolerance = parser_get_opt_param_float(params, "BasicSink:h_tolerance",
+                                               hydro_props->h_tolerance);
+
+  /* Get derived properties */
+  sp->target_neighbours = pow_dimension(sp->eta_neighbours) * kernel_norm;
+  const float delta_eta = sp->eta_neighbours * (1.f + sp->h_tolerance);
+  sp->delta_neighbours =
+      (pow_dimension(delta_eta) - pow_dimension(sp->eta_neighbours)) *
+      kernel_norm;
+
+  /* Number of iterations to converge h */
+  sp->max_smoothing_iterations =
+      parser_get_opt_param_int(params, "BasicSink:max_ghost_iterations",
+                               hydro_props->max_smoothing_iterations);
+
+  /* Time integration properties */
+  const float max_volume_change =
+      parser_get_opt_param_float(params, "BasicSink:max_volume_change", -1);
+  if (max_volume_change == -1)
+    sp->log_max_h_change = hydro_props->log_max_h_change;
+  else
+    sp->log_max_h_change = logf(powf(max_volume_change, hydro_dimension_inv));
+
+  sp->use_nibbling = parser_get_param_int(params, "BasicSink:use_nibbling");
+  if (sp->use_nibbling) {
+    sp->min_gas_mass_for_nibbling = parser_get_param_float(
+        params, "BasicSink:min_gas_mass_for_nibbling_Msun");
+    sp->min_gas_mass_for_nibbling *= phys_const->const_solar_mass;
+  }
+}
+
+/**
+ * @brief Write a sink_props struct to the given FILE as a stream of
+ * bytes.
+ *
+ * @param props the sink properties struct
+ * @param stream the file stream
+ */
+INLINE static void sink_struct_dump(const struct sink_props *props,
+                                    FILE *stream) {
+  restart_write_blocks((void *)props, sizeof(struct sink_props), 1, stream,
+                       "sink props", "Sink props");
+}
+
+/**
+ * @brief Restore a sink_props struct from the given FILE as a stream of
+ * bytes.
+ *
+ * @param props the sink properties struct
+ * @param stream the file stream
+ */
+INLINE static void sink_struct_restore(const struct sink_props *props,
+                                       FILE *stream) {
+  restart_read_blocks((void *)props, sizeof(struct sink_props), 1, stream, NULL,
+                      "Sink props");
+}
+
+#endif /* SWIFT_BASIC_SINK_PROPERTIES_H */
diff --git a/src/sink/Basic/sink_struct.h b/src/sink/Basic/sink_struct.h
new file mode 100644
index 0000000000000000000000000000000000000000..a25536824314e08071fe2bb394b24448acaf76db
--- /dev/null
+++ b/src/sink/Basic/sink_struct.h
@@ -0,0 +1,115 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Jonathan Davies (j.j.davies@ljmu.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_BASIC_STRUCT_DEFAULT_H
+#define SWIFT_BASIC_STRUCT_DEFAULT_H
+
+/**
+ * @brief Sink-related fields carried by each *gas* particle.
+ */
+struct sink_part_data {
+
+  /*! ID of the sink that will swallow this #part. */
+  long long swallow_id;
+};
+
+/**
+ * @brief Sink-related fields carried by each *sink* particle.
+ */
+struct sink_sink_data {
+
+  /*! ID of the sink that will swallow this #sink. */
+  long long swallow_id;
+
+  /*! Mass of the sink that will swallow this #sink. */
+  float swallow_mass;
+};
+
+/**
+ * @brief Return the ID of the sink that should swallow this #part.
+ *
+ * @param s_data The #part's #sink_part_data structure.
+ */
+__attribute__((always_inline)) INLINE static long long sink_get_part_swallow_id(
+    struct sink_part_data* s_data) {
+
+  return s_data->swallow_id;
+}
+
+/**
+ * @brief Update a given #part's sink data field to mark the particle has
+ * not yet been swallowed.
+ *
+ * @param s_data The #part's #sink_part_data structure.
+ */
+__attribute__((always_inline)) INLINE static void
+sink_mark_part_as_not_swallowed(struct sink_part_data* s_data) {
+
+  s_data->swallow_id = -1;
+}
+
+/**
+ * @brief Update a given #part's sink data field to mark the particle has
+ * having been been swallowed.
+ *
+ * @param p_data The #part's #sink_part_data structure.
+ */
+__attribute__((always_inline)) INLINE static void sink_mark_part_as_swallowed(
+    struct sink_part_data* s_data) {
+
+  s_data->swallow_id = -2;
+}
+
+/**
+ * @brief Update a given #sink's sink data field to mark the particle has
+ * not yet been swallowed.
+ *
+ * @param s_data The #sink's #sink_sink_data structure.
+ */
+__attribute__((always_inline)) INLINE static void
+sink_mark_sink_as_not_swallowed(struct sink_sink_data* s_data) {
+
+  s_data->swallow_id = -1;
+  s_data->swallow_mass = 0.f;
+}
+
+/**
+ * @brief Update a given #sink's sink data field to mark the particle has
+ * having been been swallowed.
+ *
+ * @param s_data The #sink's #bsink_sink_data structure.
+ */
+__attribute__((always_inline)) INLINE static void sink_mark_sink_as_merged(
+    struct sink_sink_data* s_data) {
+
+  s_data->swallow_id = -2;
+  s_data->swallow_mass = -1.f;
+}
+
+/**
+ * @brief Return the ID of the sink that should swallow this #sink.
+ *
+ * @param s_data The #sink's #sink_sink_data structure.
+ */
+__attribute__((always_inline)) INLINE static long long sink_get_sink_swallow_id(
+    struct sink_sink_data* s_data) {
+
+  return s_data->swallow_id;
+}
+
+#endif /* SWIFT_BASIC_STRUCT_DEFAULT_H */
diff --git a/src/sink/Default/sink.h b/src/sink/Default/sink.h
index 7ae8017350083d177502c29c905ab987ffa56090..1001987ae5054802f0b24eb5a9f166fe564c824a 100644
--- a/src/sink/Default/sink.h
+++ b/src/sink/Default/sink.h
@@ -30,10 +30,22 @@
 /**
  * @brief Computes the time-step of a given sink particle.
  *
- * @param sp Pointer to the sink-particle data.
+ * Note: In the Default sink, no time-step limit is applied.
+ *
+ * @param sink Pointer to the sink-particle data.
+ * @param sink_properties Properties of the sink model.
+ * @param with_cosmology Are we running with cosmological time integration.
+ * @param cosmo The current cosmological model (used if running with
+ * cosmology).
+ * @param grav_props The current gravity properties.
+ * @param time The current time (used if running without cosmology).
+ * @param time_base The time base.
  */
 __attribute__((always_inline)) INLINE static float sink_compute_timestep(
-    const struct sink* const sp) {
+    const struct sink* const sink, const struct sink_props* sink_properties,
+    const int with_cosmology, const struct cosmology* cosmo,
+    const struct gravity_props* grav_props, const double time,
+    const double time_base) {
 
   return FLT_MAX;
 }
@@ -46,30 +58,47 @@ __attribute__((always_inline)) INLINE static float sink_compute_timestep(
  *
  * @param sp The particle to act upon
  * @param sink_props The properties of the sink particles scheme.
+ * @param e The #engine
  */
 __attribute__((always_inline)) INLINE static void sink_first_init_sink(
-    struct sink* sp, const struct sink_props* sink_props) {}
+    struct sink* sp, const struct sink_props* sink_props,
+    const struct engine* e) {}
 
 /**
  * @brief Prepares a particle for the sink calculation.
  *
- * @param p The particle to act upon
+ * @param p The #part to act upon.
+ * @param sink_props The properties of the sink particles scheme.
  */
 __attribute__((always_inline)) INLINE static void sink_init_part(
-    struct part* restrict p) {}
+    struct part* restrict p, const struct sink_props* sink_props) {}
 
 /**
  * @brief Prepares a sink-particle for its interactions
  *
- * @param sp The particle to act upon
+ * @param sp The #sink particle to act upon.
  */
 __attribute__((always_inline)) INLINE static void sink_init_sink(
-    struct sink* sp) {}
+    struct sink* sp) {
+
+  sp->density.wcount = 0.f;
+  sp->density.wcount_dh = 0.f;
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  sp->N_check_density = 0;
+  sp->N_check_density_exact = 0;
+  sp->rho_check = 0.f;
+  sp->rho_check_exact = 0.f;
+  sp->n_check = 0.f;
+  sp->n_check_exact = 0.f;
+  sp->inhibited_check_exact = 0;
+#endif
+}
 
 /**
  * @brief Predict additional particle fields forward in time when drifting
  *
- * @param sp The particle
+ * @param sp The #sink.
  * @param dt_drift The drift time-step for positions.
  */
 __attribute__((always_inline)) INLINE static void sink_predict_extra(
@@ -79,7 +108,7 @@ __attribute__((always_inline)) INLINE static void sink_predict_extra(
  * @brief Sets the values to be predicted in the drifts to their values at a
  * kick time
  *
- * @param sp The particle.
+ * @param sp The #sink particle.
  */
 __attribute__((always_inline)) INLINE static void sink_reset_predicted_values(
     struct sink* restrict sp) {}
@@ -87,12 +116,86 @@ __attribute__((always_inline)) INLINE static void sink_reset_predicted_values(
 /**
  * @brief Kick the additional variables
  *
- * @param sp The particle to act upon
+ * @param sp The #sink particle to act upon
  * @param dt The time-step for this kick
  */
 __attribute__((always_inline)) INLINE static void sink_kick_extra(
     struct sink* sp, float dt) {}
 
+/**
+ * @brief Finishes the calculation of density on sinks
+ *
+ * @param si The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sink_end_density(
+    struct sink* si, const struct cosmology* cosmo) {
+
+  /* Some smoothing length multiples. */
+  const float h = si->h;
+  const float h_inv = 1.0f / h;                       /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv);       /* 1/h^d */
+  const float h_inv_dim_plus_one = h_inv_dim * h_inv; /* 1/h^(d+1) */
+
+  /* Finish the calculation by inserting the missing h-factors */
+  si->density.wcount *= h_inv_dim;
+  si->density.wcount_dh *= h_inv_dim_plus_one;
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  si->rho_check *= h_inv_dim;
+  si->n_check *= h_inv_dim;
+#endif
+}
+
+/**
+ * @brief Sets all particle fields to sensible values when the #sink has 0
+ * ngbs.
+ *
+ * @param sp The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sinks_sink_has_no_neighbours(
+    struct sink* restrict sp, const struct cosmology* cosmo) {
+
+  warning(
+      "Sink particle with ID %lld treated as having no neighbours (h: %g, "
+      "wcount: %g).",
+      sp->id, sp->h, sp->density.wcount);
+
+  /* Some smoothing length multiples. */
+  const float h = sp->h;
+  const float h_inv = 1.0f / h;                 /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
+
+  /* Re-set problematic values */
+  sp->density.wcount = kernel_root * h_inv_dim;
+  sp->density.wcount_dh = 0.f;
+}
+
+/**
+ * @brief Compute the accretion rate of the sink and any quantities
+ * required swallowing based on an accretion rate
+ *
+ * Adapted from black_holes_prepare_feedback
+ *
+ * @param si The sink particle.
+ * @param props The properties of the sink scheme.
+ * @param constants The physical constants (in internal units).
+ * @param cosmo The cosmological model.
+ * @param cooling Properties of the cooling model.
+ * @param floor_props Properties of the entropy floor.
+ * @param time Time since the start of the simulation (non-cosmo mode).
+ * @param with_cosmology Are we running with cosmology?
+ * @param dt The time-step size (in physical internal units).
+ * @param ti_begin Integer time value at the beginning of timestep
+ */
+__attribute__((always_inline)) INLINE static void sink_prepare_swallow(
+    struct sink* restrict si, const struct sink_props* props,
+    const struct phys_const* constants, const struct cosmology* cosmo,
+    const struct cooling_function_data* cooling,
+    const struct entropy_floor_properties* floor_props, const double time,
+    const int with_cosmology, const double dt, const integertime_t ti_begin) {}
+
 /**
  * @brief Calculate if the gas has the potential of becoming
  * a sink.
@@ -100,14 +203,15 @@ __attribute__((always_inline)) INLINE static void sink_kick_extra(
  * Return 0 if no sink formation should occur.
  * Note: called in runner_do_sink_formation
  *
- * @param sink_props the sink properties to use.
  * @param p the gas particles.
  * @param xp the additional properties of the gas particles.
+ * @param sink_props the sink properties to use.
  * @param phys_const the physical constants in internal units.
  * @param cosmo the cosmological parameters and properties.
  * @param hydro_props The properties of the hydro scheme.
  * @param us The internal system of units.
  * @param cooling The cooling data struct.
+ * @param entropy_floor The entropy_floor properties.
  *
  */
 INLINE static int sink_is_forming(
@@ -150,14 +254,17 @@ INLINE static int sink_should_convert_to_sink(
  *
  * Nothing to do here.
  *
- * @param e The #engine
- * @param p the gas particles.
- * @param xp the additional properties of the gas particles.
- * @param sink the new created sink  particle with its properties.
- * @param sink_props the sink properties to use.
- * @param phys_const the physical constants in internal units.
+ * @param p The gas particles.
+ * @param xp The additional properties of the gas particles.
+ * @param sink the new created #sink particle.
+ * @param e The #engine.
+ * @param sink_props The sink properties to use.
  * @param cosmo the cosmological parameters and properties.
  * @param with_cosmology if we run with cosmology.
+ * @param phys_const The physical constants in internal units.
+ * @param hydro_props The hydro properties to use.
+ * @param us The internal unit system.
+ * @param cooling The cooling function to use.
  */
 INLINE static void sink_copy_properties(
     const struct part* p, const struct xpart* xp, struct sink* sink,
@@ -166,7 +273,11 @@ INLINE static void sink_copy_properties(
     const struct phys_const* phys_const,
     const struct hydro_props* restrict hydro_props,
     const struct unit_system* restrict us,
-    const struct cooling_function_data* restrict cooling) {}
+    const struct cooling_function_data* restrict cooling) {
+
+  /* Set a smoothing length */
+  sink->h = p->h;
+}
 
 /**
  * @brief Update the properties of a sink particles by swallowing
@@ -188,9 +299,6 @@ __attribute__((always_inline)) INLINE static void sink_swallow_part(
  * @param spi The #sink to update.
  * @param spj The #sink that is swallowed.
  * @param cosmo The current cosmological model.
- * @param time Time since the start of the simulation (non-cosmo mode).
- * @param with_cosmology Are we running with cosmology?
- * @param props The properties of the black hole scheme.
  */
 __attribute__((always_inline)) INLINE static void sink_swallow_sink(
     struct sink* spi, const struct sink* spj, const struct cosmology* cosmo) {}
@@ -200,12 +308,12 @@ __attribute__((always_inline)) INLINE static void sink_swallow_sink(
  *
  * Nothing to do here.
  *
- * @param e The #engine
  * @param sink the sink particle.
- * @param sink_props the sink properties to use.
- * @param phys_const the physical constants in internal units.
- * @param cosmo the cosmological parameters and properties.
- * @param with_cosmology if we run with cosmology.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param cosmo The cosmological parameters and properties.
+ * @param with_cosmology If we run with cosmology.
+ * @param phys_const The physical constants in internal units.
  * @param us The internal unit system.
  */
 INLINE static int sink_spawn_star(struct sink* sink, const struct engine* e,
@@ -223,13 +331,13 @@ INLINE static int sink_spawn_star(struct sink* sink, const struct engine* e,
  *
  * Nothing to do here.
  *
- * @param e The #engine
- * @param sink the sink particle.
+ * @param sink The #sink particle.
  * @param sp The star particle.
- * @param sink_props the sink properties to use.
- * @param phys_const the physical constants in internal units.
- * @param cosmo the cosmological parameters and properties.
- * @param with_cosmology if we run with cosmology.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param cosmo The cosmological parameters and properties.
+ * @param with_cosmology If we run with cosmology.
+ * @param phys_const The physical constants in internal units.
  * @param us The internal unit system.
  */
 INLINE static void sink_copy_properties_to_star(
@@ -238,4 +346,92 @@ INLINE static void sink_copy_properties_to_star(
     const int with_cosmology, const struct phys_const* phys_const,
     const struct unit_system* restrict us) {}
 
+/**
+ * @brief Update the #sink particle properties before spawning a star.
+ *
+ * @param sink The #sink particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param phys_const The physical constants in internal units.
+ */
+INLINE static void sink_update_sink_properties_before_star_formation(
+    struct sink* sink, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const) {}
+
+/**
+ * @brief Update the #sink particle properties right after spawning a star.
+ *
+ * @param sink The #sink particle that spawed stars.
+ * @param sp The #spart particle spawned.
+ * @param e The #engine
+ * @param sink_props the sink properties to use.
+ * @param phys_const the physical constants in internal units.
+ * @param star_counter The star loop counter.
+ */
+INLINE static void sink_update_sink_properties_during_star_formation(
+    struct sink* sink, const struct spart* sp, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const,
+    int star_counter) {}
+
+/**
+ * @brief Update the #sink particle properties after star formation.
+ *
+ * @param sink The #sink particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param phys_const The physical constants in internal units.
+ */
+INLINE static void sink_update_sink_properties_after_star_formation(
+    struct sink* sink, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const) {}
+
+/**
+ * @brief Store the gravitational potential of a particle by copying it from
+ * its #gpart friend.
+ *
+ * @param p_data The sink data of a gas particle.
+ * @param gp The part's #gpart.
+ */
+__attribute__((always_inline)) INLINE static void sink_store_potential_in_part(
+    struct sink_part_data* p_data, const struct gpart* gp) {}
+
+/**
+ * @brief Compute all quantities required for the formation of a sink such as
+ * kinetic energy, potential energy, etc. This function works on the
+ * neighbouring gas particles.
+ *
+ * Nothing to do here.
+ *
+ * @param e The #engine.
+ * @param p The #part for which we compute the quantities.
+ * @param xp The #xpart data of the particle #p.
+ * @param pi A neighbouring #part of #p.
+ * @param xpi The #xpart data of the particle #pi.
+ * @param cosmo The cosmological parameters and properties.
+ * @param sink_props The sink properties to use.
+ */
+INLINE static void sink_prepare_part_sink_formation_gas_criteria(
+    struct engine* e, struct part* restrict p, struct xpart* restrict xp,
+    struct part* restrict pi, struct xpart* restrict xpi,
+    const struct cosmology* cosmo, const struct sink_props* sink_props) {}
+
+/**
+ * @brief Compute all quantities required for the formation of a sink. This
+ * function works on the neighbouring sink particles.
+ *
+ * Nothing to do here.
+ *
+ * @param e The #engine.
+ * @param p The #part for which we compute the quantities.
+ * @param xp The #xpart data of the particle #p.
+ * @param si A neighbouring #sink of #p.
+ * @param cosmo The cosmological parameters and properties.
+ * @param sink_props The sink properties to use.
+ */
+INLINE static void sink_prepare_part_sink_formation_sink_criteria(
+    struct engine* e, struct part* restrict p, struct xpart* restrict xp,
+    struct sink* restrict si, const int with_cosmology,
+    const struct cosmology* cosmo, const struct sink_props* sink_props,
+    const double time) {}
+
 #endif /* SWIFT_DEFAULT_SINK_H */
diff --git a/src/sink/Default/sink_iact.h b/src/sink/Default/sink_iact.h
index 5b74c60e08de4817c2ea1d071298420953e3e2ab..653ae2fe058b5341eccda3994dddbe2bf6e43b4b 100644
--- a/src/sink/Default/sink_iact.h
+++ b/src/sink/Default/sink_iact.h
@@ -35,7 +35,7 @@
 __attribute__((always_inline)) INLINE static void runner_iact_sink(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, struct part *restrict pj, const float a,
-    const float H, const struct sink_props *sink_props) {}
+    const float H) {}
 
 /**
  * @brief do sink computation after the runner_iact_density (non symmetric
@@ -53,38 +53,101 @@ __attribute__((always_inline)) INLINE static void runner_iact_sink(
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_sink(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, const struct part *restrict pj, const float a,
-    const float H, const struct sink_props *sink_props) {}
+    const float H) {}
+
+/**
+ * @brief Density interaction between two particles (non-symmetric).
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param si First particle (sink).
+ * @param pj Second particle (gas, not updated).
+ * @param with_cosmology Are we doing a cosmological run?
+ * @param cosmo The cosmological model.
+ * @param grav_props The properties of the gravity scheme (softening, G, ...).
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
+ */
+__attribute__((always_inline)) INLINE static void
+runner_iact_nonsym_sinks_gas_density(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *si, const struct part *pj, const int with_cosmology,
+    const struct cosmology *cosmo, const struct gravity_props *grav_props,
+    const struct sink_props *sink_props, const integertime_t ti_current,
+    const double time) {
+
+  float wi, wi_dx;
+
+  /* Get r. */
+  const float r = sqrtf(r2);
+
+  /* Compute the kernel function */
+  const float hi_inv = 1.0f / hi;
+  const float ui = r * hi_inv;
+  kernel_deval(ui, &wi, &wi_dx);
+
+  /* Compute contribution to the number of neighbours */
+  si->density.wcount += wi;
+  si->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  si->rho_check += hydro_get_mass(pj) * wi;
+  si->n_check += wi;
+  si->N_check_density++;
+#endif
+}
 
 /**
  * @brief Compute sink-sink swallow interaction (non-symmetric).
  *
  * @param r2 Comoving square distance between the two particles.
  * @param dx Comoving vector separating both particles (pi - pj).
- * @param ri Comoving cut off radius of particle i.
- * @param rj Comoving cut off radius of particle j.
+ * @param hi Comoving smoothing length of particle i.
+ * @param hj Comoving smoothing length of particle j.
  * @param si First sink particle.
  * @param sj Second sink particle.
+ * @param with_cosmology if we run with cosmology.
+ * @param cosmo The cosmological parameters and properties.
+ * @param grav_props The gravity scheme parameters and properties.
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_sinks_sink_swallow(const float r2, const float *dx,
-                                      const float ri, const float rj,
-                                      struct sink *restrict si,
-                                      struct sink *restrict sj) {}
+runner_iact_nonsym_sinks_sink_swallow(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *restrict si, struct sink *restrict sj,
+    const int with_cosmology, const struct cosmology *cosmo,
+    const struct gravity_props *grav_props,
+    const struct sink_props *sink_properties, const integertime_t ti_current,
+    const double time) {}
 
 /**
  * @brief Compute sink-gas swallow interaction (non-symmetric).
  *
  * @param r2 Comoving square distance between the two particles.
  * @param dx Comoving vector separating both particles (pi - pj).
- * @param ri Comoving cut off radius of particle i.
+ * @param hi Comoving smoothing length of particle i.
  * @param hj Comoving smoothing-length of particle j.
  * @param si First sink particle.
  * @param pj Second particle.
+ * @param with_cosmology if we run with cosmology.
+ * @param cosmo The cosmological parameters and properties.
+ * @param grav_props The gravity scheme parameters and properties.
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_sinks_gas_swallow(const float r2, const float *dx,
-                                     const float ri, const float hj,
-                                     struct sink *restrict si,
-                                     struct part *restrict pj) {}
+runner_iact_nonsym_sinks_gas_swallow(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *restrict si, struct part *restrict pj,
+    const int with_cosmology, const struct cosmology *cosmo,
+    const struct gravity_props *grav_props,
+    const struct sink_props *sink_properties, const integertime_t ti_current,
+    const double time) {}
 
 #endif
diff --git a/src/sink/Default/sink_io.h b/src/sink/Default/sink_io.h
index fd47e65d8b94f573835f0f44ceb293c8d4f4c6c5..c4fd30737697a61be167453e8eaf584cf4ab6349 100644
--- a/src/sink/Default/sink_io.h
+++ b/src/sink/Default/sink_io.h
@@ -33,7 +33,7 @@ INLINE static void sink_read_particles(struct sink* sinks,
                                        struct io_props* list, int* num_fields) {
 
   /* Say how much we want to read */
-  *num_fields = 4;
+  *num_fields = 5;
 
   /* List what we want to read */
   list[0] = io_make_input_field("Coordinates", DOUBLE, 3, COMPULSORY,
@@ -44,6 +44,8 @@ INLINE static void sink_read_particles(struct sink* sinks,
                                 sinks, mass);
   list[3] = io_make_input_field("ParticleIDs", LONGLONG, 1, COMPULSORY,
                                 UNIT_CONV_NO_UNITS, sinks, id);
+  list[4] = io_make_input_field("SmoothingLength", FLOAT, 1, OPTIONAL,
+                                UNIT_CONV_LENGTH, sinks, h);
 }
 
 INLINE static void convert_sink_pos(const struct engine* e,
@@ -107,7 +109,7 @@ INLINE static void sink_write_particles(const struct sink* sinks,
                                         int with_cosmology) {
 
   /* Say how much we want to write */
-  *num_fields = 4;
+  *num_fields = 5;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_sink(
@@ -122,9 +124,13 @@ INLINE static void sink_write_particles(const struct sink* sinks,
   list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f, sinks,
                                  mass, "Masses of the particles");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           sinks, id, "Unique ID of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, sinks, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
+
+  list[4] = io_make_output_field(
+      "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, sinks, h,
+      "Co-moving smoothing lengths (FWHM of the kernel) of the particles");
 
 #ifdef DEBUG_INTERACTIONS_SINKS
 
diff --git a/src/sink/Default/sink_part.h b/src/sink/Default/sink_part.h
index 112248fc81dfce1f8e25714c820e5fa4ed5f6c36..893a01c5146cee6c59f6dd26a5549792d8725b14 100644
--- a/src/sink/Default/sink_part.h
+++ b/src/sink/Default/sink_part.h
@@ -45,8 +45,18 @@ struct sink {
   /*! Particle velocity. */
   float v[3];
 
-  /*! Cut off radius. */
-  float r_cut;
+  /* Particle smoothing length */
+  float h;
+
+  struct {
+
+    /* Number of neighbours. */
+    float wcount;
+
+    /* Number of neighbours spatial derivative. */
+    float wcount_dh;
+
+  } density;
 
   /*! Sink particle mass */
   float mass;
@@ -54,6 +64,12 @@ struct sink {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
+  /*! Integer number of neighbours */
+  int num_ngbs;
+
 #ifdef SWIFT_DEBUG_CHECKS
 
   /* Time of the last drift */
@@ -64,6 +80,10 @@ struct sink {
 
 #endif
 
+  /*! Chemistry information (e.g. metal content at birth, swallowed metal
+   * content, etc.) */
+  struct chemistry_sink_data chemistry_data;
+
   /*! sink merger information (e.g. merging ID) */
   struct sink_sink_data merger_data;
 
@@ -86,6 +106,29 @@ struct sink {
   /*! List of interacting particles in compute formation SELF and PAIR */
   long long ids_ngbs_accretion[MAX_NUM_OF_NEIGHBOURS_SINKS];
 #endif
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+
+  /* Integer number of neighbours in the density loop */
+  int N_check_density;
+
+  /* Exact integer number of neighbours in the density loop */
+  int N_check_density_exact;
+
+  /*! Has this particle interacted with any unhibited neighbour? */
+  char inhibited_check_exact;
+
+  float n_check;
+
+  float n_check_exact;
+
+  float rho_check;
+
+  /*! Exact value of the density field obtained via brute-force loop */
+  float rho_check_exact;
+
+#endif
+
 } SWIFT_STRUCT_ALIGN;
 
 #endif /* SWIFT_DEFAULT_SINK_PART_H */
diff --git a/src/sink/Default/sink_properties.h b/src/sink/Default/sink_properties.h
index 6691840dc98e5c5897b881539e1c3979c17e21f0..f9bd54e74996ab3371fec36b82aa31130766a842 100644
--- a/src/sink/Default/sink_properties.h
+++ b/src/sink/Default/sink_properties.h
@@ -19,13 +19,38 @@
 #ifndef SWIFT_DEFAULT_SINK_PROPERTIES_H
 #define SWIFT_DEFAULT_SINK_PROPERTIES_H
 
+/* Local header */
+#include "feedback_properties.h"
+#include "parser.h"
+
 /**
  * @brief Properties of sink in the Default model.
  */
 struct sink_props {
 
-  /*! Cut off radius */
-  float cut_off_radius;
+  /* ----- Basic neighbour search properties ------ */
+
+  /*! Resolution parameter */
+  float eta_neighbours;
+
+  /*! Target weightd number of neighbours (for info only)*/
+  float target_neighbours;
+
+  /*! Smoothing length tolerance */
+  float h_tolerance;
+
+  /*! Tolerance on neighbour number  (for info only)*/
+  float delta_neighbours;
+
+  /*! Maximal number of iterations to converge h */
+  int max_smoothing_iterations;
+
+  /*! Maximal change of h over one time-step */
+  float log_max_h_change;
+
+  /*! Are we using a fixed cutoff radius? (all smoothing length calculations are
+   * disabled if so) */
+  char use_fixed_r_cut;
 };
 
 /**
@@ -36,15 +61,50 @@ struct sink_props {
  * @param us The internal unit system.
  * @param params The parsed parameters.
  * @param cosmo The cosmological model.
+ * @param with_feedback Are we running with feedback?
  */
-INLINE static void sink_props_init(struct sink_props *sp,
-                                   const struct phys_const *phys_const,
-                                   const struct unit_system *us,
-                                   struct swift_params *params,
-                                   const struct cosmology *cosmo) {
-
-  sp->cut_off_radius =
-      parser_get_param_float(params, "DefaultSink:cut_off_radius");
+INLINE static void sink_props_init(
+    struct sink_props *sp, struct feedback_props *fp,
+    const struct phys_const *phys_const, const struct unit_system *us,
+    struct swift_params *params, const struct hydro_props *hydro_props,
+    const struct cosmology *cosmo, const int with_feedback) {
+
+  /* Read in the basic neighbour search properties or default to the hydro
+     ones if the user did not provide any different values */
+
+  /* Kernel properties */
+  sp->eta_neighbours = parser_get_opt_param_float(
+      params, "Sinks:resolution_eta", hydro_props->eta_neighbours);
+
+  /* Tolerance for the smoothing length Newton-Raphson scheme */
+  sp->h_tolerance = parser_get_opt_param_float(params, "Sinks:h_tolerance",
+                                               hydro_props->h_tolerance);
+
+  /* Get derived properties */
+  sp->target_neighbours = pow_dimension(sp->eta_neighbours) * kernel_norm;
+  const float delta_eta = sp->eta_neighbours * (1.f + sp->h_tolerance);
+  sp->delta_neighbours =
+      (pow_dimension(delta_eta) - pow_dimension(sp->eta_neighbours)) *
+      kernel_norm;
+
+  /* Number of iterations to converge h */
+  sp->max_smoothing_iterations =
+      parser_get_opt_param_int(params, "Sinks:max_ghost_iterations",
+                               hydro_props->max_smoothing_iterations);
+
+  /* Time integration properties */
+  const float max_volume_change =
+      parser_get_opt_param_float(params, "Sinks:max_volume_change", -1);
+  if (max_volume_change == -1)
+    sp->log_max_h_change = hydro_props->log_max_h_change;
+  else
+    sp->log_max_h_change = logf(powf(max_volume_change, hydro_dimension_inv));
+
+  /* This property determines whether we're using a fixed cutoff radius (and
+   * smoothing length) for the sink. It must always be present, as we use it
+   * to skip smoothing length iterations in runner_ghost if a fixed cutoff is
+   * being used. */
+  sp->use_fixed_r_cut = 0;
 }
 
 /**
diff --git a/src/sink/GEAR/sink.h b/src/sink/GEAR/sink.h
index a3e853fc4fd49abc0eb546e4f295ab46772fc444..47baafcffc08b4645ef8679427ea73433743a21a 100644
--- a/src/sink/GEAR/sink.h
+++ b/src/sink/GEAR/sink.h
@@ -1,6 +1,7 @@
 /*******************************************************************************
  * This file is part of SWIFT.
  * Copyright (c) 2021 Loic Hausammann (loic.hausammann@epfl.ch)
+ *               2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -21,22 +22,134 @@
 
 #include <float.h>
 
+/* Put pragma if gsl around here */
+#ifdef HAVE_LIBGSL
+#include <gsl/gsl_cdf.h>
+#endif
+
 /* Local includes */
+#include "active.h"
+#include "chemistry.h"
 #include "cooling.h"
+#include "feedback.h"
 #include "minmax.h"
 #include "random.h"
+#include "sink_getters.h"
 #include "sink_part.h"
 #include "sink_properties.h"
+#include "sink_setters.h"
+#include "star_formation.h"
 
 /**
  * @brief Computes the time-step of a given sink particle.
  *
- * @param sp Pointer to the sink-particle data.
+ * @param sink Pointer to the sink-particle data.
+ * @param sink_properties Properties of the sink model.
+ * @param with_cosmology Are we running with cosmological time integration.
+ * @param cosmo The current cosmological model (used if running with
+ * cosmology).
+ * @param grav_props The current gravity properties.
+ * @param time The current time (used if running without cosmology).
+ * @param time_base The time base.
  */
 __attribute__((always_inline)) INLINE static float sink_compute_timestep(
-    const struct sink* const sp) {
+    const struct sink* const sink, const struct sink_props* sink_properties,
+    const int with_cosmology, const struct cosmology* cosmo,
+    const struct gravity_props* grav_props, const double time,
+    const double time_base) {
+
+  /* Background sink particles have no time-step limits */
+  if (sink->birth_time == -1.) {
+    return FLT_MAX;
+  }
+
+  /* CFL condition for sink. Notice the conversion to physical units ------- */
+  const float CFL_condition = sink_properties->CFL_condition;
+  const double gas_v_phys[3] = {
+      sink->to_collect.velocity_gas[0] * cosmo->a_inv,
+      sink->to_collect.velocity_gas[1] * cosmo->a_inv,
+      sink->to_collect.velocity_gas[2] * cosmo->a_inv};
+  double gas_v_norm2 = gas_v_phys[0] * gas_v_phys[0] +
+                       gas_v_phys[1] * gas_v_phys[1] +
+                       gas_v_phys[2] * gas_v_phys[2];
+
+  const double gas_c_phys =
+      sink->to_collect.sound_speed_gas * cosmo->a_factor_sound_speed;
+  const double gas_c_phys2 = gas_c_phys * gas_c_phys;
+  const float denominator = sqrtf(gas_c_phys2 + gas_v_norm2);
+  const float h_min =
+      cosmo->a * kernel_gamma * min(sink->h, sink->to_collect.minimal_h_gas);
+  float dt_cfl = 0.0;
+
+  /* This case can happen if the sink is just born. */
+  if (gas_v_norm2 == 0.0) {
+    dt_cfl = FLT_MAX;
+  } else {
+    dt_cfl = 2.f * CFL_condition * h_min / denominator;
+  }
+
+  /* Free fall time condition: the sink must anticipate gas collapse ------- */
+  const float rho_sink =
+      3.0 * sink->mass / (4.0 * M_PI * h_min * h_min * h_min);
+  const float dt_ff =
+      sqrtf(3.0 * M_PI / (32.0 * grav_props->G_Newton * rho_sink));
+
+  /* Compute sink-sink orbital integration timestep ------------------------ */
+  float dt_2_body = 0.0;
+
+  /* If there are no sink neighbours, then the values are FLT_MAX. Prevent
+     giving a NaN to the timestep */
+  if ((sink->to_collect.minimal_sink_t_c == FLT_MAX) ||
+      (sink->to_collect.minimal_sink_t_dyn == FLT_MAX)) {
+    dt_2_body = FLT_MAX;
+  } else {
+    dt_2_body = sink->to_collect.minimal_sink_t_c *
+                sink->to_collect.minimal_sink_t_dyn /
+                (sink->to_collect.minimal_sink_t_c +
+                 sink->to_collect.minimal_sink_t_dyn);
+  }
+
+  /* SF - accretion timestep ------------------------------------------------*/
+  /* Now, limit timestep by computing how much we restricted the sink accretion
+     for SF reasons compared to an unrestricted accretion.
+     Note: If we divide by mass_eligible_swallow, we get the relative error
+     compared to unrestricted swallow. */
+  const float Delta_M =
+      sink->to_collect.mass_swallowed - sink->to_collect.mass_eligible_swallow;
+
+  /* We want a big timestep if the error is small. */
+  float dt_SF = FLT_MAX;
+
+  /* If Delta_M < 0, then we are limiting the accretion rate by a huge factor.
+     To avoid biasing the SFR too much, do a small timestep to accrete the
+     remaining mass sooner. */
+  if (sink_properties->n_IMF > 0 && Delta_M < 0) {
+    /* Compute an accretion rate using this Delta_M. Use the minmal timestep
+     based on the local gas properties. If we use the current timestep, then we
+     can end up with timesteps smaller and smaller until they are smaller than
+     the minimal engine timestep. */
+    const float dt_tmp = min3(dt_cfl, dt_ff, dt_2_body);
+    const float M_dot = Delta_M / dt_tmp;
+    dt_SF = sink_properties->tolerance_SF_timestep *
+            sink->to_collect.mass_eligible_swallow / fabsf(M_dot);
+  }
+
+  /* Sink age (in internal units) */
+  double sink_age = sink_get_sink_age(sink, with_cosmology, cosmo, time);
+
+  /* Take the minimum dt --------------------------------------------------- */
+  float dt = min3(dt_cfl, dt_ff, dt_SF);
 
-  return FLT_MAX;
+  /* What age category are we in? */
+  if (sink_age > sink_properties->age_threshold_unlimited) {
+    return dt_2_body;
+  } else if (sink_age > sink_properties->age_threshold) {
+    dt = min3(dt, dt_2_body, sink_properties->max_time_step_old);
+    return dt;
+  } else {
+    dt = min3(dt, dt_2_body, sink_properties->max_time_step_young);
+    return dt;
+  }
 }
 
 /**
@@ -45,13 +158,20 @@ __attribute__((always_inline)) INLINE static float sink_compute_timestep(
  * This function is called only once just after the ICs have been
  * read in to do some conversions.
  *
- * @param sp The particle to act upon
+ * @param sp The #sink particle to act upon.
  * @param sink_props The properties of the sink particles scheme.
+ * @param e The #engine
  */
 __attribute__((always_inline)) INLINE static void sink_first_init_sink(
-    struct sink* sp, const struct sink_props* sink_props) {
+    struct sink* sp, const struct sink_props* sink_props,
+    const struct engine* e) {
+
+  /* Set the smoothing length if it is fixed. Note that, otherwise, the
+     smoothing lengths were read from the ICs. */
+  if (sink_props->use_fixed_r_cut) {
+    sp->h = sink_props->cut_off_radius / kernel_gamma;
+  }
 
-  sp->r_cut = sink_props->cut_off_radius;
   sp->time_bin = 0;
 
   sp->number_of_gas_swallows = 0;
@@ -61,32 +181,103 @@ __attribute__((always_inline)) INLINE static void sink_first_init_sink(
   sp->swallowed_angular_momentum[0] = 0.f;
   sp->swallowed_angular_momentum[1] = 0.f;
   sp->swallowed_angular_momentum[2] = 0.f;
+  sp->n_stars = 0;
+
+  sp->has_IMF_changed_from_popIII_to_popII = 0;
 
   sink_mark_sink_as_not_swallowed(&sp->merger_data);
+
+  /* Bug fix: Setup the target mass for sink formation after reading the
+     ICs. Otherwise sink->target_mass_Msun = 0.0 and a sink present in the IC
+     spawn a star of mass 0.0... */
+  sink_update_target_mass(sp, sink_props, e, 0);
+
+  /* Initialize to the mass of the sink */
+  sp->mass_tot_before_star_spawning = sp->mass;
+
+  /* Init properties based on the local gas */
+  sp->to_collect.minimal_h_gas = FLT_MAX;
+  sp->to_collect.rho_gas = 0.0;
+  sp->to_collect.sound_speed_gas = 0.0;
+  sp->to_collect.velocity_gas[0] = 0.0;
+  sp->to_collect.velocity_gas[1] = 0.0;
+  sp->to_collect.velocity_gas[2] = 0.0;
+  sp->to_collect.minimal_sink_t_c = FLT_MAX;
+  sp->to_collect.minimal_sink_t_dyn = FLT_MAX;
+  sp->to_collect.mass_eligible_swallow = 0.0;
+  sp->to_collect.mass_swallowed = sp->mass;
 }
 
 /**
  * @brief Initialisation of particle data before the hydro density loop.
  * Note: during initalisation (space_init)
  *
- * @param p The particle to act upon
+ * @param p The #part to act upon.
+ * @param sink_props The properties of the sink particles scheme.
  */
 __attribute__((always_inline)) INLINE static void sink_init_part(
-    struct part* restrict p) {
+    struct part* restrict p, const struct sink_props* sink_props) {
 
   struct sink_part_data* cpd = &p->sink_data;
 
-  cpd->can_form_sink = 1;
+  if (sink_props->disable_sink_formation) {
+    cpd->can_form_sink = 0;
+  } else {
+    cpd->can_form_sink = 1;
+  }
+  cpd->E_kin_neighbours = 0.f;
+  cpd->E_int_neighbours = 0.f;
+  cpd->E_rad_neighbours = 0.f;
+  cpd->E_pot_self_neighbours = 0.f;
+  cpd->E_pot_ext_neighbours = 0.f;
+  cpd->E_mag_neighbours = 0.f;
+  cpd->E_rot_neighbours[0] = 0.f;
+  cpd->E_rot_neighbours[1] = 0.f;
+  cpd->E_rot_neighbours[2] = 0.f;
+
+  /* Do not reset the potential to 0. Keep the value computed at the end of the
+  last step. This value is used in runner_iact_nonsym_sink() and
+  runner_iact_sink() to check which particle is at a potential minimum. If you
+  set this value to 0, then we break the check. This value is used instead of
+  gpart->potential because:
+  1) cpd->potential does not break MPI, while gpart->potential does
+  2) gpart->potential is not yet computed in runner_iact_X_sink(). */
+  /* cpd->potential = 0.f; */
+  cpd->E_mec_bound = 0.f; /* Gravitationally bound particles will have
+                             E_mec_bound < 0. This is checked before comparing
+                             any other value with this one. So no need to put
+                             it to the max of float. */
+  cpd->is_overlapping_sink = 0;
 }
 
 /**
  * @brief Initialisation of sink particle data before sink loops.
  * Note: during initalisation (space_init_sinks)
  *
- * @param sp The particle to act upon
+ * @param sp The #sink particle to act upon.
  */
 __attribute__((always_inline)) INLINE static void sink_init_sink(
     struct sink* sp) {
+
+  sp->density.wcount = 0.f;
+  sp->density.wcount_dh = 0.f;
+
+  /* Reset to the mass of the sink */
+  sp->mass_tot_before_star_spawning = sp->mass;
+
+  /* Init properties based on the local gas */
+  sp->to_collect.minimal_h_gas = FLT_MAX;
+  sp->to_collect.rho_gas = 0.0;
+  sp->to_collect.sound_speed_gas = 0.0;
+  sp->to_collect.velocity_gas[0] = 0.0;
+  sp->to_collect.velocity_gas[1] = 0.0;
+  sp->to_collect.velocity_gas[2] = 0.0;
+  sp->to_collect.minimal_sink_t_c = FLT_MAX;
+  sp->to_collect.minimal_sink_t_dyn = FLT_MAX;
+  sp->to_collect.mass_eligible_swallow = 0.0;
+  sp->to_collect.mass_swallowed = sp->mass;
+  sp->num_ngbs = 0;
+
 #ifdef DEBUG_INTERACTIONS_SINKS
   for (int i = 0; i < MAX_NUM_OF_NEIGHBOURS_SINKS; ++i)
     sp->ids_ngbs_accretion[i] = -1;
@@ -100,12 +291,22 @@ __attribute__((always_inline)) INLINE static void sink_init_sink(
     sp->ids_ngbs_formation[i] = -1;
   sp->num_ngb_formation = 0;
 #endif
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  sp->N_check_density = 0;
+  sp->N_check_density_exact = 0;
+  sp->rho_check = 0.f;
+  sp->rho_check_exact = 0.f;
+  sp->n_check = 0.f;
+  sp->n_check_exact = 0.f;
+  sp->inhibited_check_exact = 0;
+#endif
 }
 
 /**
  * @brief Predict additional particle fields forward in time when drifting
  *
- * @param sp The particle
+ * @param sp The #sink.
  * @param dt_drift The drift time-step for positions.
  */
 __attribute__((always_inline)) INLINE static void sink_predict_extra(
@@ -115,7 +316,7 @@ __attribute__((always_inline)) INLINE static void sink_predict_extra(
  * @brief Sets the values to be predicted in the drifts to their values at a
  * kick time
  *
- * @param sp The particle.
+ * @param sp The #sink particle.
  */
 __attribute__((always_inline)) INLINE static void sink_reset_predicted_values(
     struct sink* restrict sp) {}
@@ -123,12 +324,100 @@ __attribute__((always_inline)) INLINE static void sink_reset_predicted_values(
 /**
  * @brief Kick the additional variables
  *
- * @param sp The particle to act upon
+ * @param sp The #sink particle to act upon
  * @param dt The time-step for this kick
  */
 __attribute__((always_inline)) INLINE static void sink_kick_extra(
     struct sink* sp, float dt) {}
 
+/**
+ * @brief Finishes the calculation of density on sinks
+ *
+ * @param si The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sink_end_density(
+    struct sink* si, const struct cosmology* cosmo) {
+
+  const float h = si->h;
+  const float h_inv = 1.0f / h;                       /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv);       /* 1/h^d */
+  const float h_inv_dim_plus_one = h_inv_dim * h_inv; /* 1/h^(d+1) */
+
+  /* --- Finish the calculation by inserting the missing h factors --- */
+  si->to_collect.rho_gas *= h_inv_dim;
+  const float rho_inv = 1.f / si->to_collect.rho_gas;
+
+  /* For the following, we also have to undo the mass smoothing
+   * (N.B.: bp->velocity_gas is in BH frame, in internal units). */
+  si->to_collect.sound_speed_gas *= h_inv_dim * rho_inv;
+  si->to_collect.velocity_gas[0] *= h_inv_dim * rho_inv;
+  si->to_collect.velocity_gas[1] *= h_inv_dim * rho_inv;
+  si->to_collect.velocity_gas[2] *= h_inv_dim * rho_inv;
+
+  /* Finish the calculation by inserting the missing h-factors */
+  si->density.wcount *= h_inv_dim;
+  si->density.wcount_dh *= h_inv_dim_plus_one;
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  si->rho_check *= h_inv_dim;
+  si->n_check *= h_inv_dim;
+#endif
+}
+
+/**
+ * @brief Sets all particle fields to sensible values when the #sink has 0
+ * ngbs.
+ *
+ * @param sp The particle to act upon
+ * @param cosmo The current cosmological model.
+ */
+__attribute__((always_inline)) INLINE static void sinks_sink_has_no_neighbours(
+    struct sink* restrict sp, const struct cosmology* cosmo) {
+
+  warning(
+      "Sink particle with ID %lld treated as having no neighbours (h: %g, "
+      "numb_ngbs: %i).",
+      sp->id, sp->h, sp->num_ngbs);
+
+  /* Some smoothing length multiples. */
+  const float h = sp->h;
+  const float h_inv = 1.0f / h;                 /* 1/h */
+  const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */
+
+  /* Reset problematic values */
+  sp->to_collect.velocity_gas[0] = sp->v[0];
+  sp->to_collect.velocity_gas[1] = sp->v[1];
+  sp->to_collect.velocity_gas[2] = sp->v[2];
+  sp->to_collect.minimal_h_gas = h;
+  sp->density.wcount = kernel_root * h_inv_dim;
+  sp->density.wcount_dh = 0.f;
+}
+
+/**
+ * @brief Compute the accretion rate of the sink and any quantities
+ * required swallowing based on an accretion rate
+ *
+ * Adapted from black_holes_prepare_feedback
+ *
+ * @param si The sink particle.
+ * @param props The properties of the sink scheme.
+ * @param constants The physical constants (in internal units).
+ * @param cosmo The cosmological model.
+ * @param cooling Properties of the cooling model.
+ * @param floor_props Properties of the entropy floor.
+ * @param time Time since the start of the simulation (non-cosmo mode).
+ * @param with_cosmology Are we running with cosmology?
+ * @param dt The time-step size (in physical internal units).
+ * @param ti_begin Integer time value at the beginning of timestep
+ */
+__attribute__((always_inline)) INLINE static void sink_prepare_swallow(
+    struct sink* restrict si, const struct sink_props* props,
+    const struct phys_const* constants, const struct cosmology* cosmo,
+    const struct cooling_function_data* cooling,
+    const struct entropy_floor_properties* floor_props, const double time,
+    const int with_cosmology, const double dt, const integertime_t ti_begin) {}
+
 /**
  * @brief Calculate if the gas has the potential of becoming
  * a sink.
@@ -136,14 +425,15 @@ __attribute__((always_inline)) INLINE static void sink_kick_extra(
  * Return 0 if no sink formation should occur.
  * Note: called in runner_do_sink_formation
  *
- * @param sink_props the sink properties to use.
  * @param p the gas particles.
  * @param xp the additional properties of the gas particles.
+ * @param sink_props the sink properties to use.
  * @param phys_const the physical constants in internal units.
  * @param cosmo the cosmological parameters and properties.
  * @param hydro_props The properties of the hydro scheme.
  * @param us The internal system of units.
  * @param cooling The cooling data struct.
+ * @param entropy_floor The entropy_floor properties.
  *
  */
 INLINE static int sink_is_forming(
@@ -158,19 +448,84 @@ INLINE static int sink_is_forming(
   /* the particle is not elligible */
   if (!p->sink_data.can_form_sink) return 0;
 
-  const float temperature_max = sink_props->maximal_temperature;
+  const struct sink_part_data* sink_data = &p->sink_data;
+
+  const float temperature_threshold = sink_props->temperature_threshold;
   const float temperature = cooling_get_temperature(phys_const, hydro_props, us,
                                                     cosmo, cooling, p, xp);
 
   const float density_threshold = sink_props->density_threshold;
+  const float maximal_density_threshold = sink_props->maximal_density_threshold;
   const float density = hydro_get_physical_density(p, cosmo);
 
-  if (density > density_threshold && temperature < temperature_max) {
-    message("forming a sink particle ! %lld", p->id);
-    return 1;
+  const float div_v = sink_get_physical_div_v_from_part(p);
+
+  const float h = p->h;
+  const float sink_cut_off_radius = sink_props->cut_off_radius;
+
+  double E_grav = sink_data->E_pot_self_neighbours;
+  double E_rot_neighbours = sink_compute_neighbour_rotation_energy_magnitude(p);
+  double E_tot = sink_data->E_kin_neighbours + sink_data->E_int_neighbours +
+                 E_grav + sink_data->E_mag_neighbours;
+
+  /* Density criterion */
+  if (density < density_threshold) {
+    return 0;
+  }
+  /* Here we have density >= density_threshold */
+
+  /* If density_threshold <= density <= maximal_density_threshold, check the
+     temperature. If density > maximal_density_threshold, do no check the
+     temperature. */
+  if ((density <= maximal_density_threshold) &&
+      (temperature >= temperature_threshold)) {
+    return 0;
+  }
+
+  /* Contracting gas criterion */
+  if ((sink_props->sink_formation_contracting_gas_criterion) && (div_v > 0)) {
+    return 0;
   }
 
-  return 0;
+  /* Smoothing length criterion */
+  if ((sink_props->sink_formation_smoothing_length_criterion) &&
+      (kernel_gamma * h >= sink_cut_off_radius)) {
+    return 0;
+  }
+
+  /* Active neighbours criterion */
+  /* This is checked on the fly in runner_do_sink_formation(). The part is
+     flagged to not form sink through p->sink_data.can_form_sink */
+
+  /* Jeans instability criterion */
+  if ((sink_props->sink_formation_jeans_instability_criterion) &&
+      (sink_data->E_int_neighbours >= 0.5f * fabs(E_grav))) {
+    return 0;
+  }
+
+  if ((sink_props->sink_formation_jeans_instability_criterion) &&
+      (sink_data->E_int_neighbours + E_rot_neighbours >= fabs(E_grav))) {
+    return 0;
+  }
+
+  /* Bound state criterion */
+  if ((sink_props->sink_formation_bound_state_criterion) && (E_tot >= 0)) {
+    return 0;
+  }
+
+  /* Minimum of the potential criterion */
+  /* Done in density loop. The gas is then flagged through
+     sink_data.can_form_sink to not form sink. The check is done at the
+     beginning. */
+
+  /* Overlapping existing sinks criterion */
+  if (sink_props->sink_formation_overlapping_sink_criterion &&
+      sink_data->is_overlapping_sink) {
+    return 0;
+  }
+
+  message("Gas particle %lld can form a sink !", p->id);
+  return 1;
 }
 
 /**
@@ -202,16 +557,17 @@ INLINE static int sink_should_convert_to_sink(
  * @brief Copies the properties of the gas particle over to the
  * sink particle.
  *
- * Nothing to do here.
- *
- * @param e The #engine
- * @param p the gas particles.
- * @param xp the additional properties of the gas particles.
- * @param sink the new created sink  particle with its properties.
- * @param sink_props the sink properties to use.
- * @param phys_const the physical constants in internal units.
+ * @param p The gas particles.
+ * @param xp The additional properties of the gas particles.
+ * @param sink the new created #sink particle.
+ * @param e The #engine.
+ * @param sink_props The sink properties to use.
  * @param cosmo the cosmological parameters and properties.
  * @param with_cosmology if we run with cosmology.
+ * @param phys_const The physical constants in internal units.
+ * @param hydro_props The hydro properties to use.
+ * @param us The internal unit system.
+ * @param cooling The cooling function to use.
  */
 INLINE static void sink_copy_properties(
     const struct part* p, const struct xpart* xp, struct sink* sink,
@@ -225,8 +581,39 @@ INLINE static void sink_copy_properties(
   /* First initialisation */
   sink_init_sink(sink);
 
+  /* Set a smoothing length */
+  if (sink_props->use_fixed_r_cut) {
+    sink->h = sink_props->cut_off_radius / kernel_gamma;
+  } else {
+    sink->h = p->h;
+  }
+
   /* Flag it as not swallowed */
   sink_mark_sink_as_not_swallowed(&sink->merger_data);
+
+  /* Additional initialisation */
+  sink->number_of_gas_swallows = 0;
+  sink->number_of_direct_gas_swallows = 0;
+  sink->number_of_sink_swallows = 0;
+  sink->number_of_direct_sink_swallows = 0;
+  sink->swallowed_angular_momentum[0] = 0.f;
+  sink->swallowed_angular_momentum[1] = 0.f;
+  sink->swallowed_angular_momentum[2] = 0.f;
+  sink->n_stars = 0;
+  sink->has_IMF_changed_from_popIII_to_popII = 0;
+
+  /* setup the target mass for sink star formation */
+  sink_update_target_mass(sink, sink_props, e, 0);
+
+  /* Copy the chemistry properties */
+  chemistry_copy_sink_properties(p, xp, sink);
+
+  /* Note, we do not need to update sp->mass_tot_before_star_spawning because
+     it is performed within the 'sink_init_sink()' function. */
+
+  /* Set the birth time of the sink */
+  sink_set_sink_birth_time_or_scale_factor(sink, e->time, cosmo->a,
+                                           with_cosmology);
 }
 
 /**
@@ -246,27 +633,46 @@ __attribute__((always_inline)) INLINE static void sink_swallow_part(
   const float gas_mass = hydro_get_mass(p);
   const float sink_mass = sp->mass;
 
+  /* store the mass of the sink part i */
+  const float msp_old = sp->mass;
+
   /* Increase the dynamical mass of the sink. */
   sp->mass += gas_mass;
   sp->gpart->mass += gas_mass;
 
-  /* Physical velocity difference between the particles */
-  const float dv[3] = {(sp->v[0] - p->v[0]) * cosmo->a_inv,
-                       (sp->v[1] - p->v[1]) * cosmo->a_inv,
-                       (sp->v[2] - p->v[2]) * cosmo->a_inv};
+  /* Comoving and physical distance between the particles */
+  const float dx[3] = {sp->x[0] - p->x[0], sp->x[1] - p->x[1],
+                       sp->x[2] - p->x[2]};
+  const float dx_physical[3] = {dx[0] * cosmo->a, dx[1] * cosmo->a,
+                                dx[2] * cosmo->a};
+
+  /* Relative velocity between the sink and the part */
+  const float dv[3] = {sp->v[0] - p->v[0], sp->v[1] - p->v[1],
+                       sp->v[2] - p->v[2]};
+
+  const float a = cosmo->a;
+  const float H = cosmo->H;
+  const float a2H = a * a * H;
 
-  /* Physical distance between the particles */
-  const float dx[3] = {(sp->x[0] - p->x[0]) * cosmo->a,
-                       (sp->x[1] - p->x[1]) * cosmo->a,
-                       (sp->x[2] - p->x[2]) * cosmo->a};
+  /* Calculate the velocity with the Hubble flow */
+  const float v_plus_H_flow[3] = {a2H * dx[0] + dv[0], a2H * dx[1] + dv[1],
+                                  a2H * dx[2] + dv[2]};
+
+  /* Compute the physical relative velocity between the particles */
+  const float dv_physical[3] = {v_plus_H_flow[0] * cosmo->a_inv,
+                                v_plus_H_flow[1] * cosmo->a_inv,
+                                v_plus_H_flow[2] * cosmo->a_inv};
 
   /* Collect the swallowed angular momentum */
   sp->swallowed_angular_momentum[0] +=
-      gas_mass * (dx[1] * dv[2] - dx[2] * dv[1]);
+      gas_mass *
+      (dx_physical[1] * dv_physical[2] - dx_physical[2] * dv_physical[1]);
   sp->swallowed_angular_momentum[1] +=
-      gas_mass * (dx[2] * dv[0] - dx[0] * dv[2]);
+      gas_mass *
+      (dx_physical[2] * dv_physical[0] - dx_physical[0] * dv_physical[2]);
   sp->swallowed_angular_momentum[2] +=
-      gas_mass * (dx[0] * dv[1] - dx[1] * dv[0]);
+      gas_mass *
+      (dx_physical[0] * dv_physical[1] - dx_physical[1] * dv_physical[0]);
 
   /* Update the sink momentum */
   const float sink_mom[3] = {sink_mass * sp->v[0] + gas_mass * p->v[0],
@@ -280,25 +686,27 @@ __attribute__((always_inline)) INLINE static void sink_swallow_part(
   sp->gpart->v_full[1] = sp->v[1];
   sp->gpart->v_full[2] = sp->v[2];
 
-  /*
+  /* Update the sink metal masses fraction */
+  chemistry_add_part_to_sink(sp, p, msp_old);
+
+  /* This sink swallowed a gas particle */
+  sp->number_of_gas_swallows++;
+  sp->number_of_direct_gas_swallows++;
+
+  /* Update the total mass before star spawning */
+  sp->mass_tot_before_star_spawning = sp->mass;
+
+#ifdef SWIFT_DEBUG_CHECKS
   const float dr = sqrt(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]);
   message(
-      "sink %lld swallowing gas particle %lld "
-      "(Delta_v = [%f, %f, %f] U_V, "
+      "sink %lld swallow gas particle %lld. "
+      "(Mass = %e, "
+      "Delta_v = [%f, %f, %f] U_V, "
       "Delta_x = [%f, %f, %f] U_L, "
       "Delta_v_rad = %f)",
-      sp->id, p->id, -dv[0], -dv[1], -dv[2], -dx[0], -dx[1], -dx[2],
+      sp->id, p->id, sp->mass, -dv[0], -dv[1], -dv[2], -dx[0], -dx[1], -dx[2],
       (dv[0] * dx[0] + dv[1] * dx[1] + dv[2] * dx[2]) / dr);
-  */
-
-  /* Update the sink metal masses */
-  struct chemistry_sink_data* sp_chem = &sp->chemistry_data;
-  const struct chemistry_part_data* p_chem = &p->chemistry_data;
-  chemistry_add_part_to_sink(sp_chem, p_chem, gas_mass);
-
-  /* This sink swallowed a gas particle */
-  sp->number_of_gas_swallows++;
-  sp->number_of_direct_gas_swallows++;
+#endif
 }
 
 /**
@@ -316,6 +724,9 @@ __attribute__((always_inline)) INLINE static void sink_swallow_sink(
   const float spi_dyn_mass = spi->mass;
   const float spj_dyn_mass = spj->mass;
 
+  /* store the mass of the sink part i */
+  const float mi_old = spi->mass;
+
   /* Increase the masses of the sink. */
   spi->mass += spj->mass;
   spi->gpart->mass += spj->mass;
@@ -338,27 +749,39 @@ __attribute__((always_inline)) INLINE static void sink_swallow_sink(
   spi->gpart->v_full[1] = spi->v[1];
   spi->gpart->v_full[2] = spi->v[2];
 
-  /* Update the sink metal masses */
-  struct chemistry_sink_data* spi_chem = &spi->chemistry_data;
-  const struct chemistry_sink_data* spj_chem = &spj->chemistry_data;
-  chemistry_add_sink_to_sink(spi_chem, spj_chem);
+  /* Update the sink metal masses fraction */
+  chemistry_add_sink_to_sink(spi, spj, mi_old);
 
   /* This sink swallowed a sink particle */
   spi->number_of_sink_swallows++;
   spi->number_of_direct_sink_swallows++;
+
+  /* Add all other swallowed particles swallowed by the swallowed sink */
+  spi->number_of_sink_swallows += spj->number_of_sink_swallows;
+  spi->number_of_gas_swallows += spj->number_of_gas_swallows;
+
+  /* Add the stars spawned by the swallowed sink */
+  spi->n_stars += spj->n_stars;
+
+  /* Update masses */
+  spi->mass_tot_before_star_spawning = spi->mass;
+  spi->to_collect.mass_eligible_swallow +=
+      spj->to_collect.mass_eligible_swallow;
+  spi->to_collect.mass_swallowed += spj->to_collect.mass_swallowed;
+
+  message("sink %lld swallows sink particle %lld. New mass: %e.", spi->id,
+          spj->id, spi->mass);
 }
 
 /**
  * @brief Should the sink spawn a star particle?
  *
- * Nothing to do here.
- *
- * @param e The #engine
  * @param sink the sink particle.
- * @param sink_props the sink properties to use.
- * @param phys_const the physical constants in internal units.
- * @param cosmo the cosmological parameters and properties.
- * @param with_cosmology if we run with cosmology.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param cosmo The cosmological parameters and properties.
+ * @param with_cosmology If we run with cosmology.
+ * @param phys_const The physical constants in internal units.
  * @param us The internal unit system.
  */
 INLINE static int sink_spawn_star(struct sink* sink, const struct engine* e,
@@ -367,35 +790,136 @@ INLINE static int sink_spawn_star(struct sink* sink, const struct engine* e,
                                   const int with_cosmology,
                                   const struct phys_const* phys_const,
                                   const struct unit_system* restrict us) {
+  /* Convenient variables in internal units */
+  const float target_mass =
+      sink->target_mass_Msun * phys_const->const_solar_mass;
+  const float minimal_mass =
+      sink_props->sink_minimal_mass_Msun * phys_const->const_solar_mass;
 
-  const float random_number = random_unit_interval(
-      sink->id, e->ti_current, random_number_star_formation);
-  // return random_number < 1;  //1e-3;
-
-  if (sink->n_stars > 0) {
-    if (random_number < 1e-2) {
-      // sink->n_stars--;
-      // message("%lld spawn a star : n_star is now %d",sink->id,sink->n_stars);
-      return 0;
-    } else
-      return 0;
-  } else
+  /* To spawn a star, the sink must:
+     1) m_sink > target_mass,
+     2) and m_sink - target_mass >= minimal_sink_mass mass.
+     The second condition is relevant for low resolution simulations, where a
+     new born sink can already spawn stars. After spawning the stars, the sink
+     has a mass << gas mass and can get kicked away by gravitational
+     interactions. Also, if the sink's mass << gas' mass, the gas is never bound
+     to the sink and is thus never accreted. */
+  if (sink->mass > target_mass && (sink->mass - target_mass >= minimal_mass))
+    return 1;
+  else
     return 0;
 }
 
 /**
- * @brief Copy the properties of the sink particle towards the new star.
- * This function also needs to update the sink particle.
+ * @brief Give the #spart a new position.
  *
- * Nothing to do here.
+ * In GEAR: Positions are set by randomly sampling coordinates in an homogeneous
+ * sphere centered on the #sink with radius the sink's r_cut.
  *
- * @param e The #engine
- * @param sink the sink particle.
+ * @param e The #engine.
+ * @param si The #sink generating a star.
+ * @param sp The #spart generated.
+ */
+INLINE static void sink_star_formation_give_new_position(const struct engine* e,
+                                                         struct sink* si,
+                                                         struct spart* sp) {
+#ifdef SWIFT_DEBUG_CHECKS
+  if (si->x[0] != sp->x[0] || si->x[1] != sp->x[1] || si->x[2] != sp->x[2]) {
+    error(
+        "Moving particles that are not at the same location."
+        " (%g, %g, %g) - (%g, %g, %g)",
+        si->x[0], si->x[1], si->x[2], sp->x[0], sp->x[1], sp->x[2]);
+  }
+#endif
+
+  /* Put the star randomly within the accretion radius of the sink */
+  const double phi =
+      2 * M_PI *
+      random_unit_interval(sp->id, e->ti_current, (enum random_number_type)3);
+  const float rmax = si->h * kernel_gamma;
+  const double r = rmax * random_unit_interval(sp->id, e->ti_current,
+                                               (enum random_number_type)4);
+  const double cos_theta =
+      1.0 - 2.0 * random_unit_interval(sp->id, e->ti_current,
+                                       (enum random_number_type)5);
+  const double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
+
+  double new_pos[3] = {r * sin_theta * cos(phi), r * sin_theta * sin(phi),
+                       r * cos_theta};
+
+  /* Assign this new position to the star and its gpart */
+  sp->x[0] += new_pos[0];
+  sp->x[1] += new_pos[1];
+  sp->x[2] += new_pos[2];
+  sp->gpart->x[0] = sp->x[0];
+  sp->gpart->x[1] = sp->x[1];
+  sp->gpart->x[2] = sp->x[2];
+}
+
+/**
+ * @brief Give a velocity to the #spart.
+ *
+ * In GEAR: Currently, a gaussian centered on 0 is used. The standard deviation
+ * is computed based on the local gravitational dynamics of the system.
+ *
+ * @param e The #engine.
+ * @param si The #sink generating a star.
+ * @param sp The new #spart.
+ * @param sink_props The sink properties to use.
+ */
+INLINE static void sink_star_formation_give_new_velocity(
+    const struct engine* e, struct sink* si, struct spart* sp,
+    const struct sink_props* sink_props) {
+
+#ifdef HAVE_LIBGSL
+  /* Those intermediate variables are the values that will be given to the star
+     and subtracted from the sink. */
+  double v_given[3] = {0.0, 0.0, 0.0};
+  const double G_newton = e->physical_constants->const_newton_G;
+  const float rmax = si->h * kernel_gamma;
+  const double sigma_2 = G_newton * si->mass_tot_before_star_spawning / rmax;
+  const double sigma = sink_props->star_spawning_sigma_factor * sqrt(sigma_2);
+
+  for (int i = 0; i < 3; ++i) {
+
+    /* Draw a random value in unform interval (0, 1] */
+    const double random_number = random_unit_interval_part_ID_and_index(
+        sp->id, i, e->ti_current, (enum random_number_type)1);
+
+    /* Sample a gaussian with mu=0 and sigma=sigma */
+    double v_i_random = gsl_cdf_gaussian_Pinv(random_number, sigma);
+    v_given[i] = v_i_random;
+  }
+
+  /* Update the star velocity. Do not forget to update the gpart velocity */
+  sp->v[0] = si->v[0] + v_given[0];
+  sp->v[1] = si->v[1] + v_given[1];
+  sp->v[2] = si->v[2] + v_given[2];
+  sp->gpart->v_full[0] = sp->v[0];
+  sp->gpart->v_full[1] = sp->v[1];
+  sp->gpart->v_full[2] = sp->v[2];
+  message(
+      "New star velocity: v = (%lf %lf %lf). Sink velocity: v = (%lf %lf %lf). "
+      "Sigma = %lf",
+      sp->v[0], sp->v[1], sp->v[2], si->v[0], si->v[1], si->v[2], sigma);
+#else
+  error("Code not compiled with GSL. Can't compute Star new velocity.");
+#endif
+}
+
+/**
+ * @brief Copy the properties of the sink particle towards the new star. Also,
+ * give the stars some properties such as position and velocity.
+ *
+ * This function also needs to update the sink particle.
+ *
+ * @param sink The #sink particle.
  * @param sp The star particle.
- * @param sink_props the sink properties to use.
- * @param phys_const the physical constants in internal units.
- * @param cosmo the cosmological parameters and properties.
- * @param with_cosmology if we run with cosmology.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param cosmo The cosmological parameters and properties.
+ * @param with_cosmology If we run with cosmology.
+ * @param phys_const The physical constants in internal units.
  * @param us The internal unit system.
  */
 INLINE static void sink_copy_properties_to_star(
@@ -404,7 +928,342 @@ INLINE static void sink_copy_properties_to_star(
     const int with_cosmology, const struct phys_const* phys_const,
     const struct unit_system* restrict us) {
 
-  sp->h = sink->r_cut;
+  /* Give the stars a new position */
+  sink_star_formation_give_new_position(e, sink, sp);
+
+  /* Set the mass (do not forget the sink's gpart friend!) */
+  sp->mass = sink->target_mass_Msun * phys_const->const_solar_mass;
+  sp->gpart->mass = sp->mass;
+
+  /* Give a new velocity to the stars */
+  sink_star_formation_give_new_velocity(e, sink, sp, sink_props);
+
+  /* Sph smoothing length */
+  sp->h = sink->h;
+
+  /* Feedback related initialisation */
+  /* ------------------------------- */
+
+  /* Initialize the feedback */
+  feedback_init_after_star_formation(sp, e->feedback_props, sink->target_type);
+
+  /* Star formation related initalisation */
+  /* ------------------------------------ */
+
+  /* Note: The sink module need to be compiled with GEAR SF as we store data
+     in the SF struct. However, we do not need to run with --star-formation */
+
+  /* Mass at birth */
+  star_formation_set_spart_birth_mass(sp, sp->mass);
+
+  /* Store either the birth_scale_factor or birth_time */
+  star_formation_set_spart_birth_time_or_scale_factor(sp, e->time, cosmo->a,
+                                                      with_cosmology);
+
+  /* Copy the progenitor id */
+  star_formation_set_spart_progenitor_id(sp, sink->id);
+
+  /* Copy the chemistry properties */
+  /* ----------------------------- */
+
+  chemistry_copy_sink_properties_to_star(sink, sp);
+}
+
+/**
+ * @brief Update the #sink particle properties before spawning a star.
+ *
+ * In GEAR, we check if the sink had an IMF change from pop III to pop II
+ * during the last gas/sink accretion loops. If so, we draw a new target mass
+ * with the correct IMF so that stars have the right metallicities.
+ *
+ * @param sink The #sink particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param phys_const The physical constants in internal units.
+ */
+INLINE static void sink_update_sink_properties_before_star_formation(
+    struct sink* sink, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const) {
+
+  /* Has the sink accumulated enough metallicity so that the target mass
+     should be updated before spawning stars?
+     Between the last update of the target_mass_Msun, the sink may have accreted
+     gas with metallicities that that are higher than those of population III
+     stars. However, the target mass was set with the pop
+     III IMF. */
+
+  const struct feedback_props* feedback_props = e->feedback_props;
+
+  /* Pick the correct table. (if only one table, threshold is < 0) */
+  const float metal =
+      chemistry_get_sink_total_iron_mass_fraction_for_feedback(sink);
+  const float threshold = feedback_props->metallicity_max_first_stars;
+
+  /* If metal < threshold, then the sink generate first star particles. */
+  const int is_first_star = metal < threshold;
+
+  /* If the sink has not changed its IMF yet
+     (has_IMF_changed_from_popIII_to_popII = 0)
+     but is eligible to (sink metal > threshold), get a target_mass_Msun of the
+     pop II stars. */
+  if (!(sink->has_IMF_changed_from_popIII_to_popII) && !is_first_star) {
+    sink_update_target_mass(sink, sink_props, e, 0);
+
+    /* Flag the sink to have made the transition of IMF. This ensures that next
+    time we do not update the target_mass_Msun because metal > threshold
+    (otherwise we would update it without needing to) */
+    sink->has_IMF_changed_from_popIII_to_popII = 1;
+    message("IMF transition : Sink %lld will now spawn Pop II stars.",
+            sink->id);
+  }
+}
+
+/**
+ * @brief Update the #sink particle properties right after spawning a star.
+ *
+ * In GEAR: Important properties that are updated are the sink mass and the
+ * sink->target_mass_Msun to draw the next star mass.
+ *
+ * @param sink The #sink particle that spawed stars.
+ * @param sp The #spart particle spawned.
+ * @param e The #engine
+ * @param sink_props the sink properties to use.
+ * @param phys_const the physical constants in internal units.
+ * @param star_counter The star loop counter.
+ */
+INLINE static void sink_update_sink_properties_during_star_formation(
+    struct sink* sink, const struct spart* sp, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const,
+    int star_counter) {
+
+  /* count the number of stars spawned by this particle */
+  sink->n_stars++;
+
+  /* Update the mass */
+  sink->mass =
+      sink->mass - sink->target_mass_Msun * phys_const->const_solar_mass;
+
+  /* Bug fix: Do not forget to update the sink gpart's mass. */
+  sink->gpart->mass = sink->mass;
+
+  /* This message must be put carefully after giving the star its mass,
+     updated the sink mass and before changing the target_type */
+  message(
+      "%010lld spawn a star (%010lld) with mass %8.2f Msol type=%d  "
+      "star_counter=%03d. Sink remaining mass: %e Msol.",
+      sink->id, sp->id, sp->mass / phys_const->const_solar_mass,
+      sink->target_type, star_counter,
+      sink->mass / phys_const->const_solar_mass);
+
+  /* Sample the IMF to the get next target mass */
+  sink_update_target_mass(sink, sink_props, e, star_counter);
+}
+
+/**
+ * @brief Update the #sink particle properties after star formation.
+ *
+ * In GEAR, this is unused.
+ *
+ * @param sink The #sink particle.
+ * @param e The #engine
+ * @param sink_props The sink properties to use.
+ * @param phys_const The physical constants in internal units.
+ */
+INLINE static void sink_update_sink_properties_after_star_formation(
+    struct sink* sink, const struct engine* e,
+    const struct sink_props* sink_props, const struct phys_const* phys_const) {}
+
+/**
+ * @brief Store the gravitational potential of a particle by copying it from
+ * its #gpart friend.
+ *
+ * @param p_data The sink data of a gas particle.
+ * @param gp The part's #gpart.
+ */
+__attribute__((always_inline)) INLINE static void sink_store_potential_in_part(
+    struct sink_part_data* p_data, const struct gpart* gp) {
+  p_data->potential = gp->potential;
+}
+
+/**
+ * @brief Compute all quantities required for the formation of a sink such as
+ * kinetic energy, potential energy, etc. This function works on the
+ * neighbouring gas particles.
+ *
+ * @param e The #engine.
+ * @param p The #part for which we compute the quantities.
+ * @param xp The #xpart data of the particle #p.
+ * @param pi A neighbouring #part of #p.
+ * @param xpi The #xpart data of the particle #pi.
+ * @param cosmo The cosmological parameters and properties.
+ * @param sink_props The sink properties to use.
+ */
+INLINE static void sink_prepare_part_sink_formation_gas_criteria(
+    struct engine* e, struct part* restrict p, struct xpart* restrict xp,
+    struct part* restrict pi, struct xpart* restrict xpi,
+    const struct cosmology* cosmo, const struct sink_props* sink_props) {
+
+  /* If for some reason the particle has been flagged to not form sink,
+     do not continue and save some computationnal ressources. */
+  if (!p->sink_data.can_form_sink) {
+    return;
+  }
+
+  const int with_self_grav = (e->policy & engine_policy_self_gravity);
+
+  /* Physical accretion radius of part p */
+  const float r_acc_p = sink_props->cut_off_radius * cosmo->a;
+
+  /* Comoving distance of particl p */
+  const float px[3] = {(float)(p->x[0]), (float)(p->x[1]), (float)(p->x[2])};
+
+  /* No need to check if the particle has been flagged to form a sink or
+     not. This is done in runner_prepare_part_sink_formation(). */
+
+  /* Compute the pairwise physical distance */
+  const float pix[3] = {(float)(pi->x[0]), (float)(pi->x[1]),
+                        (float)(pi->x[2])};
+
+  const float dx[3] = {px[0] - pix[0], px[1] - pix[1], px[2] - pix[2]};
+  const float dx_physical[3] = {dx[0] * cosmo->a, dx[1] * cosmo->a,
+                                dx[2] * cosmo->a};
+  const float r2_physical = dx_physical[0] * dx_physical[0] +
+                            dx_physical[1] * dx_physical[1] +
+                            dx_physical[2] * dx_physical[2];
+
+  /* Checks that this part is a neighbour */
+  if ((r2_physical > r_acc_p * r_acc_p) || (r2_physical == 0.0)) {
+    return;
+  }
+
+  /* Do not form sinks if some neighbours are not active */
+  if (!part_is_active(pi, e)) {
+    p->sink_data.can_form_sink = 0;
+    return;
+  }
+
+  const float mi = hydro_get_mass(p);
+  const float u_inter_i = hydro_get_drifted_physical_internal_energy(p, cosmo);
+
+  /* Compute the relative comoving velocity between p and pi */
+  const float dv[3] = {pi->v[0] - p->v[0], pi->v[1] - p->v[1],
+                       pi->v[2] - p->v[2]};
+
+  const float a = cosmo->a;
+  const float H = cosmo->H;
+  const float a2H = a * a * H;
+
+  /* Calculate the velocity with the Hubble flow */
+  const float v_plus_H_flow[3] = {a2H * dx[0] + dv[0], a2H * dx[1] + dv[1],
+                                  a2H * dx[2] + dv[2]};
+
+  /* Compute the physical relative velocity between the particles */
+  const float dv_physical[3] = {v_plus_H_flow[0] * cosmo->a_inv,
+                                v_plus_H_flow[1] * cosmo->a_inv,
+                                v_plus_H_flow[2] * cosmo->a_inv};
+
+  const float dv_physical_squared = dv_physical[0] * dv_physical[0] +
+                                    dv_physical[1] * dv_physical[1] +
+                                    dv_physical[2] * dv_physical[2];
+
+  /* Compute specific physical angular momentum between pk and pi */
+  const float specific_angular_momentum[3] = {
+      dx_physical[1] * dv_physical[2] - dx_physical[2] * dv_physical[1],
+      dx_physical[2] * dv_physical[0] - dx_physical[0] * dv_physical[2],
+      dx_physical[0] * dv_physical[1] - dx_physical[1] * dv_physical[0]};
+
+  /* Updates the energies */
+  p->sink_data.E_kin_neighbours += 0.5f * mi * dv_physical_squared;
+  p->sink_data.E_int_neighbours += mi * u_inter_i;
+  p->sink_data.E_rad_neighbours += cooling_get_radiated_energy(xpi);
+
+  /* Notice that we skip the potential of the current particle here
+     instead of subtracting it later */
+  if ((with_self_grav) && (pi != p))
+    p->sink_data.E_pot_self_neighbours +=
+        0.5 * mi * pi->sink_data.potential * cosmo->a_inv;
+
+  /* No external potential for now */
+  /* if (gpi != NULL && with_ext_grav)	 */
+  /* p->sink_data.E_pot_ext_neighbours +=  mi *
+   * external_gravity_get_potential_energy( */
+  /* time, potential, phys_const, gpi); */
+
+  /* Need to include mhd header */
+  /* p->sink_data.E_mag_neighbours += mhd_get_magnetic_energy(p, xpi); */
+
+  /* Compute rotation energies per component */
+  p->sink_data.E_rot_neighbours[0] +=
+      0.5 * mi * specific_angular_momentum[0] * specific_angular_momentum[0] /
+      sqrtf(dx_physical[1] * dx_physical[1] + dx_physical[2] * dx_physical[2]);
+  p->sink_data.E_rot_neighbours[1] +=
+      0.5 * mi * specific_angular_momentum[1] * specific_angular_momentum[1] /
+      sqrtf(dx_physical[0] * dx_physical[0] + dx_physical[2] * dx_physical[2]);
+  p->sink_data.E_rot_neighbours[2] +=
+      0.5 * mi * specific_angular_momentum[2] * specific_angular_momentum[2] /
+      sqrtf(dx_physical[0] * dx_physical[0] + dx_physical[1] * dx_physical[1]);
+
+  /* Shall we reset the values of the energies for the next timestep? No, it is
+     done in cell_drift.c and space_init.c, for active particles. The
+     potential is set in runner_others.c->runner_do_end_grav_force() */
+}
+
+/**
+ * @brief Compute all quantities required for the formation of a sink. This
+ * function works on the neighbouring sink particles.
+ *
+ * @param e The #engine.
+ * @param p The #part for which we compute the quantities.
+ * @param xp The #xpart data of the particle #p.
+ * @param si A neighbouring #sink of #p.
+ * @param cosmo The cosmological parameters and properties.
+ * @param sink_props The sink properties to use.
+ */
+INLINE static void sink_prepare_part_sink_formation_sink_criteria(
+    struct engine* e, struct part* restrict p, struct xpart* restrict xp,
+    struct sink* restrict si, const int with_cosmology,
+    const struct cosmology* cosmo, const struct sink_props* sink_props,
+    const double time) {
+
+  /* Do not continue if the gas cannot form sink for any reason */
+  if (!p->sink_data.can_form_sink) {
+    return;
+  }
+
+  /* Determine if the sink is dead, i.e. if its age is bigger than the
+     age_threshold_unlimited */
+  const int sink_age = sink_get_sink_age(si, with_cosmology, cosmo, time);
+  char is_dead = sink_age > sink_props->age_threshold_unlimited;
+
+  /* If the sink is dead, do not check the criteria for the si - p pair. */
+  if (is_dead) {
+    return;
+  }
+
+  /* Physical accretion radius of part p */
+  const float r_acc_p = sink_props->cut_off_radius * cosmo->a;
+
+  /* Physical accretion radius of sink si */
+  const float rmax = si->h * kernel_gamma;
+  const float r_acc_si = rmax * cosmo->a;
+
+  /* Comoving distance of particl p */
+  const float px[3] = {(float)(p->x[0]), (float)(p->x[1]), (float)(p->x[2])};
+
+  /* Compute the pairwise physical distance */
+  const float six[3] = {(float)(si->x[0]), (float)(si->x[1]),
+                        (float)(si->x[2])};
+
+  const float dx[3] = {(px[0] - six[0]) * cosmo->a, (px[1] - six[1]) * cosmo->a,
+                       (px[2] - six[2]) * cosmo->a};
+  const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
+
+  /* If forming a sink from this particle will create a sink overlapping an
+     existing sink's accretion radius, do not form a sink. This criterion can
+     be disabled. */
+  if (r2 < (r_acc_si + r_acc_p) * (r_acc_si + r_acc_p)) {
+    p->sink_data.is_overlapping_sink = 1;
+  }
 }
 
 #endif /* SWIFT_GEAR_SINK_H */
diff --git a/src/sink/GEAR/sink_getters.h b/src/sink/GEAR/sink_getters.h
new file mode 100644
index 0000000000000000000000000000000000000000..4612555b4a70816cbc8d5b657588bf4ae438023e
--- /dev/null
+++ b/src/sink/GEAR/sink_getters.h
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ *******************************************************************************/
+#ifndef SWIFT_GEAR_SINK_GETTERS_H
+#define SWIFT_GEAR_SINK_GETTERS_H
+
+#include "cosmology.h"
+#include "sink_part.h"
+
+/**
+ * @file src/sink/GEAR/sink_getters.h
+ * @brief Getter functions for GEAR sink scheme to avoid exposing
+ * implementation details to the outer world. Keep the code clean and lean.
+ */
+
+/**
+ * @brief Get the sink age in interal units.
+ *
+ * @param sink The #sink.
+ * @param with_cosmology If we run with cosmology.
+ * @param cosmo The co Birth scale-factor of the star.
+ * @param with_cosmology If we run with cosmology.
+ */
+
+__attribute__((always_inline)) INLINE double sink_get_sink_age(
+    const struct sink* restrict sink, const int with_cosmology,
+    const struct cosmology* cosmo, const double time) {
+  double sink_age;
+  if (with_cosmology) {
+
+    /* Deal with rounding issues */
+    if (sink->birth_scale_factor >= cosmo->a) {
+      sink_age = 0.;
+    } else {
+      sink_age = cosmology_get_delta_time_from_scale_factors(
+          cosmo, sink->birth_scale_factor, cosmo->a);
+    }
+  } else {
+    sink_age = time - sink->birth_time;
+  }
+  return sink_age;
+}
+
+/**
+ * @brief Compute the rotational energy of the neighbouring gas particles.
+ *
+ * Note: This function must be used after having computed the rotational energy
+ * per components, i.e. after sink_prepare_part_sink_formation().
+ *
+ * @param p The gas particle.
+ *
+ */
+INLINE static double sink_compute_neighbour_rotation_energy_magnitude(
+    const struct part* restrict p) {
+  double E_rot_x = p->sink_data.E_rot_neighbours[0];
+  double E_rot_y = p->sink_data.E_rot_neighbours[1];
+  double E_rot_z = p->sink_data.E_rot_neighbours[2];
+  double E_rot =
+      sqrtf(E_rot_x * E_rot_x + E_rot_y * E_rot_y + E_rot_z * E_rot_z);
+  return E_rot;
+}
+
+/**
+ * @brief Retrieve the physical velocity divergence from the gas particle.
+ *
+ * @param p The gas particles.
+ *
+ */
+INLINE static float sink_get_physical_div_v_from_part(
+    const struct part* restrict p) {
+
+  float div_v = 0.0;
+
+  /* The implementation of div_v depends on the Hydro scheme. Furthermore, some
+     add a Hubble flow term, some do not. We need to take care of this */
+#ifdef SPHENIX_SPH
+  /* SPHENIX is already including the Hubble flow. */
+  div_v = hydro_get_div_v(p);
+#elif GADGET2_SPH
+  div_v = p->density.div_v;
+
+  /* Add the missing term */
+  div_v += hydro_dimension * cosmo->H;
+#elif MINIMAL_SPH
+  div_v = hydro_get_div_v(p);
+
+  /* Add the missing term */
+  div_v += hydro_dimension * cosmo->H;
+#elif GASOLINE_SPH
+  /* Copy the velocity divergence */
+  div_v = (1. / 3.) * (p->viscosity.velocity_gradient[0][0] +
+                       p->viscosity.velocity_gradient[1][1] +
+                       p->viscosity.velocity_gradient[2][2]);
+#elif HOPKINS_PU_SPH
+  div_v = p->density.div_v;
+#else
+#error \
+    "This scheme is not implemented. Note that Different scheme apply the Hubble flow in different places. Be careful about it."
+#endif
+  return div_v;
+}
+
+/**
+ * @brief Compute the angular momentum-based criterion for sink-sink
+ * interaction.
+ *
+ * This function calculates the angular momentum of a sink particle relative to
+ * another particle (sink or gas) and evaluates the Keplerian angular momentum.
+ *
+ * @param dx Comoving vector separating the two particles (pi - pj).
+ * @param dv_plus_H_flow Comoving relative velocity including the Hubble flow.
+ * @param r Comoving distance between the two particles.
+ * @param r_cut_i Comoving cut-off radius of particle i.
+ * @param mass_i Mass of particle i.
+ * @param cosmo The cosmological parameters and properties
+ * @param grav_props The gravity scheme parameters and properties
+ * @param L2_kepler (return) Keplerian angular momentum squared of particle i.
+ * @param L2_j (return) Specific angular momentum squared relative to particle
+ * j.
+ */
+__attribute__((always_inline)) INLINE static void
+sink_compute_angular_momenta_criterion(
+    const float dx[3], const float dv_plus_H_flow[3], const float r,
+    const float r_cut_i, const float mass_i, const struct cosmology* cosmo,
+    const struct gravity_props* grav_props, float* L2_kepler, float* L2_j) {
+
+  /* Compute the physical relative velocity between the particles */
+  const float dv_physical[3] = {dv_plus_H_flow[0] * cosmo->a_inv,
+                                dv_plus_H_flow[1] * cosmo->a_inv,
+                                dv_plus_H_flow[2] * cosmo->a_inv};
+
+  /* Compute the physical distance between the particles */
+  const float dx_physical[3] = {dx[0] * cosmo->a, dx[1] * cosmo->a,
+                                dx[2] * cosmo->a};
+  const float r_physical = r * cosmo->a;
+
+  /* Momentum check------------------------------------------------------- */
+  /* Relative momentum of the gas */
+  const float specific_angular_momentum[3] = {
+      dx_physical[1] * dv_physical[2] - dx_physical[2] * dv_physical[1],
+      dx_physical[2] * dv_physical[0] - dx_physical[0] * dv_physical[2],
+      dx_physical[0] * dv_physical[1] - dx_physical[1] * dv_physical[0]};
+
+  *L2_j = specific_angular_momentum[0] * specific_angular_momentum[0] +
+          specific_angular_momentum[1] * specific_angular_momentum[1] +
+          specific_angular_momentum[2] * specific_angular_momentum[2];
+
+  /* Keplerian angular speed squared */
+  const float omega_acc_2 =
+      grav_props->G_Newton * mass_i / (r_physical * r_physical * r_physical);
+
+  /*Keplerian angular momentum squared */
+  *L2_kepler = (r_cut_i * r_cut_i * r_cut_i * r_cut_i) * omega_acc_2;
+}
+
+#endif /* SWIFT_GEAR_SINK_GETTERS_H */
diff --git a/src/sink/GEAR/sink_iact.h b/src/sink/GEAR/sink_iact.h
index 137ab008e8bcd8dff2f02a41212c6c6632c0c918..d52d6b94f917df2bcab6440b8b3e80856ff9b385 100644
--- a/src/sink/GEAR/sink_iact.h
+++ b/src/sink/GEAR/sink_iact.h
@@ -1,6 +1,7 @@
 /*******************************************************************************
  * This file is part of SWIFT.
  * Copyright (c) 2021 Loic Hausammann (loic.hausammann@epfl.ch)
+ *               2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -19,10 +20,20 @@
 #ifndef SWIFT_GEAR_SINKS_IACT_H
 #define SWIFT_GEAR_SINKS_IACT_H
 
+/* Local includes */
+#include "gravity.h"
+#include "gravity_iact.h"
+#include "sink.h"
+#include "sink_getters.h"
+#include "sink_properties.h"
+
 /**
  * @brief do sink computation after the runner_iact_density (symmetric
  * version)
  *
+ * In GEAR: This function deactivates the sink formation ability of #part not
+ * at a potential minimum.
+ *
  * @param r2 Comoving square distance between the two particles.
  * @param dx Comoving vector separating both particles (pi - pj).
  * @param hi Comoving smoothing-length of particle i.
@@ -35,12 +46,40 @@
 __attribute__((always_inline)) INLINE static void runner_iact_sink(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, struct part *restrict pj, const float a,
-    const float H, const struct sink_props *sink_props) {}
+    const float H) {
+
+  /* In order to prevent the formation of two sink particles too close together,
+   * we keep only gas particles with the smallest potential. The distance at
+   * which to prevent sink formation is the cutoff radius if this is fixed, or
+   * it is the variable smoothing length times gamma. */
+
+  const float r = sqrtf(r2);
+  const float rmax = max(hi, hj) * kernel_gamma;
+
+  if (r < rmax) {
+    float potential_i = pi->sink_data.potential;
+    float potential_j = pj->sink_data.potential;
+
+    /* prevent the particle with the largest potential to form a sink */
+    if (potential_i > potential_j) {
+      pi->sink_data.can_form_sink = 0;
+      return;
+    }
+
+    if (potential_j > potential_i) {
+      pj->sink_data.can_form_sink = 0;
+      return;
+    }
+  }
+}
 
 /**
  * @brief do sink computation after the runner_iact_density (non symmetric
  * version)
  *
+ * In GEAR: This function deactivates the sink formation ability of #part not
+ * at a potential minimum.
+ *
  * @param r2 Comoving square distance between the two particles.
  * @param dx Comoving vector separating both particles (pi - pj).
  * @param hi Comoving smoothing-length of particle i.
@@ -53,18 +92,19 @@ __attribute__((always_inline)) INLINE static void runner_iact_sink(
 __attribute__((always_inline)) INLINE static void runner_iact_nonsym_sink(
     const float r2, const float dx[3], const float hi, const float hj,
     struct part *restrict pi, const struct part *restrict pj, const float a,
-    const float H, const struct sink_props *sink_props) {
+    const float H) {
 
-  /* In order to prevent the formation of two sink particles at a distance
-   * smaller than the sink cutoff radius, we keep only gas particles with
-   * the smallest potential. */
+  /* In order to prevent the formation of two sink particles too close together,
+   * we keep only gas particles with the smallest potential. The distance at
+   * which to prevent sink formation is the cutoff radius if this is fixed, or
+   * it is the variable smoothing length times gamma. */
 
   const float r = sqrtf(r2);
+  const float rmax = max(hi, hj) * kernel_gamma;
 
-  if (r < sink_props->cut_off_radius) {
-
-    float potential_i = pi->gpart->potential;
-    float potential_j = pj->gpart->potential;
+  if (r < rmax) {
+    float potential_i = pi->sink_data.potential;
+    float potential_j = pj->sink_data.potential;
 
     /* if the potential is larger
      * prevent the particle to form a sink */
@@ -72,46 +112,308 @@ __attribute__((always_inline)) INLINE static void runner_iact_nonsym_sink(
   }
 }
 
+/**
+ * @brief Density interaction between two particles (non-symmetric).
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing length of particle i.
+ * @param hj Comoving smoothing-length of particle j.
+ * @param si First particle (sink).
+ * @param pj Second particle (gas, not updated).
+ * @param with_cosmology Are we doing a cosmological run?
+ * @param cosmo The cosmological model.
+ * @param grav_props The properties of the gravity scheme (softening, G, ...).
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
+ */
+__attribute__((always_inline)) INLINE static void
+runner_iact_nonsym_sinks_gas_density(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *si, const struct part *pj, const int with_cosmology,
+    const struct cosmology *cosmo, const struct gravity_props *grav_props,
+    const struct sink_props *sink_props, const integertime_t ti_current,
+    const double time) {
+
+  /* Contribution to the number of neighbours in cutoff radius */
+  si->num_ngbs++;
+
+  float wi, wi_dx;
+
+  /* Compute the kernel function */
+  const float r = sqrtf(r2);
+  const float hi_inv = 1.0f / hi;
+  const float ui = r * hi_inv;
+  kernel_deval(ui, &wi, &wi_dx);
+
+  /* Neighbour gas mass */
+  const float mj = hydro_get_mass(pj);
+
+  /* Minimum smoothing length accros the neighbours */
+  /* AND the sink smoothing length */
+  si->to_collect.minimal_h_gas = min(hj, si->to_collect.minimal_h_gas);
+
+  /* Contribution to the BH gas density */
+  si->to_collect.rho_gas += mj * wi;
+
+  /* Contribution to the smoothed sound speed */
+  si->to_collect.sound_speed_gas += mj * wi * hydro_get_comoving_soundspeed(pj);
+
+  /* Neighbour's (drifted) velocity in the frame of the sink
+   * (we don't include a Hubble term since we are interested in the
+   * velocity contribution at the location of the sink) */
+  const float dv[3] = {pj->v[0] - si->v[0], pj->v[1] - si->v[1],
+                       pj->v[2] - si->v[2]};
+
+  /* Contribution to the smoothed velocity (gas w.r.t. black hole) */
+  si->to_collect.velocity_gas[0] += mj * dv[0] * wi;
+  si->to_collect.velocity_gas[1] += mj * dv[1] * wi;
+  si->to_collect.velocity_gas[2] += mj * dv[2] * wi;
+
+  /* Compute contribution to the number of neighbours */
+  si->density.wcount += wi;
+  si->density.wcount_dh -= (hydro_dimension * wi + ui * wi_dx);
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+  si->rho_check += hydro_get_mass(pj) * wi;
+  si->n_check += wi;
+  si->N_check_density++;
+#endif
+}
+
+/**
+ * @brief  Update the properties of a sink particles from its sink neighbours.
+ *
+ * Warning: No symmetric interaction for timesteps since we are in
+ * runner_iact_nonsym_sinks_sink_swallow() --> pay attention to not break MPI.
+ *
+ * @param r2 Comoving square distance between the two particles.
+ * @param dx Comoving vector separating both particles (pi - pj).
+ * @param hi Comoving smoothing length of particle i.
+ * @param hj Comoving smoothing length of particle j.
+ * @param si First sink particle.
+ * @param sj Second sink particle.
+ */
+__attribute__((always_inline)) INLINE static void
+sink_collect_properties_from_sink(const float r2, const float dx[3],
+                                  const float hi, const float hj,
+                                  struct sink *restrict si,
+                                  struct sink *restrict sj,
+                                  const struct gravity_props *grav_props) {
+
+  /* Neighbour's (drifted) velocity in the frame of the sink i
+   * (we don't include a Hubble term since we are interested in the
+   * velocity contribution at the location of the sink) */
+  const float dv[3] = {sj->v[0] - si->v[0], sj->v[1] - si->v[1],
+                       sj->v[2] - si->v[2]};
+  const float dv_norm = sqrtf(dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2]);
+
+  /* Get the gravitional softening */
+  const float eps = gravity_get_softening(si->gpart, grav_props);
+  const float eps2 = eps * eps;
+  const float eps_inv = 1.f / eps;
+  const float eps_inv3 = eps_inv * eps_inv * eps_inv;
+
+  /* Compute the kernel potential and force with mass = 1.0. We multiply by
+     the mass below if needed. */
+  float dphi_dr, pot;
+  runner_iact_grav_pp_full(r2, eps2, eps_inv, eps_inv3, 1.0, &dphi_dr, &pot);
+
+  /* From Grudic et al. (2021) eq 6, we replace the plummer functionnal form
+     sqrt(r^2 + eps^2) by the kernel 1.0/|phi(r,H=3*eps)| */
+  const float t_c = 1.0 / (fabsf(pot) * dv_norm);
+  si->to_collect.minimal_sink_t_c = min(t_c, si->to_collect.minimal_sink_t_c);
+
+  /* From Grudic et al. (2021) eq 7, we replace the plummer functionnal form
+     (r^2 + eps^2)^{3/2} by the kernel |(d phi(r,H=3*eps)/ dr)^{-1}| */
+  const float denominator = grav_props->G_Newton * (si->mass + sj->mass);
+  const float numerator = 1.0 / fabsf(dphi_dr);
+  const float t_dyn = sqrt(numerator / denominator);
+  si->to_collect.minimal_sink_t_dyn =
+      min(t_dyn, si->to_collect.minimal_sink_t_dyn);
+}
+
 /**
  * @brief Compute sink-sink swallow interaction (non-symmetric).
  *
+ * Note: Energies are computed with physical quantities, not the comoving ones.
+ *
+ * MPI note: This functions invokes the gpart. Hence, it must be performed only
+ * on the local node (similarly to runner_iact_nonsym_bh_bh_repos()).
+ *
  * @param r2 Comoving square distance between the two particles.
  * @param dx Comoving vector separating both particles (pi - pj).
- * @param ri Comoving cut off radius of particle i.
- * @param rj Comoving cut off radius of particle j.
+ * @param hi Comoving smoothing length of particle i.
+ * @param hj Comoving smoothing length of particle j.
  * @param si First sink particle.
  * @param sj Second sink particle.
+ * @param with_cosmology if we run with cosmology.
+ * @param cosmo The cosmological parameters and properties.
+ * @param grav_props The gravity scheme parameters and properties.
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_sinks_sink_swallow(const float r2, const float *dx,
-                                      const float ri, const float rj,
-                                      struct sink *restrict si,
-                                      struct sink *restrict sj) {
+runner_iact_nonsym_sinks_sink_swallow(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *restrict si, struct sink *restrict sj,
+    const int with_cosmology, const struct cosmology *cosmo,
+    const struct gravity_props *grav_props,
+    const struct sink_props *sink_properties, const integertime_t ti_current,
+    const double time) {
+
+  /* Convert the smoothing length back into a cutoff radius */
+  const float hig = hi * kernel_gamma;
 
-  /* See runner_iact_nonsym_bh_bh_swallow.
-   * The sink with the smaller mass will be merged onto the one with the
-   * larger mass.
-   * To avoid rounding issues, we additionally check for IDs if the sink
-   * have the exact same mass. */
+  const float r = sqrtf(r2);
+  const float f_acc_r_acc_i = sink_properties->f_acc * hig;
 
-  /* We should check the relative energy */
+  /* Determine if the sink is dead, i.e. if its age is bigger than the
+     age_threshold_unlimited */
+  const int si_age = sink_get_sink_age(si, with_cosmology, cosmo, time);
+  char si_is_dead = si_age > sink_properties->age_threshold_unlimited;
 
-  if ((sj->mass < si->mass) || (sj->mass == si->mass && sj->id < si->id)) {
+  const int sj_age = sink_get_sink_age(sj, with_cosmology, cosmo, time);
+  char sj_is_dead = sj_age > sink_properties->age_threshold_unlimited;
 
-    /* This particle is swallowed by the sink with the largest mass of all the
-     * candidates wanting to swallow it (we use IDs to break ties)*/
-    if ((sj->merger_data.swallow_mass < si->mass) ||
-        (sj->merger_data.swallow_mass == si->mass &&
-         sj->merger_data.swallow_id < si->id)) {
+  /* Collect the properties for 2-body interactions if one is sink is alive. If
+     they are both dead, we do not want to restrict the timesteps for 2-body
+     encounters since they won't merge. */
+  if (!si_is_dead || !sj_is_dead) {
+    sink_collect_properties_from_sink(r2, dx, hi, hj, si, sj, grav_props);
+  }
 
-      // message("sink %lld wants to swallow sink particle %lld", si->id,
-      // sj->id);
+  /* If si is dead, do not swallow sj. However, sj can swallow si if it alive.
+   */
+  if (si_is_dead) {
+    return;
+  }
 
-      sj->merger_data.swallow_id = si->id;
-      sj->merger_data.swallow_mass = si->mass;
+  /* If the sink j falls within f_acc*r_acc of sink i, then the
+     lightest is accreted on the most massive without further check.
+     Note that this is a non-symmetric interaction. So, we do not need to check
+     for the f_acc*r_acc_j case here. */
+  if (r < f_acc_r_acc_i) {
+    /* The sink with the smaller mass will be merged onto the one with the
+     * larger mass.
+     * To avoid rounding issues, we additionally check for IDs if the sink
+     * have the exact same mass. */
+    if ((sj->mass < si->mass) || (sj->mass == si->mass && sj->id < si->id)) {
+      /* This particle is swallowed by the sink with the largest mass of all the
+       * candidates wanting to swallow it (we use IDs to break ties)*/
+      if ((sj->merger_data.swallow_mass < si->mass) ||
+          (sj->merger_data.swallow_mass == si->mass &&
+           sj->merger_data.swallow_id < si->id)) {
+        sj->merger_data.swallow_id = si->id;
+        sj->merger_data.swallow_mass = si->mass;
+      }
     }
-  }
+  } else {
+
+    /* Relative velocity between the sinks */
+    const float dv[3] = {sj->v[0] - si->v[0], sj->v[1] - si->v[1],
+                         sj->v[2] - si->v[2]};
+
+    const float a = cosmo->a;
+    const float H = cosmo->H;
+    const float a2H = a * a * H;
+
+    /* Calculate the velocity with the Hubble flow */
+    const float v_plus_H_flow[3] = {a2H * dx[0] + dv[0], a2H * dx[1] + dv[1],
+                                    a2H * dx[2] + dv[2]};
+
+    /* Compute the physical relative velocity between the particles */
+    const float dv_physical[3] = {v_plus_H_flow[0] * cosmo->a_inv,
+                                  v_plus_H_flow[1] * cosmo->a_inv,
+                                  v_plus_H_flow[2] * cosmo->a_inv};
+
+    const float dv_physical_squared = dv_physical[0] * dv_physical[0] +
+                                      dv_physical[1] * dv_physical[1] +
+                                      dv_physical[2] * dv_physical[2];
+
+    /* Momentum check------------------------------------------------------- */
+    float L2_j = 0.0;      /* Relative momentum of the sink j */
+    float L2_kepler = 0.0; /* Keplerian angular momentum squared */
+    sink_compute_angular_momenta_criterion(
+        dx, v_plus_H_flow, r, si->h * kernel_gamma, si->mass, cosmo, grav_props,
+        &L2_kepler, &L2_j);
+
+    /* To be accreted, the sink momentum should lower than the keplerian orbit
+     * momentum. */
+    if (L2_j > L2_kepler) {
+      return;
+    }
+
+    /* Binding energy check------------------------------------------------- */
+    /* Kinetic energy per unit mass of the sink */
+    const float E_kin_rel = 0.5f * dv_physical_squared;
+
+    /* Compute the Newtonian or softened potential the sink exherts onto the
+       gas particle */
+    const float eps = gravity_get_softening(si->gpart, grav_props);
+    const float eps2 = eps * eps;
+    const float eps_inv = 1.f / eps;
+    const float eps_inv3 = eps_inv * eps_inv * eps_inv;
+    const float si_mass = si->mass;
+    const float sj_mass = sj->mass;
+
+    float dummy, pot_ij, pot_ji;
+    runner_iact_grav_pp_full(r2, eps2, eps_inv, eps_inv3, si_mass, &dummy,
+                             &pot_ij);
+    runner_iact_grav_pp_full(r2, eps2, eps_inv, eps_inv3, sj_mass, &dummy,
+                             &pot_ji);
 
+    /* Compute the physical potential energies per unit mass :
+                           E_pot_phys = G*pot_grav*a^(-1) + c(a).
+       The normalization is c(a) = 0. */
+    const float E_pot_ij = grav_props->G_Newton * pot_ij * cosmo->a_inv;
+    const float E_pot_ji = grav_props->G_Newton * pot_ji * cosmo->a_inv;
+
+    /* Mechanical energy per unit mass of the pair i-j and j-i */
+    const float E_mec_si = E_kin_rel + E_pot_ij;
+    const float E_mec_sj = E_kin_rel + E_pot_ji;
+
+    /* Now, check if one is bound to the other */
+    if ((E_mec_si > 0) || (E_mec_sj > 0)) {
+      return;
+    }
+
+    /* Swallowed mass threshold--------------------------------------------- */
+    si->to_collect.mass_eligible_swallow += sj->mass;
+
+    /* Maximal mass that can be swallowed within a single timestep */
+    const float mass_swallow_limit = sink_properties->n_IMF * si->mass_IMF;
+
+    /* If the mass exceeds the threshold, do not swallow. Make sure you can at
+       least swallow a particle to avoid running into the problem of never being
+       able to spawn a star.
+       If n_IMF <= 0, then disable this criterion */
+    if (sink_properties->n_IMF > 0 &&
+        si->to_collect.mass_swallowed >= mass_swallow_limit &&
+        si->to_collect.mass_eligible_swallow != 0) {
+      return;
+    }
+
+    /* Increment the swallowd mass */
+    si->to_collect.mass_swallowed += sj->mass;
+
+    /* The sink with the smaller mass will be merged onto the one with the
+     * larger mass.
+     * To avoid rounding issues, we additionally check for IDs if the sink
+     * have the exact same mass. */
+    if ((sj->mass < si->mass) || (sj->mass == si->mass && sj->id < si->id)) {
+      /* This particle is swallowed by the sink with the largest mass of all the
+       * candidates wanting to swallow it (we use IDs to break ties)*/
+      if ((sj->merger_data.swallow_mass < si->mass) ||
+          (sj->merger_data.swallow_mass == si->mass &&
+           sj->merger_data.swallow_id < si->id)) {
+        sj->merger_data.swallow_id = si->id;
+        sj->merger_data.swallow_mass = si->mass;
+      }
+    }
+  }
 #ifdef DEBUG_INTERACTIONS_SINKS
   /* Update ngb counters */
   if (si->num_ngb_formation < MAX_NUM_OF_NEIGHBOURS_SINKS)
@@ -125,28 +427,163 @@ runner_iact_nonsym_sinks_sink_swallow(const float r2, const float *dx,
 /**
  * @brief Compute sink-gas swallow interaction (non-symmetric).
  *
+ * Note: Energies are computed with physical quantities, not the comoving ones.
+ *
+ * MPI note: This functions invokes the gpart. Hence, it must be performed only
+ * on the local node (similarly to runner_iact_nonsym_bh_gas_repos()).
+ *
  * @param r2 Comoving square distance between the two particles.
  * @param dx Comoving vector separating both particles (pi - pj).
- * @param ri Comoving cut off radius of particle i.
+ * @param hi Comoving smoothing length of particle i.
  * @param hj Comoving smoothing-length of particle j.
  * @param si First sink particle.
  * @param pj Second particle.
+ * @param with_cosmology if we run with cosmology.
+ * @param cosmo The cosmological parameters and properties.
+ * @param grav_props The gravity scheme parameters and properties.
+ * @param sink_props the sink properties to use.
+ * @param ti_current Current integer time value (for random numbers).
+ * @param time current physical time in the simulation
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_sinks_gas_swallow(const float r2, const float *dx,
-                                     const float ri, const float hj,
-                                     struct sink *restrict si,
-                                     struct part *restrict pj) {
+runner_iact_nonsym_sinks_gas_swallow(
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct sink *restrict si, struct part *restrict pj,
+    const int with_cosmology, const struct cosmology *cosmo,
+    const struct gravity_props *grav_props,
+    const struct sink_props *sink_properties, const integertime_t ti_current,
+    const double time) {
 
-  /* See see runner_iact_nonsym_bh_gas_swallow.
-   * We first check if a gas particle has not been already marked to
-   * be swallowed by another sink particle. */
+  /* Convert the smoothing length back into a cutoff radius */
+  const float hig = hi * kernel_gamma;
 
-  /* We should check the relative energy */
+  const float r = sqrtf(r2);
+  const float f_acc_r_acc = sink_properties->f_acc * hig;
+
+  /* Determine if the sink is dead, i.e. if its age is bigger than the
+     age_threshold_unlimited */
+  const int sink_age = sink_get_sink_age(si, with_cosmology, cosmo, time);
+  char is_dead = sink_age > sink_properties->age_threshold_unlimited;
+
+  /* If si is dead, do not swallow pj. */
+  if (is_dead) {
+    return;
+  }
+
+  /* If the gas falls within f_acc*r_acc, it is accreted without further check
+   */
+  if (r < f_acc_r_acc) {
+    warning("Gas %lld within sink %lld inner accretion radius", pj->id, si->id);
+    /* Check if a gas particle has not been already marked to be swallowed by
+       another sink particle. */
+    if (pj->sink_data.swallow_id < si->id) {
+      pj->sink_data.swallow_id = si->id;
+    }
+
+    /* f_acc*r_acc <= r <= r_acc, we perform other checks */
+  } else if ((r >= f_acc_r_acc) && (r < hig)) {
+
+    /* Relative velocity between the sinks */
+    const float dv[3] = {pj->v[0] - si->v[0], pj->v[1] - si->v[1],
+                         pj->v[2] - si->v[2]};
+
+    const float a = cosmo->a;
+    const float H = cosmo->H;
+    const float a2H = a * a * H;
+
+    /* Calculate the velocity with the Hubble flow */
+    const float v_plus_H_flow[3] = {a2H * dx[0] + dv[0], a2H * dx[1] + dv[1],
+                                    a2H * dx[2] + dv[2]};
+
+    /* Compute the physical relative velocity between the particles */
+    const float dv_physical[3] = {v_plus_H_flow[0] * cosmo->a_inv,
+                                  v_plus_H_flow[1] * cosmo->a_inv,
+                                  v_plus_H_flow[2] * cosmo->a_inv};
+
+    const float dv_physical_squared = dv_physical[0] * dv_physical[0] +
+                                      dv_physical[1] * dv_physical[1] +
+                                      dv_physical[2] * dv_physical[2];
+
+    /* Momentum check------------------------------------------------------- */
+    float L2_gas_j = 0.0;  /* Relative momentum of the gas */
+    float L2_kepler = 0.0; /* Keplerian angular momentum squared */
+    sink_compute_angular_momenta_criterion(
+        dx, v_plus_H_flow, r, si->h * kernel_gamma, si->mass, cosmo, grav_props,
+        &L2_kepler, &L2_gas_j);
+
+    /* To be accreted, the gas momentum should lower than the keplerian orbit
+     * momentum. */
+    if (L2_gas_j > L2_kepler) {
+      return;
+    }
+
+    /* Energy check--------------------------------------------------------- */
+    /* Kinetic energy per unit mass of the gas */
+    float E_kin_relative_gas = 0.5f * dv_physical_squared;
+
+    /* Compute the Newtonian or softened potential the sink exherts onto the
+       gas particle */
+    const float eps = gravity_get_softening(si->gpart, grav_props);
+    const float eps2 = eps * eps;
+    const float eps_inv = 1.f / eps;
+    const float eps_inv3 = eps_inv * eps_inv * eps_inv;
+    const float sink_mass = si->mass;
+    float dummy, pot_ij;
+    runner_iact_grav_pp_full(r2, eps2, eps_inv, eps_inv3, sink_mass, &dummy,
+                             &pot_ij);
+
+    /* Compute the physical potential energy per unit mass  that the sink
+       exerts in the gas :
+                       E_pot_phys = G*pot_grav*a^(-1) + c(a).
+       The normalization is c(a) = 0. */
+    const float E_pot_gas = grav_props->G_Newton * pot_ij * cosmo->a_inv;
+
+    /* Update: Add thermal energy per unit mass  to avoid the sink to swallow
+       hot gas regions */
+    const float E_therm = hydro_get_drifted_physical_internal_energy(pj, cosmo);
+
+    /* Energy per unit mass of the pair sink-gas */
+    const float E_mec_sink_part = E_kin_relative_gas + E_pot_gas + E_therm;
+
+    /* To be accreted, the gas must be gravitationally bound to the sink. */
+    if (E_mec_sink_part >= 0) return;
+
+    /* To be accreted, the gas smoothing length must be smaller than the sink
+       smoothing length. This is similar to AMR codes requesting the maximum
+       refinement level close to the sink. */
+    if (sink_properties->sink_formation_smoothing_length_criterion &&
+        (pj->h >= si->h))
+      return;
+
+    /* Most bound pair check------------------------------------------------ */
+    /* The pair gas-sink must be the most bound among all sinks */
+    if (E_mec_sink_part >= pj->sink_data.E_mec_bound) {
+      return;
+    }
+
+    /* Swallowed mass threshold--------------------------------------------- */
+    si->to_collect.mass_eligible_swallow += hydro_get_mass(pj);
+
+    /* Maximal mass that can be swallowed within a single timestep */
+    const float mass_swallow_limit = sink_properties->n_IMF * si->mass_IMF;
+
+    /* If the mass exceeds the threshold, do not swallow. Make sure you can at
+       least swallow a particle to avoid running into the problem of never being
+       able to spawn a star.
+       If n_IMF <= 0, then disable this criterion */
+    if (sink_properties->n_IMF > 0 &&
+        si->to_collect.mass_swallowed >= mass_swallow_limit &&
+        si->to_collect.mass_eligible_swallow != 0) {
+      return;
+    }
 
-  // message("sink %lld wants to swallow gas particle %lld", si->id, pj->id);
+    /* Increment the swallowd mass */
+    si->to_collect.mass_swallowed += hydro_get_mass(pj);
 
-  if (pj->sink_data.swallow_id < si->id) {
+    /* --------------------------------------------------------------------- */
+    /* Since this pair gas-sink is the most bound, keep track of the
+       E_mec_bound and set the swallow_id accordingly */
+    pj->sink_data.E_mec_bound = E_mec_sink_part;
     pj->sink_data.swallow_id = si->id;
   }
 
diff --git a/src/sink/GEAR/sink_io.h b/src/sink/GEAR/sink_io.h
index ac23fc3ab898284b1009c25c366a357c8ef8538e..79a427be5d705c1c3d30d96eaaca06bdffc3453a 100644
--- a/src/sink/GEAR/sink_io.h
+++ b/src/sink/GEAR/sink_io.h
@@ -1,6 +1,7 @@
 /*******************************************************************************
  * This file is part of SWIFT.
  * Copyright (c) 2021 Loic Hausammann (loic.hausammann@epfl.ch)
+ *               2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -33,7 +34,7 @@ INLINE static void sink_read_particles(struct sink* sinks,
                                        struct io_props* list, int* num_fields) {
 
   /* Say how much we want to read */
-  *num_fields = 4;
+  *num_fields = 6;
 
   /* List what we want to read */
   list[0] = io_make_input_field("Coordinates", DOUBLE, 3, COMPULSORY,
@@ -44,6 +45,10 @@ INLINE static void sink_read_particles(struct sink* sinks,
                                 sinks, mass);
   list[3] = io_make_input_field("ParticleIDs", LONGLONG, 1, COMPULSORY,
                                 UNIT_CONV_NO_UNITS, sinks, id);
+  list[4] = io_make_input_field("SmoothingLength", FLOAT, 1, OPTIONAL,
+                                UNIT_CONV_LENGTH, sinks, h);
+  list[5] = io_make_input_field("BirthTime", FLOAT, 1, OPTIONAL, UNIT_CONV_MASS,
+                                sinks, birth_time);
 }
 
 INLINE static void convert_sink_pos(const struct engine* e,
@@ -94,6 +99,21 @@ INLINE static void convert_sink_vel(const struct engine* e,
   ret[2] *= cosmo->a_inv;
 }
 
+INLINE static void convert_sink_target_mass(const struct engine* e,
+                                            const struct sink* sink,
+                                            float* ret) {
+  /* Recall that the target_mass_Msun is in M_sun in the code. We nee to convert
+     it to internal units for consistency in the output. */
+  ret[0] = sink->target_mass_Msun * e->physical_constants->const_solar_mass;
+}
+
+INLINE static void convert_sink_swallowed_angular_momentum(
+    const struct engine* e, const struct sink* sink, float* ret) {
+  ret[0] = sink->swallowed_angular_momentum[0];
+  ret[1] = sink->swallowed_angular_momentum[1];
+  ret[2] = sink->swallowed_angular_momentum[2];
+}
+
 /**
  * @brief Specifies which sink-particle fields to write to a dataset
  *
@@ -107,7 +127,7 @@ INLINE static void sink_write_particles(const struct sink* sinks,
                                         int with_cosmology) {
 
   /* Say how much we want to write */
-  *num_fields = 4;
+  *num_fields = 11;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_sink(
@@ -122,9 +142,52 @@ INLINE static void sink_write_particles(const struct sink* sinks,
   list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f, sinks,
                                  mass, "Masses of the particles");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           sinks, id, "Unique ID of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, sinks, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
+
+  list[4] = io_make_output_field(
+      "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, sinks, h,
+      "Co-moving smoothing lengths (FWHM of the kernel) of the particles");
+
+  list[5] = io_make_physical_output_field(
+      "NumberOfSinkSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, sinks,
+      number_of_sink_swallows, /*can convert to comoving=*/0,
+      "Total number of sink merger events");
+
+  list[6] = io_make_physical_output_field(
+      "NumberOfGasSwallows", INT, 1, UNIT_CONV_NO_UNITS, 0.f, sinks,
+      number_of_gas_swallows, /*can convert to comoving=*/0,
+      "Total number of gas merger events");
+
+  list[7] = io_make_output_field_convert_sink(
+      "TargetMass", FLOAT, 1, UNIT_CONV_MASS, 0.f, sinks,
+      convert_sink_target_mass, "Sink target mass to spawn star particles");
+
+  list[8] = io_make_physical_output_field(
+      "Nstars", INT, 1, UNIT_CONV_NO_UNITS, 0.f, sinks, n_stars,
+      /*can convert to comoving=*/0,
+      "Number of stars spawned by the sink particles");
+
+  /* Note: Since the swallowed momentum is computed with the physical velocity,
+     i.e. including the Hubble flow term, it is not convertible to comoving
+     frame. */
+  list[9] = io_make_physical_output_field_convert_sink(
+      "SwallowedAngularMomentum", FLOAT, 3, UNIT_CONV_ANGULAR_MOMENTUM, 0.f,
+      sinks,
+      /*can convert to comoving=*/0, convert_sink_swallowed_angular_momentum,
+      "Physical swallowed angular momentum of the particles");
+
+  if (with_cosmology) {
+    list[10] = io_make_physical_output_field(
+        "BirthScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, sinks,
+        birth_scale_factor, /*can convert to comoving=*/0,
+        "Scale-factors at which the sinks were born");
+  } else {
+    list[10] =
+        io_make_output_field("BirthTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, sinks,
+                             birth_time, "Times at which the sinks were born");
+  }
 
 #ifdef DEBUG_INTERACTIONS_SINKS
 
@@ -145,6 +208,7 @@ INLINE static void sink_write_particles(const struct sink* sinks,
   list[3] = io_make_output_field(
       "Ids_ngb_merger", LONGLONG, MAX_NUM_OF_NEIGHBOURS_SINKS,
       UNIT_CONV_NO_UNITS, 0.f, sinks, ids_ngbs_merger, "IDs of the neighbors");
+
 #endif
 }
 
diff --git a/src/sink/GEAR/sink_part.h b/src/sink/GEAR/sink_part.h
index b07457516f26188bbd9c1c11f19ffc1dec349e42..1f5390397745ae77c98c28383c24538a28f445cf 100644
--- a/src/sink/GEAR/sink_part.h
+++ b/src/sink/GEAR/sink_part.h
@@ -1,6 +1,7 @@
 /*******************************************************************************
  * This file is part of SWIFT.
  * Copyright (c) 2021 Loic Hausammann (loic.hausammann@epfl.ch)
+ *               2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -45,16 +46,82 @@ struct sink {
   /*! Particle velocity. */
   float v[3];
 
-  /*! Cut off radius. */
-  float r_cut;
+  /* Particle smoothing length, or r_cut/kernel_gamma if using a fixed cutoff*/
+  float h;
+
+  struct {
+
+    /* Number of neighbours. */
+    float wcount;
+
+    /* Number of neighbours spatial derivative. */
+    float wcount_dh;
+
+  } density;
 
   /*! Sink particle mass */
   float mass;
 
+  /*! Sink target mass. In Msun. */
+  float target_mass_Msun;
+
+  /* Mass of the IMF this sinks is currently affected to. In internal units. */
+  double mass_IMF;
+
+  /*! Integer number of neighbours */
+  int num_ngbs;
+
+  /*! Mass of the sink before starting the star spawning loop */
+  float mass_tot_before_star_spawning;
+
+  /*! Sink target stellar type */
+  enum stellar_type target_type;
+
+  /*! Union for the birth time and birth scale factor */
+  union {
+
+    /*! Birth time */
+    float birth_time;
+
+    /*! Birth scale factor */
+    float birth_scale_factor;
+  };
+
+  struct {
+
+    /*! Minimal gas smoothing length */
+    float minimal_h_gas;
+
+    /*! Density of the gas surrounding the sink. */
+    float rho_gas;
+
+    /*! Smoothed sound speed of the gas surrounding the sink. */
+    float sound_speed_gas;
+
+    /*! Smoothed velocity of the gas surrounding the sink, in the frame of the
+      sink (internal units) */
+    float velocity_gas[3];
+
+    /*! Minimal t_c between all sink neighbours */
+    float minimal_sink_t_c;
+
+    /*! Minimal dynamical time between all sink neighbours */
+    float minimal_sink_t_dyn;
+
+    /*! Total mass that passes all criteria before the accretion limit */
+    float mass_eligible_swallow;
+
+    /*! Swallowed mass during this timestep */
+    float mass_swallowed;
+  } to_collect;
+
   /*! Particle time bin */
   timebin_t time_bin;
 
-  /*! number of stars contained in the sink */
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
+  /*! Number of stars spawned by this sink */
   int n_stars;
 
   /*! Total (physical) angular momentum accumulated by swallowing particles */
@@ -76,6 +143,10 @@ struct sink {
    * by merged-in sinks) */
   int number_of_direct_gas_swallows;
 
+  /*! Flag to determine if a sink has already changed its IMF from pop III to
+     pop II. */
+  int has_IMF_changed_from_popIII_to_popII;
+
   /*! Chemistry information (e.g. metal content at birth, swallowed metal
    * content, etc.) */
   struct chemistry_sink_data chemistry_data;
@@ -112,6 +183,28 @@ struct sink {
   /*! List of interacting particles in compute formation SELF and PAIR */
   long long ids_ngbs_accretion[MAX_NUM_OF_NEIGHBOURS_SINKS];
 #endif
+
+#ifdef SWIFT_SINK_DENSITY_CHECKS
+
+  /* Integer number of neighbours in the density loop */
+  int N_check_density;
+
+  /* Exact integer number of neighbours in the density loop */
+  int N_check_density_exact;
+
+  /*! Has this particle interacted with any unhibited neighbour? */
+  char inhibited_check_exact;
+
+  float n_check;
+
+  float n_check_exact;
+
+  float rho_check;
+
+  /*! Exact value of the density field obtained via brute-force loop */
+  float rho_check_exact;
+
+#endif
 } SWIFT_STRUCT_ALIGN;
 
 #endif /* SWIFT_GEAR_SINK_PART_H */
diff --git a/src/sink/GEAR/sink_properties.h b/src/sink/GEAR/sink_properties.h
index 4a28ed53ac7df0fda8ce44d13afc7d0157267203..53e3f3455a5aa5e20b1f1ba30964ca71f0ace57a 100644
--- a/src/sink/GEAR/sink_properties.h
+++ b/src/sink/GEAR/sink_properties.h
@@ -1,6 +1,7 @@
 /*******************************************************************************
  * This file is part of SWIFT.
  * Copyright (c) 2020 Loic Hausammann (loic.hausammann@epfl.ch)
+ *               2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -16,24 +17,209 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  ******************************************************************************/
-#ifndef SWIFT_DEFAULT_SINK_PROPERTIES_H
-#define SWIFT_DEFAULT_SINK_PROPERTIES_H
+#ifndef SWIFT_GEAR_SINK_PROPERTIES_H
+#define SWIFT_GEAR_SINK_PROPERTIES_H
+
+/* Local header */
+#include "feedback_properties.h"
+#include "parser.h"
+
+/* Some default values for the parameters to be read in the YAML file */
+#define sink_gear_f_acc_default 0.8
+#define sink_gear_star_spawning_sigma_factor_default 0.2
+#define sink_gear_n_imf_default FLT_MAX /* No accretion restriction */
+#define sink_gear_tolerance_sf_timestep_default 0.5
+
+/* Sink formation is activated */
+#define sink_gear_disable_sink_formation_default 0
+
+/* By default all current implemented criteria are active */
+#define sink_gear_sink_formation_criterion_all_default 1
 
 /**
  * @brief Properties of sink in the Default model.
  */
 struct sink_props {
 
+  /* ----- Basic neighbour search properties ------ */
+
+  /*! Resolution parameter */
+  float eta_neighbours;
+
+  /*! Target weightd number of neighbours (for info only)*/
+  float target_neighbours;
+
+  /*! Smoothing length tolerance */
+  float h_tolerance;
+
+  /*! Tolerance on neighbour number  (for info only)*/
+  float delta_neighbours;
+
+  /*! Maximal number of iterations to converge h */
+  int max_smoothing_iterations;
+
+  /*! Maximal change of h over one time-step */
+  float log_max_h_change;
+
+  /*! Are we using a fixed cutoff radius? (all smoothing length calculations are
+   * disabled if so) */
+  char use_fixed_r_cut;
+
   /*! Cut off radius */
   float cut_off_radius;
 
-  /*! Maximal gas temperature for forming a star. */
-  float maximal_temperature;
+  /* Fraction of the cut-off radius for gas accretion. It should respect 0 <=
+     f_acc <= 1 */
+  float f_acc;
+
+  /*! Maximal gas temperature for forming a sink. */
+  float temperature_threshold;
 
-  /*! Minimal gas density for forming a star. */
+  /*! Minimal gas density for forming a sink with the temperature threshold. */
   float density_threshold;
+
+  /*! Gas density for forming a sink without the temperature threshold. */
+  float maximal_density_threshold;
+
+  /*! Mass of the stellar particle representing the low mass stars
+   * (continuous IMF sampling). In M_sun. */
+  float stellar_particle_mass_Msun;
+
+  /*! Minimal mass of stars represented by discrete particles. In M_sun. */
+  float minimal_discrete_mass_Msun;
+
+  /*! Mass of the stellar particle representing the low mass stars
+   * (continuous IMF sampling). In M_sun. First stars */
+  float stellar_particle_mass_first_stars_Msun;
+
+  /*! Minimal mass of stars represented by discrete particles. In M_sun.
+   * First stars. */
+  float minimal_discrete_mass_first_stars_Msun;
+
+  /*! Sink formation criteria selecter : some criteria can be left out.  */
+  char sink_formation_contracting_gas_criterion;
+  char sink_formation_smoothing_length_criterion;
+  char sink_formation_jeans_instability_criterion;
+  char sink_formation_bound_state_criterion;
+  char sink_formation_overlapping_sink_criterion;
+
+  /*! Disable sink formation? (e.g. used in sink accretion tests). Default: 0
+     (keep sink formation) */
+  char disable_sink_formation;
+
+  /*! Factor to rescale the velocity dispersion of the stars when they are
+     spawned */
+  double star_spawning_sigma_factor;
+
+  /*! Minimal sink mass in Msun. This prevents m_sink << m_gas in low
+    resolution simulations. */
+  float sink_minimal_mass_Msun;
+
+  /***************************************************************************/
+  /*! Maximal time-step length of young sinks (internal units) */
+  double max_time_step_young;
+
+  /*! Maximal time-step length of old sinks (internal units) */
+  double max_time_step_old;
+
+  /*! Age threshold for the young/old transition (internal units) */
+  double age_threshold;
+
+  /*! Age threshold for the transition to unlimited time-step size (internal
+   * units) */
+  double age_threshold_unlimited;
+
+  /*! Time integration CFL condition factor */
+  float CFL_condition;
+
+  /*! Number of times the IMF mass can be swallowed in a single timestep */
+  float n_IMF;
+
+  /*! Tolerance parameter for SF timestep constraint */
+  float tolerance_SF_timestep;
 };
 
+/**
+ * @brief Initialise the probabilities to get a stellar mass (continuous
+ * sampling of the IMF)
+ *
+ * @param sp The #sink_props.
+ * @param phys_const The physical constants in the internal unit system.
+ * @param us The internal unit system.
+ * @param params The parsed parameters.
+ * @param cosmo The cosmological model.
+ */
+INLINE static void sink_props_init_probabilities(
+    struct sink_props *sp, struct initial_mass_function *imf,
+    const struct phys_const *phys_const, int first_stars) {
+
+  /* get the IMF mass limits (all in Msol) */
+  float mass_min = imf->mass_min;
+  float mass_max = imf->mass_max;
+
+  /* Treat separately the cases of first star or not. */
+  if (!first_stars) {
+    imf->minimal_discrete_mass_Msun = sp->minimal_discrete_mass_Msun;
+    imf->stellar_particle_mass_Msun = sp->stellar_particle_mass_Msun;
+  } else {
+    imf->minimal_discrete_mass_Msun =
+        sp->minimal_discrete_mass_first_stars_Msun;
+    imf->stellar_particle_mass_Msun =
+        sp->stellar_particle_mass_first_stars_Msun;
+  }
+
+  /* Sanity check */
+  if (imf->minimal_discrete_mass_Msun < imf->mass_limits[imf->n_parts - 1])
+    error(
+        "minimal_discrete_mass (=%8.3f) cannot be smaller than the mass limit "
+        "(=%8.3f) of the last IMF segment,",
+        imf->minimal_discrete_mass_Msun, imf->mass_limits[imf->n_parts - 1]);
+
+  /* Compute the IMF mass (in solar mass) below the minimal IMF discrete mass
+     (continuous part). */
+  double Mtot, Md, Mc;
+  initial_mass_function_compute_Mc_Md_Mtot(imf, &Mc, &Md, &Mtot);
+
+  /* Compute the number of stars in the continuous part of the IMF */
+  double Nc = initial_mass_function_get_imf_number_fraction(
+                  imf, mass_min, imf->minimal_discrete_mass_Msun) *
+              Mtot;
+
+  /* Compute the number of stars in the discrete part of the IMF */
+  double Nd = initial_mass_function_get_imf_number_fraction(
+                  imf, imf->minimal_discrete_mass_Msun, mass_max) *
+              Mtot;
+
+  if (engine_rank == 0) {
+    message("Mass of the continuous part (in M_sun) : %g", Mc);
+    message("Mass of the discrete   part (in M_sun) : %g", Md);
+    message("Total IMF mass (in M_sun)              : %g", Mtot);
+    message("Number of stars in the continuous part : %g", Nc);
+    message("Number of stars in the discrete   part : %g", Nd);
+  }
+
+  /* if no continous part, return */
+  if (Mc == 0) {
+    imf->sink_Pc = 0;
+    imf->stellar_particle_mass_Msun = 0;
+    if (engine_rank == 0) {
+      message("probability of the continuous part    : %g", 0.);
+      message("probability of the discrete   part    : %g", 1.);
+    }
+    return;
+  }
+
+  /* Compute the probabilities */
+  double Pc = 1 / (1 + Nd);
+  double Pd = 1 - Pc;
+  imf->sink_Pc = Pc;
+
+  if (engine_rank == 0) {
+    message("probability of the continuous part     : %g", Pc);
+    message("probability of the discrete   part     : %g", Pd);
+  }
+}
+
 /**
  * @brief Initialise the sink properties from the parameter file.
  *
@@ -42,30 +228,249 @@ struct sink_props {
  * @param us The internal unit system.
  * @param params The parsed parameters.
  * @param cosmo The cosmological model.
+ * @param with_feedback Are we running with feedback?
  */
-INLINE static void sink_props_init(struct sink_props *sp,
-                                   const struct phys_const *phys_const,
-                                   const struct unit_system *us,
-                                   struct swift_params *params,
-                                   const struct cosmology *cosmo) {
+INLINE static void sink_props_init(
+    struct sink_props *sp, struct feedback_props *fp,
+    const struct phys_const *phys_const, const struct unit_system *us,
+    struct swift_params *params, const struct hydro_props *hydro_props,
+    const struct cosmology *cosmo, const int with_feedback) {
+
+  /* Read in the basic neighbour search properties or default to the hydro
+     ones if the user did not provide any different values */
+
+  /* Kernel properties */
+  sp->eta_neighbours = parser_get_opt_param_float(
+      params, "Sinks:resolution_eta", hydro_props->eta_neighbours);
+
+  /* Tolerance for the smoothing length Newton-Raphson scheme */
+  sp->h_tolerance = parser_get_opt_param_float(params, "Sinks:h_tolerance",
+                                               hydro_props->h_tolerance);
+
+  /* Get derived properties */
+  sp->target_neighbours = pow_dimension(sp->eta_neighbours) * kernel_norm;
+  const float delta_eta = sp->eta_neighbours * (1.f + sp->h_tolerance);
+  sp->delta_neighbours =
+      (pow_dimension(delta_eta) - pow_dimension(sp->eta_neighbours)) *
+      kernel_norm;
+
+  /* Number of iterations to converge h */
+  sp->max_smoothing_iterations =
+      parser_get_opt_param_int(params, "Sinks:max_ghost_iterations",
+                               hydro_props->max_smoothing_iterations);
 
-  sp->cut_off_radius =
-      parser_get_param_float(params, "GEARSink:cut_off_radius");
+  /* Time integration properties */
+  const float max_volume_change =
+      parser_get_opt_param_float(params, "Sinks:max_volume_change", -1);
+  if (max_volume_change == -1)
+    sp->log_max_h_change = hydro_props->log_max_h_change;
+  else
+    sp->log_max_h_change = logf(powf(max_volume_change, hydro_dimension_inv));
 
-  sp->maximal_temperature =
-      parser_get_param_float(params, "GEARSink:maximal_temperature");
+  /* If we do not run with feedback, abort and print an error */
+  if (!with_feedback)
+    error(
+        "ERROR: Running with sink but without feedback. GEAR sink model needs "
+        "to be run with --sink and --feedback");
+
+  /* This property is used in all models to flag if we're using a fixed cutoff.
+   * If it is set to 1, we use a fixed r_cut (read in below) and don't
+   * (re)calculate h. If not, the code will use the variable h*kernel_gamma as a
+   * cutoff radius.
+   */
+  sp->use_fixed_r_cut =
+      parser_get_param_char(params, "GEARSink:use_fixed_cut_off_radius");
+
+  /* The property cut_off_radius is now only used in the GEAR model.
+   * It is ignored if use_fixed_r_cut is 0. */
+  if (sp->use_fixed_r_cut) {
+    sp->cut_off_radius =
+        parser_get_param_float(params, "GEARSink:cut_off_radius");
+  }
+
+  sp->f_acc = parser_get_opt_param_float(params, "GEARSink:f_acc",
+                                         sink_gear_f_acc_default);
+
+  /* Check that sp->f_acc respects 0 <= f_acc <= 1 */
+  if ((sp->f_acc < 0) || (sp->f_acc > 1)) {
+    error(
+        "The sink f_acc has not an allowed value. It should respect 0 <= f_acc "
+        "<= 1. Current value f_acc = %f.",
+        sp->f_acc);
+  }
+
+  sp->temperature_threshold =
+      parser_get_param_float(params, "GEARSink:temperature_threshold_K");
 
   sp->density_threshold =
-      parser_get_param_float(params, "GEARSink:density_threshold");
+      parser_get_param_float(params, "GEARSink:density_threshold_Hpcm3");
+
+  sp->maximal_density_threshold = parser_get_opt_param_float(
+      params, "GEARSink:maximal_density_threshold_Hpcm3", FLT_MAX);
+
+  if (sp->maximal_density_threshold < sp->density_threshold) {
+    error(
+        "maximal_density_threshold_Hpcm3 must be larger than "
+        "density_threshold_Hpcm3");
+  }
+
+  sp->stellar_particle_mass_Msun =
+      parser_get_param_float(params, "GEARSink:stellar_particle_mass_Msun");
+
+  sp->minimal_discrete_mass_Msun =
+      parser_get_param_float(params, "GEARSink:minimal_discrete_mass_Msun");
+
+  sp->stellar_particle_mass_first_stars_Msun = parser_get_param_float(
+      params, "GEARSink:stellar_particle_mass_first_stars_Msun");
+
+  sp->minimal_discrete_mass_first_stars_Msun = parser_get_param_float(
+      params, "GEARSink:minimal_discrete_mass_first_stars_Msun");
+
+  sp->star_spawning_sigma_factor =
+      parser_get_opt_param_float(params, "GEARSink:star_spawning_sigma_factor",
+                                 sink_gear_star_spawning_sigma_factor_default);
+
+  sp->n_IMF = parser_get_opt_param_float(params, "GEARSink:n_IMF",
+                                         sink_gear_n_imf_default);
+
+  sp->sink_minimal_mass_Msun =
+      parser_get_opt_param_float(params, "GEARSink:sink_minimal_mass_Msun", 0.);
+
+  /* Sink formation criterion parameters (all active by default) */
+  sp->sink_formation_contracting_gas_criterion = parser_get_opt_param_int(
+      params, "GEARSink:sink_formation_contracting_gas_criterion",
+      sink_gear_sink_formation_criterion_all_default);
+
+  sp->sink_formation_smoothing_length_criterion = parser_get_opt_param_int(
+      params, "GEARSink:sink_formation_smoothing_length_criterion",
+      sink_gear_sink_formation_criterion_all_default);
+
+  sp->sink_formation_jeans_instability_criterion = parser_get_opt_param_int(
+      params, "GEARSink:sink_formation_jeans_instability_criterion",
+      sink_gear_sink_formation_criterion_all_default);
+
+  sp->sink_formation_bound_state_criterion = parser_get_opt_param_int(
+      params, "GEARSink:sink_formation_bound_state_criterion",
+      sink_gear_sink_formation_criterion_all_default);
+
+  sp->sink_formation_overlapping_sink_criterion = parser_get_opt_param_int(
+      params, "GEARSink:sink_formation_overlapping_sink_criterion",
+      sink_gear_sink_formation_criterion_all_default);
+
+  /* Should we disable sink formation ? */
+  sp->disable_sink_formation =
+      parser_get_opt_param_int(params, "GEARSink:disable_sink_formation",
+                               sink_gear_disable_sink_formation_default);
+
+  /* Maximal time-step lengths */
+  const double max_time_step_young_Myr = parser_get_opt_param_float(
+      params, "GEARSink:max_timestep_young_Myr", FLT_MAX);
+  const double max_time_step_old_Myr = parser_get_opt_param_float(
+      params, "GEARSink:max_timestep_old_Myr", FLT_MAX);
+  const double age_threshold_Myr = parser_get_opt_param_float(
+      params, "GEARSink:timestep_age_threshold_Myr", FLT_MAX);
+  const double age_threshold_unlimited_Myr = parser_get_opt_param_float(
+      params, "GEARSink:timestep_age_threshold_unlimited_Myr", FLT_MAX);
+
+  /* Check for consistency */
+  if (age_threshold_unlimited_Myr != 0. && age_threshold_Myr != FLT_MAX) {
+    if (age_threshold_unlimited_Myr < age_threshold_Myr)
+      error(
+          "The age threshold for unlimited sink time-step sizes (%e Myr) is "
+          "smaller than the transition threshold from young to old ages (%e "
+          "Myr)",
+          age_threshold_unlimited_Myr, age_threshold_Myr);
+  }
+
+  /* Timestep tolerance paramters */
+  sp->CFL_condition = parser_get_param_float(params, "GEARSink:CFL_condition");
+
+  sp->tolerance_SF_timestep =
+      parser_get_opt_param_float(params, "GEARSink:tolerance_SF_timestep",
+                                 sink_gear_tolerance_sf_timestep_default);
 
   /* Apply unit change */
-  sp->maximal_temperature /=
+  sp->temperature_threshold /=
       units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
 
-  sp->density_threshold /= units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
+  const double m_p_cgs = phys_const->const_proton_mass *
+                         units_cgs_conversion_factor(us, UNIT_CONV_MASS);
+  sp->density_threshold *=
+      m_p_cgs / units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
+  sp->maximal_density_threshold *=
+      m_p_cgs / units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
+
+  const double Myr_internal_units = 1e6 * phys_const->const_year;
+  sp->max_time_step_young = max_time_step_young_Myr * Myr_internal_units;
+  sp->max_time_step_old = max_time_step_old_Myr * Myr_internal_units;
+  sp->age_threshold = age_threshold_Myr * Myr_internal_units;
+  sp->age_threshold_unlimited =
+      age_threshold_unlimited_Myr * Myr_internal_units;
+
+  /* here, we need to differenciate between the stellar models */
+  struct initial_mass_function *imf;
+  struct stellar_model *sm;
+
+  sm = &fp->stellar_model;
+  imf = &sm->imf;
+
+  /* Initialize for the stellar models (PopII) */
+  sink_props_init_probabilities(sp, imf, phys_const, 0);
+
+  /* Now initialize the first stars. */
+  if (fp->metallicity_max_first_stars != -1) {
+    sm = &fp->stellar_model_first_stars;
+    imf = &sm->imf;
+    sink_props_init_probabilities(sp, imf, phys_const, 1);
+  }
+  if (engine_rank == 0) {
+    message("temperature_threshold                        = %g",
+            sp->temperature_threshold);
+    message("density_threshold                            = %g",
+            sp->density_threshold);
+    message("maximal_density_threshold                    = %g",
+            sp->maximal_density_threshold);
+    message("sink_minimal_mass (in M_sun)                 = %g",
+            sp->sink_minimal_mass_Msun);
+    message("stellar_particle_mass (in M_sun)             = %g",
+            sp->stellar_particle_mass_Msun);
+    message("minimal_discrete_mass (in M_sun)             = %g",
+            sp->minimal_discrete_mass_Msun);
+
+    message("stellar_particle_mass_first_stars (in M_sun) = %g",
+            sp->stellar_particle_mass_first_stars_Msun);
+    message("minimal_discrete_mass_first_stars (in M_sun) = %g",
+            sp->minimal_discrete_mass_first_stars_Msun);
+
+    /* Print information about the functionalities */
+    message("disable_sink_formation                       = %d",
+            sp->disable_sink_formation);
+    message("sink_formation_contracting_gas_criterion     = %d",
+            sp->sink_formation_contracting_gas_criterion);
+    message("sink_formation_smoothing_length_criterion    = %d",
+            sp->sink_formation_smoothing_length_criterion);
+    message("sink_formation_jeans_instability_criterion   = %d",
+            sp->sink_formation_jeans_instability_criterion);
+    message("sink_formation_bound_state_criterion         = %d",
+            sp->sink_formation_bound_state_criterion);
+    message("sink_formation_overlapping_sink_criterion    = %d",
+            sp->sink_formation_overlapping_sink_criterion);
 
-  message("maximal_temperature = %g", sp->maximal_temperature);
-  message("density_threshold  = %g", sp->density_threshold);
+    /* Print timestep parameters information */
+    message("sink max_timestep_young                      = %e",
+            sp->max_time_step_young);
+    message("sink max_timestep_old                        = %e",
+            sp->max_time_step_old);
+    message("sink age_threshold from young to old         = %e",
+            sp->age_threshold);
+    message("sink age_threshold from old to unlimited     = %e",
+            sp->age_threshold_unlimited);
+    message("sink C_CFL                                   = %e",
+            sp->CFL_condition);
+    message("tolerance_SF_timestep                        = %e",
+            sp->tolerance_SF_timestep);
+    message("n_IMF                                        = %e", sp->n_IMF);
+  }
 }
 
 /**
@@ -94,4 +499,4 @@ INLINE static void sink_struct_restore(const struct sink_props *props,
                       "Sink props");
 }
 
-#endif /* SWIFT_DEFAULT_SINK_PROPERTIES_H */
+#endif /* SWIFT_GEAR_SINK_PROPERTIES_H */
diff --git a/src/sink/GEAR/sink_setters.h b/src/sink/GEAR/sink_setters.h
new file mode 100644
index 0000000000000000000000000000000000000000..f28cea1c1c093b8558a9ce0dabac8edb44071e55
--- /dev/null
+++ b/src/sink/GEAR/sink_setters.h
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ *******************************************************************************/
+#ifndef SWIFT_GEAR_SINK_SETTERS_H
+#define SWIFT_GEAR_SINK_SETTERS_H
+
+#include "sink_part.h"
+
+/**
+ * @file src/sink/GEAR/sink_setters.h
+ * @brief Setters functions for GEAR sink scheme to avoid exposing
+ * implementation details to the outer world. Keep the code clean and lean.
+ */
+
+/**
+ * @brief Set the birth time/scale-factor of a #sink particle.
+ *
+ * @param sink The #sink.
+ * @param birth_time Birth time of the #sink.
+ * @param birth_scale_factor Birth scale-factor of the star.
+ * @param with_cosmology If we run with cosmology.
+ */
+
+__attribute__((always_inline)) INLINE void
+sink_set_sink_birth_time_or_scale_factor(struct sink* restrict sink,
+                                         const float birth_time,
+                                         const float birth_scale_factor,
+                                         const int with_cosmology) {
+  if (with_cosmology) {
+    sink->birth_scale_factor = birth_scale_factor;
+  } else {
+    sink->birth_time = birth_time;
+  }
+}
+
+/**
+ * @brief Update the target mass of the sink particle.
+ *
+ * @param sink the #sink particle.
+ * @param sink_props the sink properties to use.
+ * @param phys_const the physical constants in internal units.
+ * @param e The #engine
+ * @param star_counter The star loop counter.
+ */
+INLINE static void sink_update_target_mass(struct sink* sink,
+                                           const struct sink_props* sink_props,
+                                           const struct engine* e,
+                                           int star_counter) {
+
+  float random_number = random_unit_interval_part_ID_and_index(
+      sink->id, star_counter, e->ti_current, random_number_sink_formation);
+
+  const struct feedback_props* feedback_props = e->feedback_props;
+
+  /* Pick the correct table. (if only one table, threshold is < 0) */
+  const float metal =
+      chemistry_get_sink_total_iron_mass_fraction_for_feedback(sink);
+  const float threshold = feedback_props->metallicity_max_first_stars;
+
+  /* If metal < threshold, then the sink generates first star particles. */
+  const int is_first_star = metal < threshold;
+  const struct stellar_model* model;
+  double minimal_discrete_mass_Msun;
+
+  /* Take the correct values if your are a first star or not. */
+  if (!is_first_star) /* (metal >= threshold)*/ {
+    model = &feedback_props->stellar_model;
+    minimal_discrete_mass_Msun = sink_props->minimal_discrete_mass_Msun;
+  } else {
+    model = &feedback_props->stellar_model_first_stars;
+    minimal_discrete_mass_Msun =
+        sink_props->minimal_discrete_mass_first_stars_Msun;
+  }
+
+  const struct initial_mass_function* imf = &model->imf;
+
+  if (random_number < imf->sink_Pc) {
+    /* We are dealing with the continous part of the IMF. */
+    sink->target_mass_Msun = imf->stellar_particle_mass_Msun;
+    sink->target_type = star_population_continuous_IMF;
+  } else {
+    /* We are dealing with the discrete part of the IMF. */
+    random_number = random_unit_interval_part_ID_and_index(
+        sink->id, star_counter + 1, e->ti_current,
+        random_number_sink_formation);
+    double m = initial_mass_function_sample_power_law(
+        minimal_discrete_mass_Msun, imf->mass_max, imf->exp[imf->n_parts - 1],
+        random_number);
+    sink->target_mass_Msun = m;
+    sink->target_type = single_star;
+  }
+
+  /* Also store the mass of the IMF into the sink. */
+  double dummy;
+  ;
+  initial_mass_function_compute_Mc_Md_Mtot(imf, &dummy, &dummy,
+                                           &sink->mass_IMF);
+
+  /* Convert from M_sun to internal units. */
+  sink->mass_IMF *= e->physical_constants->const_solar_mass;
+}
+#endif /* SWIFT_GEAR_SINK_SETTERS_H */
diff --git a/src/sink/GEAR/sink_struct.h b/src/sink/GEAR/sink_struct.h
index c5427f2f970530ca873a437b7a39a48c19d0639d..57c7761b21862e5a49bd70c3cc67ad9ebd32bc66 100644
--- a/src/sink/GEAR/sink_struct.h
+++ b/src/sink/GEAR/sink_struct.h
@@ -1,6 +1,7 @@
 /*******************************************************************************
  * This file is part of SWIFT.
  * Copyright (c) 2022 Yves Revaz (yves.revaz@epfl.ch)
+ *               2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -29,6 +30,45 @@ struct sink_part_data {
 
   /*! Gravitational potential of the particle */
   uint8_t can_form_sink;
+
+  /* Total kinetic energy of the neigbouring gas particles (i.e. inside
+   * sink_cut_off_radius) */
+  double E_kin_neighbours;
+
+  /* Total interal energy of the neigbouring gas particles (i.e. inside
+   * sink_cut_off_radius) */
+  double E_int_neighbours;
+
+  /* Total radiated energy of the neigbouring gas particles (i.e. inside
+   * sink_cut_off_radius) */
+  double E_rad_neighbours;
+
+  /* Total self potential energy of the neigbouring gas particles (i.e. inside
+   * sink_cut_off_radius) */
+  double E_pot_self_neighbours;
+
+  /* Total external potential energy of the neigbouring gas particles (i.e.
+   * inside sink_cut_off_radius) */
+  double E_pot_ext_neighbours;
+
+  /* Total magnetic energy of the neigbouring gas particles (i.e. inside
+   * sink_cut_off_radius) */
+  double E_mag_neighbours;
+
+  /* Total rotational energy per component (x, y and z) of the neigbouring gas
+   * particles  (i.e. inside sink_cut_off_radius) */
+  double E_rot_neighbours[3];
+
+  /* Potential of the particle copied from the #gpart */
+  float potential;
+
+  /* Mechanical energy between the part and the sink with swallow_id.
+   * This is used to check that this part is, out of all sinks, the most bound
+   to the sink with swallow_id. */
+  double E_mec_bound;
+
+  /* Does the future sink overalp an existing one ? */
+  uint8_t is_overlapping_sink;
 };
 
 /**
diff --git a/src/sink_debug.h b/src/sink_debug.h
index 2346711e7387ebe3e480c759e0322e3c1f407d8d..44b1937d435d5095f69f1c767520d8b300b93814 100644
--- a/src/sink_debug.h
+++ b/src/sink_debug.h
@@ -25,6 +25,8 @@
 /* Import the debug routines of the right sink definition */
 #if defined(SINK_NONE)
 #include "./sink/Default/sink_debug.h"
+#elif defined(SINK_BASIC)
+#include "./sink/Basic/sink_debug.h"
 #elif defined(SINK_GEAR)
 #include "./sink/GEAR/sink_debug.h"
 #else
diff --git a/src/sink_iact.h b/src/sink_iact.h
new file mode 100644
index 0000000000000000000000000000000000000000..4d5b6467f816fe1e6952f2a29b30f7f24ae20f02
--- /dev/null
+++ b/src/sink_iact.h
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2020 Loic Hausammann (loic.hausammann@epfl.ch)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_SINK_IACT_H
+#define SWIFT_SINK_IACT_H
+
+/* Config parameters. */
+#include <config.h>
+
+/* Select the correct sink model */
+#if defined(SINK_NONE)
+#include "./sink/Default/sink_iact.h"
+#elif defined(SINK_BASIC)
+#include "./sink/Basic/sink_iact.h"
+#elif defined(SINK_GEAR)
+#include "./sink/GEAR/sink_iact.h"
+#else
+#error "Invalid choice of sink model"
+#endif
+
+#endif /* SWIFT_SINK_IACT_H */
diff --git a/src/sink_io.h b/src/sink_io.h
index f5b0be176ca4dadf75bc825a9f2f001e1eef2883..685824f93bbd97351278f9ecc7c9988006a24fb8 100644
--- a/src/sink_io.h
+++ b/src/sink_io.h
@@ -24,6 +24,8 @@
 /* Load the correct sink type */
 #if defined(SINK_NONE)
 #include "./sink/Default/sink_io.h"
+#elif defined(SINK_BASIC)
+#include "./sink/Basic/sink_io.h"
 #elif defined(SINK_GEAR)
 #include "./sink/GEAR/sink_io.h"
 #else
diff --git a/src/sink_properties.h b/src/sink_properties.h
index 2ec9bd9d00fa09ef01792a42e3117096e65244ff..7700fa5395816857b87594c87cc97af578460edd 100644
--- a/src/sink_properties.h
+++ b/src/sink_properties.h
@@ -25,6 +25,8 @@
 /* Select the correct sink model */
 #if defined(SINK_NONE)
 #include "./sink/Default/sink_properties.h"
+#elif defined(SINK_BASIC)
+#include "./sink/Basic/sink_properties.h"
 #elif defined(SINK_GEAR)
 #include "./sink/GEAR/sink_properties.h"
 #else
diff --git a/src/sink_struct.h b/src/sink_struct.h
index 00e6380298182149d8666663c977c5be665ac11d..7164c1cb20cd6b544a4f03e4932c3cfc0900a696 100644
--- a/src/sink_struct.h
+++ b/src/sink_struct.h
@@ -33,6 +33,8 @@
 /* Import the right black holes definition */
 #if defined(SINK_NONE)
 #include "./sink/Default/sink_struct.h"
+#elif defined(SINK_BASIC)
+#include "./sink/Basic/sink_struct.h"
 #elif defined(SINK_GEAR)
 #include "./sink/GEAR/sink_struct.h"
 #else
diff --git a/src/space.c b/src/space.c
index 9459ca444e64389b36eaeef7ff0cf99b65a81f38..29088cc79e3291455e619a1db2b79bee376f9765 100644
--- a/src/space.c
+++ b/src/space.c
@@ -72,6 +72,19 @@ int space_subsize_pair_grav = space_subsize_pair_grav_default;
 int space_subsize_self_grav = space_subsize_self_grav_default;
 int space_subdepth_diff_grav = space_subdepth_diff_grav_default;
 int space_maxsize = space_maxsize_default;
+int space_grid_split_threshold = space_grid_split_threshold_default;
+
+/* Recursion sizes */
+int space_recurse_size_self_hydro = space_recurse_size_self_hydro_default;
+int space_recurse_size_pair_hydro = space_recurse_size_pair_hydro_default;
+int space_recurse_size_self_stars = space_recurse_size_self_stars_default;
+int space_recurse_size_pair_stars = space_recurse_size_pair_stars_default;
+int space_recurse_size_self_black_holes =
+    space_recurse_size_self_black_holes_default;
+int space_recurse_size_pair_black_holes =
+    space_recurse_size_pair_black_holes_default;
+int space_recurse_size_self_sinks = space_recurse_size_self_sinks_default;
+int space_recurse_size_pair_sinks = space_recurse_size_pair_sinks_default;
 
 /*! Number of extra #part we allocate memory for per top-level cell */
 int space_extra_parts = space_extra_parts_default;
@@ -149,6 +162,11 @@ void space_free_foreign_parts(struct space *s, const int clear_cell_pointers) {
     s->size_bparts_foreign = 0;
     s->bparts_foreign = NULL;
   }
+  if (s->sinks_foreign != NULL) {
+    swift_free("sinks_foreign", s->sinks_foreign);
+    s->size_sinks_foreign = 0;
+    s->sinks_foreign = NULL;
+  }
   if (clear_cell_pointers) {
     for (int k = 0; k < s->e->nr_proxies; k++) {
       for (int j = 0; j < s->e->proxies[k].nr_cells_in; j++) {
@@ -180,7 +198,7 @@ void space_reorder_extra_gparts_mapper(void *map_data, int num_cells,
 
   for (int ind = 0; ind < num_cells; ind++) {
     struct cell *c = &cells_top[local_cells[ind]];
-    cell_reorder_extra_gparts(c, s->parts, s->sparts, s->sinks);
+    cell_reorder_extra_gparts(c, s->parts, s->sparts, s->sinks, s->bparts);
   }
 }
 
@@ -455,6 +473,9 @@ void space_getcells(struct space *s, int nr_cells, struct cell **cells,
                          space_cellallocchunk * sizeof(struct cell)) != 0)
         error("Failed to allocate more cells.");
 
+      /* This allocation is never correctly freed, that is ok. */
+      swift_ignore_leak(s->cells_sub[tpid]);
+
       /* Clear the newly-allocated cells. */
       bzero(s->cells_sub[tpid], sizeof(struct cell) * space_cellallocchunk);
 
@@ -503,6 +524,7 @@ void space_getcells(struct space *s, int nr_cells, struct cell **cells,
     cells[j]->nodeID = -1;
     cells[j]->tpid = tpid;
     if (lock_init(&cells[j]->hydro.lock) != 0 ||
+        lock_init(&cells[j]->hydro.extra_sort_lock) != 0 ||
         lock_init(&cells[j]->grav.plock) != 0 ||
         lock_init(&cells[j]->grav.mlock) != 0 ||
         lock_init(&cells[j]->stars.lock) != 0 ||
@@ -844,6 +866,58 @@ void space_convert_rt_quantities(struct space *s, int verbose) {
             clocks_getunit());
 }
 
+void space_post_init_parts_mapper(void *restrict map_data, int count,
+                                  void *restrict extra_data) {
+  struct space *s = (struct space *)extra_data;
+  const struct engine *restrict e = s->e;
+
+  const struct hydro_props *restrict hydro_props = e->hydro_properties;
+  const struct phys_const *restrict phys_const = e->physical_constants;
+  const struct unit_system *us = s->e->internal_units;
+  const struct cosmology *restrict cosmo = e->cosmology;
+  const struct cooling_function_data *cool_func = e->cooling_func;
+
+  struct part *restrict p = (struct part *)map_data;
+  const ptrdiff_t delta = p - s->parts;
+  struct xpart *restrict xp = s->xparts + delta;
+
+  /* Loop over all the particles ignoring the extra buffer ones for on-the-fly
+   * creation
+   * Here we can initialize the cooling properties of the (x-)particles
+   * using quantities (like the density) defined only after the neighbour loop.
+   *
+   * */
+
+  for (int k = 0; k < count; k++) {
+    cooling_post_init_part(phys_const, us, hydro_props, cosmo, cool_func, &p[k],
+                           &xp[k]);
+  }
+}
+
+/**
+ * @brief Calls the #part post-initialisation function on all particles in the
+ * space.
+ * Here we can initialize the cooling properties of the (x-)particles
+ * using quantities (like the density) defined only after the initial neighbour
+ * loop.
+ *
+ * @param s The #space.
+ * @param verbose Are we talkative?
+ */
+void space_post_init_parts(struct space *s, int verbose) {
+
+  const ticks tic = getticks();
+
+  if (s->nr_parts > 0)
+    threadpool_map(&s->e->threadpool, space_post_init_parts_mapper, s->parts,
+                   s->nr_parts, sizeof(struct part), threadpool_auto_chunk_size,
+                   s);
+
+  if (verbose)
+    message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
+            clocks_getunit());
+}
+
 void space_collect_sum_part_mass(void *restrict map_data, int count,
                                  void *restrict extra_data) {
 
@@ -943,7 +1017,7 @@ void space_collect_sum_bpart_mass(void *restrict map_data, int count,
 }
 
 /**
- * @breif Collect the mean mass of each particle type in the #space.
+ * @brief Collect the mean mass of each particle type in the #space.
  */
 void space_collect_mean_masses(struct space *s, int verbose) {
 
@@ -1191,9 +1265,37 @@ void space_init(struct space *s, struct swift_params *params,
                                space_subsize_self_grav_default);
   space_splitsize = parser_get_opt_param_int(
       params, "Scheduler:cell_split_size", space_splitsize_default);
+  space_grid_split_threshold = parser_get_opt_param_int(
+      params, "Scheduler:grid_split_threshold", space_grid_split_threshold);
   space_subdepth_diff_grav =
       parser_get_opt_param_int(params, "Scheduler:cell_subdepth_diff_grav",
                                space_subdepth_diff_grav_default);
+
+  space_recurse_size_self_hydro =
+      parser_get_opt_param_int(params, "Scheduler:cell_recurse_size_self_hydro",
+                               space_recurse_size_self_hydro_default);
+  space_recurse_size_pair_hydro =
+      parser_get_opt_param_int(params, "Scheduler:cell_recurse_size_pair_hydro",
+                               space_recurse_size_pair_hydro_default);
+  space_recurse_size_self_stars =
+      parser_get_opt_param_int(params, "Scheduler:cell_recurse_size_self_stars",
+                               space_recurse_size_self_stars_default);
+  space_recurse_size_pair_stars =
+      parser_get_opt_param_int(params, "Scheduler:cell_recurse_size_pair_stars",
+                               space_recurse_size_pair_stars_default);
+  space_recurse_size_self_black_holes = parser_get_opt_param_int(
+      params, "Scheduler:cell_recurse_size_self_black_holes",
+      space_recurse_size_self_black_holes_default);
+  space_recurse_size_pair_black_holes = parser_get_opt_param_int(
+      params, "Scheduler:cell_recurse_size_pair_black_holes",
+      space_recurse_size_pair_black_holes_default);
+  space_recurse_size_self_sinks =
+      parser_get_opt_param_int(params, "Scheduler:cell_recurse_size_self_sinks",
+                               space_recurse_size_self_sinks_default);
+  space_recurse_size_pair_sinks =
+      parser_get_opt_param_int(params, "Scheduler:cell_recurse_size_pair_sinks",
+                               space_recurse_size_pair_sinks_default);
+
   space_extra_parts = parser_get_opt_param_int(
       params, "Scheduler:cell_extra_parts", space_extra_parts_default);
   space_extra_sparts = parser_get_opt_param_int(
@@ -1614,8 +1716,8 @@ void space_remap_ids(struct space *s, int nr_nodes, int verbose) {
 
   if (verbose) message("Remapping all the IDs");
 
-  size_t local_nr_dm_background = 0;
-  size_t local_nr_nuparts = 0;
+  long long local_nr_dm_background = 0;
+  long long local_nr_nuparts = 0;
   for (size_t i = 0; i < s->nr_gparts; ++i) {
     if (s->gparts[i].type == swift_type_neutrino)
       local_nr_nuparts++;
@@ -1624,17 +1726,17 @@ void space_remap_ids(struct space *s, int nr_nodes, int verbose) {
   }
 
   /* Get the current local number of particles */
-  const size_t local_nr_parts = s->nr_parts;
-  const size_t local_nr_sinks = s->nr_sinks;
-  const size_t local_nr_gparts = s->nr_gparts;
-  const size_t local_nr_sparts = s->nr_sparts;
-  const size_t local_nr_bparts = s->nr_bparts;
-  const size_t local_nr_baryons =
+  long long local_nr_parts = s->nr_parts;
+  long long local_nr_sinks = s->nr_sinks;
+  long long local_nr_gparts = s->nr_gparts;
+  long long local_nr_sparts = s->nr_sparts;
+  long long local_nr_bparts = s->nr_bparts;
+  long long local_nr_baryons =
       local_nr_parts + local_nr_sinks + local_nr_sparts + local_nr_bparts;
-  const size_t local_nr_dm = local_nr_gparts > 0
-                                 ? local_nr_gparts - local_nr_baryons -
-                                       local_nr_nuparts - local_nr_dm_background
-                                 : 0;
+  long long local_nr_dm = local_nr_gparts > 0
+                              ? local_nr_gparts - local_nr_baryons -
+                                    local_nr_nuparts - local_nr_dm_background
+                              : 0;
 
   /* Get the global offsets */
   long long offset_parts = 0;
@@ -1700,21 +1802,21 @@ void space_remap_ids(struct space *s, int nr_nodes, int verbose) {
                 total_bparts + total_nuparts);
 
   /* We can now remap the IDs in the range [offset offset + local_nr] */
-  for (size_t i = 0; i < local_nr_parts; ++i) {
+  for (long long i = 0; i < local_nr_parts; ++i) {
     s->parts[i].id = offset_parts + i;
   }
-  for (size_t i = 0; i < local_nr_sinks; ++i) {
+  for (long long i = 0; i < local_nr_sinks; ++i) {
     s->sinks[i].id = offset_sinks + i;
   }
-  for (size_t i = 0; i < local_nr_sparts; ++i) {
+  for (long long i = 0; i < local_nr_sparts; ++i) {
     s->sparts[i].id = offset_sparts + i;
   }
-  for (size_t i = 0; i < local_nr_bparts; ++i) {
+  for (long long i = 0; i < local_nr_bparts; ++i) {
     s->bparts[i].id = offset_bparts + i;
   }
-  size_t count_dm = 0;
-  size_t count_dm_background = 0;
-  size_t count_nu = 0;
+  long long count_dm = 0;
+  long long count_dm_background = 0;
+  long long count_nu = 0;
   for (size_t i = 0; i < s->nr_gparts; ++i) {
     if (s->gparts[i].type == swift_type_dark_matter) {
       s->gparts[i].id_or_neg_offset = offset_dm + count_dm;
@@ -2122,6 +2224,8 @@ void space_check_drift_point(struct space *s, integertime_t ti_drift,
   space_map_cells_pre(s, 1, cell_check_part_drift_point, &ti_drift);
   space_map_cells_pre(s, 1, cell_check_gpart_drift_point, &ti_drift);
   space_map_cells_pre(s, 1, cell_check_spart_drift_point, &ti_drift);
+  space_map_cells_pre(s, 1, cell_check_bpart_drift_point, &ti_drift);
+  space_map_cells_pre(s, 1, cell_check_sink_drift_point, &ti_drift);
   if (multipole)
     space_map_cells_pre(s, 1, cell_check_multipole_drift_point, &ti_drift);
 #else
@@ -2268,6 +2372,55 @@ void space_check_bpart_swallow_mapper(void *map_data, int nr_bparts,
 #endif
 }
 
+/**
+ * @brief #threadpool mapper function for the swallow debugging check
+ */
+void space_check_part_sink_swallow_mapper(void *map_data, int nr_parts,
+                                          void *extra_data) {
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Unpack the data */
+  struct part *restrict parts = (struct part *)map_data;
+
+  /* Verify that all particles have been swallowed or are untouched */
+  for (int k = 0; k < nr_parts; k++) {
+
+    if (parts[k].time_bin == time_bin_inhibited) continue;
+
+    const long long swallow_id = sink_get_part_swallow_id(&parts[k].sink_data);
+
+    if (swallow_id != -1)
+      error("Particle has not been swallowed! id=%lld", parts[k].id);
+  }
+#else
+  error("Calling debugging code without debugging flag activated.");
+#endif
+}
+
+/**
+ * @brief #threadpool mapper function for the swallow debugging check
+ */
+void space_check_sink_sink_swallow_mapper(void *map_data, int nr_sinks,
+                                          void *extra_data) {
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Unpack the data */
+  struct sink *restrict sinks = (struct sink *)map_data;
+
+  /* Verify that all particles have been swallowed or are untouched */
+  for (int k = 0; k < nr_sinks; k++) {
+
+    if (sinks[k].time_bin == time_bin_inhibited) continue;
+
+    const long long swallow_id =
+        sink_get_sink_swallow_id(&sinks[k].merger_data);
+
+    if (swallow_id != -1)
+      error("Sink particle has not been swallowed! id=%lld", sinks[k].id);
+  }
+#else
+  error("Calling debugging code without debugging flag activated.");
+#endif
+}
+
 /**
  * @brief Checks that all particles have their swallow flag in a "no swallow"
  * state.
@@ -2286,6 +2439,16 @@ void space_check_swallow(struct space *s) {
   threadpool_map(&s->e->threadpool, space_check_bpart_swallow_mapper, s->bparts,
                  s->nr_bparts, sizeof(struct bpart), threadpool_auto_chunk_size,
                  /*extra_data=*/NULL);
+
+  threadpool_map(&s->e->threadpool, space_check_part_sink_swallow_mapper,
+                 s->parts, s->nr_parts, sizeof(struct part),
+                 threadpool_auto_chunk_size,
+                 /*extra_data=*/NULL);
+
+  threadpool_map(&s->e->threadpool, space_check_sink_sink_swallow_mapper,
+                 s->sinks, s->nr_sinks, sizeof(struct sink),
+                 threadpool_auto_chunk_size,
+                 /*extra_data=*/NULL);
 #else
   error("Calling debugging code without debugging flag activated.");
 #endif
@@ -2360,6 +2523,23 @@ void space_after_snap_tracer(struct space *s, int verbose) {
   }
 }
 
+/**
+ * @brief Marks a top-level cell as having been updated by one of the
+ * time-step updating tasks.
+ */
+void space_mark_cell_as_updated(struct space *s, const struct cell *c) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  if (c != c->top) error("Function can only be called at the top level!");
+#endif
+
+  /* Get the offest into the top-level cell array */
+  const size_t delta = c - s->cells_top;
+
+  /* Mark as updated */
+  atomic_inc(&s->cells_top_updated[delta]);
+}
+
 /**
  * @brief Frees up the memory allocated for this #space
  */
@@ -2367,6 +2547,7 @@ void space_clean(struct space *s) {
 
   for (int i = 0; i < s->nr_cells; ++i) cell_clean(&s->cells_top[i]);
   swift_free("cells_top", s->cells_top);
+  swift_free("cells_top_updated", s->cells_top_updated);
   swift_free("multipoles_top", s->multipoles_top);
   swift_free("local_cells_top", s->local_cells_top);
   swift_free("local_cells_with_tasks_top", s->local_cells_with_tasks_top);
@@ -2384,6 +2565,7 @@ void space_clean(struct space *s) {
   swift_free("sparts_foreign", s->sparts_foreign);
   swift_free("gparts_foreign", s->gparts_foreign);
   swift_free("bparts_foreign", s->bparts_foreign);
+  swift_free("sinks_foreign", s->sinks_foreign);
 #endif
   free(s->cells_sub);
   free(s->multipoles_sub);
@@ -2409,6 +2591,9 @@ void space_struct_dump(struct space *s, FILE *stream) {
                        "space_splitsize", "space_splitsize");
   restart_write_blocks(&space_maxsize, sizeof(int), 1, stream, "space_maxsize",
                        "space_maxsize");
+  restart_write_blocks(&space_grid_split_threshold, sizeof(int), 1, stream,
+                       "space_grid_split_threshold",
+                       "space_grid_split_threshold");
   restart_write_blocks(&space_subsize_pair_hydro, sizeof(int), 1, stream,
                        "space_subsize_pair_hydro", "space_subsize_pair_hydro");
   restart_write_blocks(&space_subsize_self_hydro, sizeof(int), 1, stream,
@@ -2433,6 +2618,30 @@ void space_struct_dump(struct space *s, FILE *stream) {
                        "space_extra_sparts", "space_extra_sparts");
   restart_write_blocks(&space_extra_bparts, sizeof(int), 1, stream,
                        "space_extra_bparts", "space_extra_bparts");
+  restart_write_blocks(&space_recurse_size_self_hydro, sizeof(int), 1, stream,
+                       "space_recurse_size_self_hydro",
+                       "space_recurse_size_self_hydro");
+  restart_write_blocks(&space_recurse_size_pair_hydro, sizeof(int), 1, stream,
+                       "space_recurse_size_pair_hydro",
+                       "space_recurse_size_pair_hydro");
+  restart_write_blocks(&space_recurse_size_self_stars, sizeof(int), 1, stream,
+                       "space_recurse_size_self_stars",
+                       "space_recurse_size_self_stars");
+  restart_write_blocks(&space_recurse_size_pair_stars, sizeof(int), 1, stream,
+                       "space_recurse_size_pair_stars",
+                       "space_recurse_size_pair_stars");
+  restart_write_blocks(&space_recurse_size_self_black_holes, sizeof(int), 1,
+                       stream, "space_recurse_size_self_black_holes",
+                       "space_recurse_size_self_black_holes");
+  restart_write_blocks(&space_recurse_size_pair_black_holes, sizeof(int), 1,
+                       stream, "space_recurse_size_pair_black_holes",
+                       "space_recurse_size_pair_black_holes");
+  restart_write_blocks(&space_recurse_size_self_sinks, sizeof(int), 1, stream,
+                       "space_recurse_size_self_sinks",
+                       "space_recurse_size_self_sinks");
+  restart_write_blocks(&space_recurse_size_pair_sinks, sizeof(int), 1, stream,
+                       "space_recurse_size_pair_sinks",
+                       "space_recurse_size_pair_sinks");
   restart_write_blocks(&space_expected_max_nr_strays, sizeof(int), 1, stream,
                        "space_expected_max_nr_strays",
                        "space_expected_max_nr_strays");
@@ -2494,6 +2703,8 @@ void space_struct_restore(struct space *s, FILE *stream) {
                       "space_splitsize");
   restart_read_blocks(&space_maxsize, sizeof(int), 1, stream, NULL,
                       "space_maxsize");
+  restart_read_blocks(&space_grid_split_threshold, sizeof(int), 1, stream, NULL,
+                      "space_grid_split_threshold");
   restart_read_blocks(&space_subsize_pair_hydro, sizeof(int), 1, stream, NULL,
                       "space_subsize_pair_hydro");
   restart_read_blocks(&space_subsize_self_hydro, sizeof(int), 1, stream, NULL,
@@ -2518,6 +2729,22 @@ void space_struct_restore(struct space *s, FILE *stream) {
                       "space_extra_sparts");
   restart_read_blocks(&space_extra_bparts, sizeof(int), 1, stream, NULL,
                       "space_extra_bparts");
+  restart_read_blocks(&space_recurse_size_self_hydro, sizeof(int), 1, stream,
+                      NULL, "space_recurse_size_self_hydro");
+  restart_read_blocks(&space_recurse_size_pair_hydro, sizeof(int), 1, stream,
+                      NULL, "space_recurse_size_pair_hydro");
+  restart_read_blocks(&space_recurse_size_self_stars, sizeof(int), 1, stream,
+                      NULL, "space_recurse_size_self_stars");
+  restart_read_blocks(&space_recurse_size_pair_stars, sizeof(int), 1, stream,
+                      NULL, "space_recurse_size_pair_stars");
+  restart_read_blocks(&space_recurse_size_self_black_holes, sizeof(int), 1,
+                      stream, NULL, "space_recurse_size_self_black_holes");
+  restart_read_blocks(&space_recurse_size_pair_black_holes, sizeof(int), 1,
+                      stream, NULL, "space_recurse_size_pair_black_holes");
+  restart_read_blocks(&space_recurse_size_self_sinks, sizeof(int), 1, stream,
+                      NULL, "space_recurse_size_self_sinks");
+  restart_read_blocks(&space_recurse_size_pair_sinks, sizeof(int), 1, stream,
+                      NULL, "space_recurse_size_pair_sinks");
   restart_read_blocks(&space_expected_max_nr_strays, sizeof(int), 1, stream,
                       NULL, "space_expected_max_nr_strays");
   restart_read_blocks(&engine_max_parts_per_ghost, sizeof(int), 1, stream, NULL,
@@ -2535,6 +2762,7 @@ void space_struct_restore(struct space *s, FILE *stream) {
 
   /* Things that should be reconstructed in a rebuild. */
   s->cells_top = NULL;
+  s->cells_top_updated = NULL;
   s->cells_sub = NULL;
   s->multipoles_top = NULL;
   s->multipoles_sub = NULL;
@@ -2553,6 +2781,8 @@ void space_struct_restore(struct space *s, FILE *stream) {
   s->size_sparts_foreign = 0;
   s->bparts_foreign = NULL;
   s->size_bparts_foreign = 0;
+  s->sinks_foreign = NULL;
+  s->size_sinks_foreign = 0;
 #endif
 
   /* More things to read. */
@@ -2669,9 +2899,10 @@ void space_write_cell(const struct space *s, FILE *f, const struct cell *c) {
 
   /* Write line for current cell */
   fprintf(f, "%lld,%lld,%i,", c->cellID, parent, c->nodeID);
-  fprintf(f, "%i,%i,%i,%s,%s,%g,%g,%g,%g,%g,%g, ", c->hydro.count,
-          c->stars.count, c->grav.count, superID, hydro_superID, c->loc[0],
-          c->loc[1], c->loc[2], c->width[0], c->width[1], c->width[2]);
+  fprintf(f, "%i,%i,%i,%i,%s,%s,%g,%g,%g,%g,%g,%g, ", c->hydro.count,
+          c->stars.count, c->grav.count, c->sinks.count, superID, hydro_superID,
+          c->loc[0], c->loc[1], c->loc[2], c->width[0], c->width[1],
+          c->width[2]);
   fprintf(f, "%g, %g, %i, %i\n", c->hydro.h_max, c->stars.h_max, c->depth,
           c->maxdepth);
 
@@ -2703,7 +2934,7 @@ void space_write_cell_hierarchy(const struct space *s, int j) {
   if (engine_rank == 0) {
     fprintf(f, "name,parent,mpi_rank,");
     fprintf(f,
-            "hydro_count,stars_count,gpart_count,super,hydro_super,"
+            "hydro_count,stars_count,gpart_count,sinks_count,super,hydro_super,"
             "loc1,loc2,loc3,width1,width2,width3,");
     fprintf(f, "hydro_h_max,stars_h_max,depth,maxdepth\n");
 
diff --git a/src/space.h b/src/space.h
index ff7d0f399347657e6f91b0baac098a9254a7cfeb..b064db7cae6f2130a7c7d9fab68c63645801f67c 100644
--- a/src/space.h
+++ b/src/space.h
@@ -48,9 +48,10 @@ struct hydro_props;
 #define space_cellallocchunk 1000
 #define space_splitsize_default 400
 #define space_maxsize_default 8000000
+#define space_grid_split_threshold_default 400
 #define space_extra_parts_default 0
-#define space_extra_gparts_default 0
-#define space_extra_sparts_default 100
+#define space_extra_gparts_default 200
+#define space_extra_sparts_default 200
 #define space_extra_bparts_default 0
 #define space_extra_sinks_default 0
 #define space_expected_max_nr_strays_default 100
@@ -60,6 +61,14 @@ struct hydro_props;
 #define space_subsize_self_stars_default 32000
 #define space_subsize_pair_grav_default 256000000
 #define space_subsize_self_grav_default 32000
+#define space_recurse_size_self_hydro_default 100
+#define space_recurse_size_pair_hydro_default 100
+#define space_recurse_size_self_stars_default 100
+#define space_recurse_size_pair_stars_default 100
+#define space_recurse_size_self_black_holes_default 100
+#define space_recurse_size_pair_black_holes_default 100
+#define space_recurse_size_self_sinks_default 100
+#define space_recurse_size_pair_sinks_default 100
 #define space_subdepth_diff_grav_default 4
 #define space_max_top_level_cells_default 12
 #define space_stretch 1.10f
@@ -72,6 +81,7 @@ struct hydro_props;
  * restore these. */
 extern int space_splitsize;
 extern int space_maxsize;
+extern int space_grid_split_threshold;
 extern int space_subsize_pair_hydro;
 extern int space_subsize_self_hydro;
 extern int space_subsize_pair_stars;
@@ -79,6 +89,14 @@ extern int space_subsize_self_stars;
 extern int space_subsize_pair_grav;
 extern int space_subsize_self_grav;
 extern int space_subdepth_diff_grav;
+extern int space_recurse_size_self_hydro;
+extern int space_recurse_size_pair_hydro;
+extern int space_recurse_size_self_stars;
+extern int space_recurse_size_pair_stars;
+extern int space_recurse_size_self_black_holes;
+extern int space_recurse_size_pair_black_holes;
+extern int space_recurse_size_self_sinks;
+extern int space_recurse_size_pair_sinks;
 extern int space_extra_parts;
 extern int space_extra_gparts;
 extern int space_extra_sparts;
@@ -329,6 +347,9 @@ struct space {
   /*! Structure dealing with the computation of a unique ID */
   struct unique_id unique_id;
 
+  /*! Have the top-level cells' time-steps been updated? */
+  char *cells_top_updated;
+
 #ifdef WITH_MPI
 
   /*! Buffers for parts that we will receive from foreign cells. */
@@ -347,6 +368,10 @@ struct space {
   struct bpart *bparts_foreign;
   size_t nr_bparts_foreign, size_bparts_foreign;
 
+  /*! Buffers for sink-parts that we will receive from foreign cells. */
+  struct sink *sinks_foreign;
+  size_t nr_sinks_foreign, size_sinks_foreign;
+
 #endif
 };
 
@@ -426,8 +451,10 @@ void space_init_sparts(struct space *s, int verbose);
 void space_init_bparts(struct space *s, int verbose);
 void space_init_sinks(struct space *s, int verbose);
 void space_after_snap_tracer(struct space *s, int verbose);
+void space_mark_cell_as_updated(struct space *s, const struct cell *c);
 void space_convert_quantities(struct space *s, int verbose);
 void space_convert_rt_quantities(struct space *s, int verbose);
+void space_post_init_parts(struct space *s, int verbose);
 void space_link_cleanup(struct space *s);
 void space_check_drift_point(struct space *s, integertime_t ti_drift,
                              int multipole);
diff --git a/src/space_cell_index.c b/src/space_cell_index.c
index 7f4f617af2b620f67f9c526d4d77e155ed4d2084..c0ad4f2df00160cc341d585cf6295c2728e4795a 100644
--- a/src/space_cell_index.c
+++ b/src/space_cell_index.c
@@ -80,7 +80,7 @@ void space_parts_get_cell_index_mapper(void *map_data, int nr_parts,
   const double ih_z = s->iwidth[2];
 
   /* Init the local count buffer. */
-  int *cell_counts = (int *)calloc(sizeof(int), s->nr_cells);
+  int *cell_counts = (int *)calloc(s->nr_cells, sizeof(int));
   if (cell_counts == NULL)
     error("Failed to allocate temporary cell count buffer.");
 
@@ -207,7 +207,7 @@ void space_gparts_get_cell_index_mapper(void *map_data, int nr_gparts,
   const double ih_z = s->iwidth[2];
 
   /* Init the local count buffer. */
-  int *cell_counts = (int *)calloc(sizeof(int), s->nr_cells);
+  int *cell_counts = (int *)calloc(s->nr_cells, sizeof(int));
   if (cell_counts == NULL)
     error("Failed to allocate temporary cell count buffer.");
 
@@ -339,7 +339,7 @@ void space_sparts_get_cell_index_mapper(void *map_data, int nr_sparts,
   const double ih_z = s->iwidth[2];
 
   /* Init the local count buffer. */
-  int *cell_counts = (int *)calloc(sizeof(int), s->nr_cells);
+  int *cell_counts = (int *)calloc(s->nr_cells, sizeof(int));
   if (cell_counts == NULL)
     error("Failed to allocate temporary cell count buffer.");
 
@@ -467,7 +467,7 @@ void space_bparts_get_cell_index_mapper(void *map_data, int nr_bparts,
   const double ih_z = s->iwidth[2];
 
   /* Init the local count buffer. */
-  int *cell_counts = (int *)calloc(sizeof(int), s->nr_cells);
+  int *cell_counts = (int *)calloc(s->nr_cells, sizeof(int));
   if (cell_counts == NULL)
     error("Failed to allocate temporary cell count buffer.");
 
@@ -595,7 +595,7 @@ void space_sinks_get_cell_index_mapper(void *map_data, int nr_sinks,
   const double ih_z = s->iwidth[2];
 
   /* Init the local count buffer. */
-  int *cell_counts = (int *)calloc(sizeof(int), s->nr_cells);
+  int *cell_counts = (int *)calloc(s->nr_cells, sizeof(int));
   if (cell_counts == NULL)
     error("Failed to allocate temporary cell count buffer.");
 
@@ -692,8 +692,8 @@ void space_sinks_get_cell_index_mapper(void *map_data, int nr_sinks,
   if (count_extra_sink) atomic_add(&data->count_extra_sink, count_extra_sink);
 
   /* Write back the minimal part mass and velocity sum */
-  atomic_min_f(&s->min_spart_mass, min_mass);
-  atomic_add_f(&s->sum_spart_vel_norm, sum_vel_norm);
+  atomic_min_f(&s->min_sink_mass, min_mass);
+  atomic_add_f(&s->sum_sink_vel_norm, sum_vel_norm);
 }
 
 /**
diff --git a/src/space_extras.c b/src/space_extras.c
index c7bd9dcfe6401eb4a6a2ca9c1dfa101dc501c43f..4951eed2f8b5ba11e4a4fca9feab6d1c5fb72fd6 100644
--- a/src/space_extras.c
+++ b/src/space_extras.c
@@ -363,6 +363,7 @@ void space_allocate_extras(struct space *s, int verbose) {
       bzero(&s->sinks[i], sizeof(struct sink));
       s->sinks[i].time_bin = time_bin_not_created;
       s->sinks[i].id = -42;
+      sink_mark_sink_as_not_swallowed(&s->sinks[i].merger_data);
     }
 
     /* Put the spare particles in their correct cell */
diff --git a/src/space_first_init.c b/src/space_first_init.c
index ad98957bfefed0ea209296e7b542fc89ac1b5123..3232e445f0fb21fb1dfeae14d91786efa2ed8688 100644
--- a/src/space_first_init.c
+++ b/src/space_first_init.c
@@ -144,6 +144,7 @@ void space_first_init_parts_mapper(void *restrict map_data, int count,
     particle_splitting_mark_part_as_not_split(&xp[k].split_data, p[k].id);
 
     /* And the radiative transfer */
+    rt_first_init_timestep_data(&p[k]);
     rt_first_init_part(&p[k], cosmo, rt_props);
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -450,6 +451,7 @@ void space_first_init_sinks_mapper(void *restrict map_data, int count,
   const struct space *restrict s = (struct space *)extra_data;
   const struct engine *e = s->e;
   const struct sink_props *props = e->sink_properties;
+  const struct chemistry_global_data *chemistry = e->chemistry;
 
 #ifdef SWIFT_DEBUG_CHECKS
   const ptrdiff_t delta = sink - s->sinks;
@@ -479,7 +481,18 @@ void space_first_init_sinks_mapper(void *restrict map_data, int count,
   /* Initialise the rest */
   for (int k = 0; k < count; k++) {
 
-    sink_first_init_sink(&sink[k], props);
+    /* Initialize the sinks */
+    sink_first_init_sink(&sink[k], props, e);
+
+    /* And the sink merger markers */
+    sink_mark_sink_as_not_swallowed(&sink[k].merger_data);
+
+    /* Also initialize the chemistry */
+    chemistry_first_init_sink(chemistry, &sink[k]);
+
+    /* Note: Here we can add X_first_init_sink() for other modules */
+
+    /* TODO (for Darwin): add CSDS when it's ready */
 
 #ifdef SWIFT_DEBUG_CHECKS
     if (sink[k].gpart && sink[k].gpart->id_or_neg_offset != -(k + delta))
diff --git a/src/space_getsid.h b/src/space_getsid.h
index 2124e2e1f027c54248d084c0c35da8a19d9efe66..df81615d3cf1053d5642093743616d164d143ceb 100644
--- a/src/space_getsid.h
+++ b/src/space_getsid.h
@@ -43,9 +43,9 @@
  *
  * @return The shift ID and set shift, may or may not swap ci and cj.
  */
-__attribute__((always_inline, nonnull)) INLINE static int space_getsid(
-    const struct space *s, struct cell **ci, struct cell **cj,
-    double shift[3]) {
+__attribute__((always_inline, nonnull)) INLINE static int
+space_getsid_and_swap_cells(const struct space *s, struct cell **ci,
+                            struct cell **cj, double shift[3]) {
 
   /* Get the relative distance between the pairs, wrapping. */
   const int periodic = s->periodic;
diff --git a/src/space_init.c b/src/space_init.c
index 900e841ba6e38065f470a12935c730fd2d65c8f8..b2afce56ebd58ac3b403233073cb2feae48a8005 100644
--- a/src/space_init.c
+++ b/src/space_init.c
@@ -26,6 +26,7 @@
 #include "space.h"
 
 /* Local headers. */
+#include "adaptive_softening.h"
 #include "black_holes.h"
 #include "chemistry.h"
 #include "engine.h"
@@ -51,6 +52,7 @@ void space_init_parts_mapper(void *restrict map_data, int count,
 
   for (int k = 0; k < count; k++) {
     hydro_init_part(&parts[k], hs);
+    adaptive_softening_init_part(&parts[k]);
     mhd_init_part(&parts[k]);
     black_holes_init_potential(&parts[k].black_holes_data);
     chemistry_init_part(&parts[k], e->chemistry);
@@ -60,7 +62,7 @@ void space_init_parts_mapper(void *restrict map_data, int count,
     tracers_after_init(&parts[k], &xparts[k], e->internal_units,
                        e->physical_constants, with_cosmology, e->cosmology,
                        e->hydro_properties, e->cooling_func, e->time);
-    sink_init_part(&parts[k]);
+    sink_init_part(&parts[k], e->sink_properties);
   }
 }
 
diff --git a/src/space_rebuild.c b/src/space_rebuild.c
index 393a932a820bc147c8d21a515d1cd0ec6b801ee7..f83a25766e7437a74d03c87d9d22da821a687904 100644
--- a/src/space_rebuild.c
+++ b/src/space_rebuild.c
@@ -491,17 +491,20 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
     size_t nr_gparts_exchanged = s->nr_gparts - nr_gparts;
     size_t nr_sparts_exchanged = s->nr_sparts - nr_sparts;
     size_t nr_bparts_exchanged = s->nr_bparts - nr_bparts;
+    size_t nr_sinks_exchanged = s->nr_sinks - nr_sinks;
     engine_exchange_strays(s->e, nr_parts, &h_index[nr_parts],
                            &nr_parts_exchanged, nr_gparts, &g_index[nr_gparts],
                            &nr_gparts_exchanged, nr_sparts, &s_index[nr_sparts],
                            &nr_sparts_exchanged, nr_bparts, &b_index[nr_bparts],
-                           &nr_bparts_exchanged);
+                           &nr_bparts_exchanged, nr_sinks,
+                           &sink_index[nr_sinks], &nr_sinks_exchanged);
 
     /* Set the new particle counts. */
     s->nr_parts = nr_parts + nr_parts_exchanged;
     s->nr_gparts = nr_gparts + nr_gparts_exchanged;
     s->nr_sparts = nr_sparts + nr_sparts_exchanged;
     s->nr_bparts = nr_bparts + nr_bparts_exchanged;
+    s->nr_sinks = nr_sinks + nr_sinks_exchanged;
 
   } else {
 #ifdef SWIFT_DEBUG_CHECKS
@@ -511,6 +514,8 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
       error("Number of sparts changing after repartition");
     if (s->nr_gparts != nr_gparts)
       error("Number of gparts changing after repartition");
+    if (s->nr_sinks != nr_sinks)
+      error("Number of sinks changing after repartition");
 #endif
   }
 
@@ -521,6 +526,7 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
       cell_spart_counts[k] = 0;
       cell_gpart_counts[k] = 0;
       cell_bpart_counts[k] = 0;
+      cell_sink_counts[k] = 0;
     }
   }
 
@@ -549,7 +555,7 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
   }
 
   /* Re-allocate the index array for the bparts if needed.. */
-  if (s->nr_bparts + 1 > s_index_size) {
+  if (s->nr_bparts + 1 > b_index_size) {
     int *bind_new;
     if ((bind_new = (int *)swift_malloc(
              "b_index", sizeof(int) * (s->nr_bparts + 1))) == NULL)
@@ -560,6 +566,17 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
     b_index = bind_new;
   }
 
+  /* Re-allocate the index array for the sinks if needed.. */
+  if (s->nr_sinks + 1 > sink_index_size) {
+    int *sink_ind_new;
+    if ((sink_ind_new = (int *)swift_malloc(
+             "sink_index", sizeof(int) * (s->nr_sinks + 1))) == NULL)
+      error("Failed to allocate temporary sink-particle indices.");
+    memcpy(sink_ind_new, sink_index, sizeof(int) * nr_sinks);
+    swift_free("sink_index", sink_index);
+    sink_index = sink_ind_new;
+  }
+
   const int cdim[3] = {s->cdim[0], s->cdim[1], s->cdim[2]};
   const double ih[3] = {s->iwidth[0], s->iwidth[1], s->iwidth[2]};
 
@@ -605,6 +622,20 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
   }
   nr_bparts = s->nr_bparts;
 
+  /* Assign each received sink to its cell. */
+  for (size_t k = nr_sinks; k < s->nr_sinks; k++) {
+    const struct sink *const sink = &s->sinks[k];
+    sink_index[k] = cell_getid(cdim, sink->x[0] * ih[0], sink->x[1] * ih[1],
+                               sink->x[2] * ih[2]);
+    cell_sink_counts[sink_index[k]]++;
+#ifdef SWIFT_DEBUG_CHECKS
+    if (cells_top[sink_index[k]].nodeID != local_nodeID)
+      error("Received sink-part that does not belong to me (nodeID=%i).",
+            cells_top[sink_index[k]].nodeID);
+#endif
+  }
+  nr_sinks = s->nr_sinks;
+
 #else /* WITH_MPI */
 
   /* Update the part, spart and bpart counters */
@@ -719,14 +750,14 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
       error("Inhibited particle sorted into a cell!");
 
     /* New cell index */
-    const int new_bind =
+    const int new_sink_ind =
         cell_getid(s->cdim, sink->x[0] * s->iwidth[0],
                    sink->x[1] * s->iwidth[1], sink->x[2] * s->iwidth[2]);
 
     /* New cell of this sink */
-    const struct cell *c = &s->cells_top[new_bind];
+    const struct cell *c = &s->cells_top[new_sink_ind];
 
-    if (sink_index[k] != new_bind)
+    if (sink_index[k] != new_sink_ind)
       error("sink's new cell index not matching sorted index.");
 
     if (sink->x[0] < c->loc[0] || sink->x[0] > c->loc[0] + c->width[0] ||
@@ -935,6 +966,7 @@ void space_rebuild(struct space *s, int repartitioned, int verbose) {
 
       /* Store the state at rebuild time */
       c->stars.parts_rebuild = c->stars.parts;
+      c->sinks.parts_rebuild = c->sinks.parts;
       c->grav.parts_rebuild = c->grav.parts;
 
       c->hydro.count_total = c->hydro.count + space_extra_parts;
diff --git a/src/space_recycle.c b/src/space_recycle.c
index 657d3e23bb1743e8bacc79031bc175174fbf4eb1..73b8bb4ebaac3c1321cad51bed83a5a114987be6 100644
--- a/src/space_recycle.c
+++ b/src/space_recycle.c
@@ -90,7 +90,9 @@ void space_rebuild_recycle_mapper(void *map_data, int num_elements,
                          multipole_rec_end);
     c->hydro.sorts = NULL;
     c->stars.sorts = NULL;
+#ifdef SWIFT_DEBUG_CHECKS
     c->nr_tasks = 0;
+#endif
     c->grav.nr_mm_tasks = 0;
     c->hydro.density = NULL;
     c->hydro.gradient = NULL;
@@ -138,13 +140,14 @@ void space_rebuild_recycle_mapper(void *map_data, int num_elements,
     c->stars.feedback = NULL;
     c->stars.prepare1 = NULL;
     c->stars.prepare2 = NULL;
+    c->sinks.density = NULL;
     c->sinks.swallow = NULL;
     c->sinks.do_sink_swallow = NULL;
     c->sinks.do_gas_swallow = NULL;
     c->black_holes.density_ghost = NULL;
-    c->black_holes.swallow_ghost_0 = NULL;
     c->black_holes.swallow_ghost_1 = NULL;
     c->black_holes.swallow_ghost_2 = NULL;
+    c->black_holes.swallow_ghost_3 = NULL;
     c->black_holes.density = NULL;
     c->black_holes.swallow = NULL;
     c->black_holes.do_gas_swallow = NULL;
@@ -169,6 +172,7 @@ void space_rebuild_recycle_mapper(void *map_data, int num_elements,
     c->black_holes.black_holes_in = NULL;
     c->black_holes.black_holes_out = NULL;
     c->sinks.sink_in = NULL;
+    c->sinks.density_ghost = NULL;
     c->sinks.sink_ghost1 = NULL;
     c->sinks.sink_ghost2 = NULL;
     c->sinks.sink_out = NULL;
@@ -191,6 +195,7 @@ void space_rebuild_recycle_mapper(void *map_data, int num_elements,
     c->grav.parts = NULL;
     c->grav.parts_rebuild = NULL;
     c->sinks.parts = NULL;
+    c->sinks.parts_rebuild = NULL;
     c->stars.parts = NULL;
     c->stars.parts_rebuild = NULL;
     c->black_holes.parts = NULL;
@@ -247,6 +252,7 @@ void space_recycle(struct space *s, struct cell *c) {
   if (lock_destroy(&c->hydro.lock) != 0 || lock_destroy(&c->grav.plock) != 0 ||
       lock_destroy(&c->grav.mlock) != 0 || lock_destroy(&c->stars.lock) != 0 ||
       lock_destroy(&c->sinks.lock) != 0 ||
+      lock_destroy(&c->hydro.extra_sort_lock) != 0 ||
       lock_destroy(&c->sinks.sink_formation_lock) != 0 ||
       lock_destroy(&c->black_holes.lock) != 0 ||
       lock_destroy(&c->grav.star_formation_lock) != 0 ||
@@ -297,6 +303,7 @@ void space_recycle_list(struct space *s, struct cell *cell_list_begin,
         lock_destroy(&c->grav.mlock) != 0 ||
         lock_destroy(&c->stars.lock) != 0 ||
         lock_destroy(&c->sinks.lock) != 0 ||
+        lock_destroy(&c->hydro.extra_sort_lock) != 0 ||
         lock_destroy(&c->sinks.sink_formation_lock) != 0 ||
         lock_destroy(&c->black_holes.lock) != 0 ||
         lock_destroy(&c->stars.star_formation_lock) != 0 ||
diff --git a/src/space_regrid.c b/src/space_regrid.c
index 95fa4d9cd96ea5ab1832ba2e01d6295a8320a45b..3138050dff359a0ed06f4fbe05629d0788f42f20 100644
--- a/src/space_regrid.c
+++ b/src/space_regrid.c
@@ -64,8 +64,8 @@ void space_regrid(struct space *s, int verbose) {
         if (c->black_holes.h_max > h_max) {
           h_max = c->black_holes.h_max;
         }
-        if (c->sinks.r_cut_max > h_max) {
-          h_max = c->sinks.r_cut_max / kernel_gamma;
+        if (c->sinks.h_max > h_max) {
+          h_max = c->sinks.h_max;
         }
       }
 
@@ -82,8 +82,8 @@ void space_regrid(struct space *s, int verbose) {
         if (c->nodeID == engine_rank && c->black_holes.h_max > h_max) {
           h_max = c->black_holes.h_max;
         }
-        if (c->nodeID == engine_rank && c->sinks.r_cut_max > h_max) {
-          h_max = c->sinks.r_cut_max / kernel_gamma;
+        if (c->nodeID == engine_rank && c->sinks.h_max > h_max) {
+          h_max = c->sinks.h_max;
         }
       }
 
@@ -99,7 +99,7 @@ void space_regrid(struct space *s, int verbose) {
         if (s->bparts[k].h > h_max) h_max = s->bparts[k].h;
       }
       for (size_t k = 0; k < nr_sinks; k++) {
-        if (s->sinks[k].r_cut > h_max) h_max = s->sinks[k].r_cut / kernel_gamma;
+        if (s->sinks[k].h > h_max) h_max = s->sinks[k].h;
       }
     }
   }
@@ -207,6 +207,7 @@ void space_regrid(struct space *s, int verbose) {
       swift_free("local_cells_with_particles_top",
                  s->local_cells_with_particles_top);
       swift_free("cells_top", s->cells_top);
+      swift_free("cells_top_updated", s->cells_top_updated);
       swift_free("multipoles_top", s->multipoles_top);
     }
 
@@ -239,6 +240,11 @@ void space_regrid(struct space *s, int verbose) {
       bzero(s->multipoles_top, s->nr_cells * sizeof(struct gravity_tensors));
     }
 
+    if (swift_memalign("cells_top_updated", (void **)&s->cells_top_updated,
+                       cell_align, s->nr_cells * sizeof(char)) != 0)
+      error("Failed to allocate top-level cells.");
+    bzero(s->cells_top_updated, s->nr_cells * sizeof(char));
+
     /* Allocate the indices of local cells */
     if (swift_memalign("local_cells_top", (void **)&s->local_cells_top,
                        SWIFT_STRUCT_ALIGNMENT, s->nr_cells * sizeof(int)) != 0)
@@ -272,6 +278,8 @@ void space_regrid(struct space *s, int verbose) {
     for (int k = 0; k < s->nr_cells; k++) {
       if (lock_init(&s->cells_top[k].hydro.lock) != 0)
         error("Failed to init spinlock for hydro.");
+      if (lock_init(&s->cells_top[k].hydro.extra_sort_lock) != 0)
+        error("Failed to init spinlock for hydro extra sort.");
       if (lock_init(&s->cells_top[k].grav.plock) != 0)
         error("Failed to init spinlock for gravity.");
       if (lock_init(&s->cells_top[k].grav.mlock) != 0)
@@ -303,6 +311,8 @@ void space_regrid(struct space *s, int verbose) {
           c->width[1] = s->width[1];
           c->width[2] = s->width[2];
           c->dmin = dmin;
+          c->h_min_allowed = c->dmin * 0.5 * (1. / kernel_gamma);
+          c->h_max_allowed = c->dmin * (1. / kernel_gamma);
           c->depth = 0;
           c->split = 0;
           c->hydro.count = 0;
@@ -388,7 +398,7 @@ void space_regrid(struct space *s, int verbose) {
     // message( "rebuilding upper-level cells took %.3f %s." ,
     // clocks_from_ticks(double)(getticks() - tic), clocks_getunit());
 
-  }      /* re-build upper-level cells? */
+  } /* re-build upper-level cells? */
   else { /* Otherwise, just clean up the cells. */
 
     /* Free the old cells, if they were allocated. */
diff --git a/src/space_split.c b/src/space_split.c
index 526ac16d12093f10fc936a8a97428c10dac3891c..ced8bbb64f26b77b7361b5fd5779efdfe468606f 100644
--- a/src/space_split.c
+++ b/src/space_split.c
@@ -74,18 +74,14 @@ void space_split_recursive(struct space *s, struct cell *c,
   float black_holes_h_max_active = 0.f;
   float sinks_h_max = 0.f;
   float sinks_h_max_active = 0.f;
-  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_end_max = 0,
-                ti_hydro_beg_max = 0;
+  integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_beg_max = 0;
   integertime_t ti_rt_end_min = max_nr_timesteps, ti_rt_beg_max = 0;
   integertime_t ti_rt_min_step_size = max_nr_timesteps;
-  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_end_max = 0,
-                ti_gravity_beg_max = 0;
-  integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_end_max = 0,
-                ti_stars_beg_max = 0;
-  integertime_t ti_sinks_end_min = max_nr_timesteps, ti_sinks_end_max = 0,
-                ti_sinks_beg_max = 0;
+  integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_beg_max = 0;
+  integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_beg_max = 0;
+  integertime_t ti_sinks_end_min = max_nr_timesteps, ti_sinks_beg_max = 0;
   integertime_t ti_black_holes_end_min = max_nr_timesteps,
-                ti_black_holes_end_max = 0, ti_black_holes_beg_max = 0;
+                ti_black_holes_beg_max = 0;
   struct part *parts = c->hydro.parts;
   struct gpart *gparts = c->grav.parts;
   struct spart *sparts = c->stars.parts;
@@ -94,6 +90,7 @@ void space_split_recursive(struct space *s, struct cell *c,
   struct sink *sinks = c->sinks.parts;
   struct engine *e = s->e;
   const integertime_t ti_current = e->ti_current;
+  const int with_rt = e->policy & engine_policy_rt;
 
   /* Set the top level cell tpid. Doing it here ensures top level cells
    * have the same tpid as their progeny. */
@@ -230,6 +227,8 @@ void space_split_recursive(struct space *s, struct cell *c,
       cp->width[1] = c->width[1] / 2;
       cp->width[2] = c->width[2] / 2;
       cp->dmin = c->dmin / 2;
+      cp->h_min_allowed = cp->dmin * 0.5 * (1. / kernel_gamma);
+      cp->h_max_allowed = cp->dmin * (1. / kernel_gamma);
       if (k & 4) cp->loc[0] += cp->width[0];
       if (k & 2) cp->loc[1] += cp->width[1];
       if (k & 1) cp->loc[2] += cp->width[2];
@@ -243,8 +242,8 @@ void space_split_recursive(struct space *s, struct cell *c,
       cp->stars.h_max_active = 0.f;
       cp->stars.dx_max_part = 0.f;
       cp->stars.dx_max_sort = 0.f;
-      cp->sinks.r_cut_max = 0.f;
-      cp->sinks.r_cut_max_active = 0.f;
+      cp->sinks.h_max = 0.f;
+      cp->sinks.h_max_active = 0.f;
       cp->sinks.dx_max_part = 0.f;
       cp->black_holes.h_max = 0.f;
       cp->black_holes.h_max_active = 0.f;
@@ -308,9 +307,8 @@ void space_split_recursive(struct space *s, struct cell *c,
         black_holes_h_max = max(black_holes_h_max, cp->black_holes.h_max);
         black_holes_h_max_active =
             max(black_holes_h_max_active, cp->black_holes.h_max_active);
-        sinks_h_max = max(sinks_h_max, cp->sinks.r_cut_max);
-        sinks_h_max_active =
-            max(sinks_h_max_active, cp->sinks.r_cut_max_active);
+        sinks_h_max = max(sinks_h_max, cp->sinks.h_max);
+        sinks_h_max_active = max(sinks_h_max_active, cp->sinks.h_max_active);
 
         ti_hydro_end_min = min(ti_hydro_end_min, cp->hydro.ti_end_min);
         ti_hydro_beg_max = max(ti_hydro_beg_max, cp->hydro.ti_beg_max);
@@ -442,7 +440,7 @@ void space_split_recursive(struct space *s, struct cell *c,
       gravity_multipole_compute_power(&c->grav.multipole->m_pole);
 
     } /* Deal with gravity */
-  }   /* Split or let it be? */
+  } /* Split or let it be? */
 
   /* Otherwise, collect the data from the particles this cell. */
   else {
@@ -453,21 +451,21 @@ void space_split_recursive(struct space *s, struct cell *c,
     maxdepth = c->depth;
 
     ti_hydro_end_min = max_nr_timesteps;
-    ti_hydro_end_max = 0;
     ti_hydro_beg_max = 0;
 
     ti_gravity_end_min = max_nr_timesteps;
-    ti_gravity_end_max = 0;
     ti_gravity_beg_max = 0;
 
     ti_stars_end_min = max_nr_timesteps;
-    ti_stars_end_max = 0;
     ti_stars_beg_max = 0;
 
     ti_black_holes_end_min = max_nr_timesteps;
-    ti_black_holes_end_max = 0;
     ti_black_holes_beg_max = 0;
 
+    ti_rt_end_min = max_nr_timesteps;
+    ti_rt_beg_max = 0;
+    ti_rt_min_step_size = max_nr_timesteps;
+
     /* parts: Get dt_min/dt_max and h_max. */
     for (int k = 0; k < count; k++) {
 #ifdef SWIFT_DEBUG_CHECKS
@@ -482,25 +480,33 @@ void space_split_recursive(struct space *s, struct cell *c,
       const timebin_t time_bin_rt = parts[k].rt_time_data.time_bin;
       const integertime_t ti_end = get_integer_time_end(ti_current, time_bin);
       const integertime_t ti_beg = get_integer_time_begin(ti_current, time_bin);
-      const integertime_t ti_rt_end =
-          get_integer_time_end(ti_current, time_bin_rt);
-      const integertime_t ti_rt_beg =
-          get_integer_time_begin(ti_current, time_bin_rt);
-      const integertime_t ti_rt_step = get_integer_timestep(time_bin_rt);
 
       ti_hydro_end_min = min(ti_hydro_end_min, ti_end);
-      ti_hydro_end_max = max(ti_hydro_end_max, ti_end);
       ti_hydro_beg_max = max(ti_hydro_beg_max, ti_beg);
 
-      ti_rt_end_min = min(ti_rt_end_min, ti_rt_end);
-      ti_rt_beg_max = max(ti_rt_beg_max, ti_rt_beg);
-      ti_rt_min_step_size = min(ti_rt_min_step_size, ti_rt_step);
+      if (with_rt) {
+        /* Contrary to other physics, RT doesn't have its own particle type.
+         * So collect time step data from particles only when we're running
+         * with RT. Otherwise, we may find cells which are active or in
+         * impossible timezones. Skipping this check results in cells having
+         * RT times = max_nr_timesteps or zero, respecively. */
+        const integertime_t ti_rt_end =
+            get_integer_time_end(ti_current, time_bin_rt);
+        const integertime_t ti_rt_beg =
+            get_integer_time_begin(ti_current, time_bin_rt);
+        const integertime_t ti_rt_step = get_integer_timestep(time_bin_rt);
+        ti_rt_end_min = min(ti_rt_end_min, ti_rt_end);
+        ti_rt_beg_max = max(ti_rt_beg_max, ti_rt_beg);
+        ti_rt_min_step_size = min(ti_rt_min_step_size, ti_rt_step);
+      }
 
       h_max = max(h_max, parts[k].h);
 
       if (part_is_active(&parts[k], e))
         h_max_active = max(h_max_active, parts[k].h);
 
+      cell_set_part_h_depth(&parts[k], c);
+
       /* Collect SFR from the particles after rebuilt */
       star_formation_logger_log_inactive_part(&parts[k], &xparts[k],
                                               &c->stars.sfh);
@@ -528,7 +534,6 @@ void space_split_recursive(struct space *s, struct cell *c,
       const integertime_t ti_beg = get_integer_time_begin(ti_current, time_bin);
 
       ti_gravity_end_min = min(ti_gravity_end_min, ti_end);
-      ti_gravity_end_max = max(ti_gravity_end_max, ti_end);
       ti_gravity_beg_max = max(ti_gravity_beg_max, ti_beg);
     }
 
@@ -547,7 +552,6 @@ void space_split_recursive(struct space *s, struct cell *c,
       const integertime_t ti_beg = get_integer_time_begin(ti_current, time_bin);
 
       ti_stars_end_min = min(ti_stars_end_min, ti_end);
-      ti_stars_end_max = max(ti_stars_end_max, ti_end);
       ti_stars_beg_max = max(ti_stars_beg_max, ti_beg);
 
       stars_h_max = max(stars_h_max, sparts[k].h);
@@ -555,6 +559,8 @@ void space_split_recursive(struct space *s, struct cell *c,
       if (spart_is_active(&sparts[k], e))
         stars_h_max_active = max(stars_h_max_active, sparts[k].h);
 
+      cell_set_spart_h_depth(&sparts[k], c);
+
       /* Reset x_diff */
       sparts[k].x_diff[0] = 0.f;
       sparts[k].x_diff[1] = 0.f;
@@ -576,13 +582,14 @@ void space_split_recursive(struct space *s, struct cell *c,
       const integertime_t ti_beg = get_integer_time_begin(ti_current, time_bin);
 
       ti_sinks_end_min = min(ti_sinks_end_min, ti_end);
-      ti_sinks_end_max = max(ti_sinks_end_max, ti_end);
       ti_sinks_beg_max = max(ti_sinks_beg_max, ti_beg);
 
-      sinks_h_max = max(sinks_h_max, sinks[k].r_cut);
+      sinks_h_max = max(sinks_h_max, sinks[k].h);
 
       if (sink_is_active(&sinks[k], e))
-        sinks_h_max_active = max(sinks_h_max_active, sinks[k].r_cut);
+        sinks_h_max_active = max(sinks_h_max_active, sinks[k].h);
+
+      cell_set_sink_h_depth(&sinks[k], c);
 
       /* Reset x_diff */
       sinks[k].x_diff[0] = 0.f;
@@ -605,7 +612,6 @@ void space_split_recursive(struct space *s, struct cell *c,
       const integertime_t ti_beg = get_integer_time_begin(ti_current, time_bin);
 
       ti_black_holes_end_min = min(ti_black_holes_end_min, ti_end);
-      ti_black_holes_end_max = max(ti_black_holes_end_max, ti_end);
       ti_black_holes_beg_max = max(ti_black_holes_beg_max, ti_beg);
 
       black_holes_h_max = max(black_holes_h_max, bparts[k].h);
@@ -613,6 +619,8 @@ void space_split_recursive(struct space *s, struct cell *c,
       if (bpart_is_active(&bparts[k], e))
         black_holes_h_max_active = max(black_holes_h_max_active, bparts[k].h);
 
+      cell_set_bpart_h_depth(&bparts[k], c);
+
       /* Reset x_diff */
       bparts[k].x_diff[0] = 0.f;
       bparts[k].x_diff[1] = 0.f;
@@ -667,8 +675,8 @@ void space_split_recursive(struct space *s, struct cell *c,
   c->stars.h_max_active = stars_h_max_active;
   c->sinks.ti_end_min = ti_sinks_end_min;
   c->sinks.ti_beg_max = ti_sinks_beg_max;
-  c->sinks.r_cut_max = sinks_h_max;
-  c->sinks.r_cut_max_active = sinks_h_max_active;
+  c->sinks.h_max = sinks_h_max;
+  c->sinks.h_max_active = sinks_h_max_active;
   c->black_holes.ti_end_min = ti_black_holes_end_min;
   c->black_holes.ti_beg_max = ti_black_holes_beg_max;
   c->black_holes.h_max = black_holes_h_max;
diff --git a/src/space_unique_id.c b/src/space_unique_id.c
index 137eb7189bc50bc5054265441691e42503abe0b2..481d8a362799e310422e6020361d8ea7fc1ab913 100644
--- a/src/space_unique_id.c
+++ b/src/space_unique_id.c
@@ -48,7 +48,7 @@ void space_update_unique_id(struct space *s) {
     return;
   }
 
-  const int require_new_batch = s->unique_id.next_batch.current == 0;
+  int require_new_batch = s->unique_id.next_batch.current == 0;
 
 #ifdef WITH_MPI
   const struct engine *e = s->e;
@@ -82,9 +82,10 @@ void space_update_unique_id(struct space *s) {
 #endif  // WITH_MPI
 
   /* Compute the size of each batch. */
-  const long long batch_size = (space_extra_parts + space_extra_sparts +
-                                space_extra_gparts + space_extra_bparts) *
-                               s->nr_cells;
+  const long long batch_size =
+      (space_extra_parts + space_extra_sparts + space_extra_gparts +
+       space_extra_bparts + space_extra_sinks) *
+      s->nr_cells;
 
   /* Get a new batch. */
   if (require_new_batch) {
@@ -210,9 +211,10 @@ void space_init_unique_id(struct space *s, int nr_nodes) {
   s->unique_id.global_next_id++;
 
   /* Compute the size of each batch. */
-  const long long batch_size = (space_extra_parts + space_extra_sparts +
-                                space_extra_gparts + space_extra_bparts) *
-                               s->nr_cells;
+  const long long batch_size =
+      (space_extra_parts + space_extra_sparts + space_extra_gparts +
+       space_extra_bparts + space_extra_sinks) *
+      s->nr_cells;
 
   /* Compute the initial batchs (each rank has 2 batchs). */
   if (s->unique_id.global_next_id > LLONG_MAX - 2 * engine_rank * batch_size) {
diff --git a/src/star_formation/EAGLE/star_formation.h b/src/star_formation/EAGLE/star_formation.h
index 8fd2ca89c60ec8656a3b8081cc6e2e71f3fc77c5..ef50af61e6a8a877a53fcb668b6a4455f1321913 100644
--- a/src/star_formation/EAGLE/star_formation.h
+++ b/src/star_formation/EAGLE/star_formation.h
@@ -66,7 +66,7 @@ enum star_formation_threshold {
  */
 struct star_formation {
 
-  /* SF law ------------------------------------------------------------*/
+  /* SF law --------------------------------------------------------------- */
 
   /*! Which SF law are we using? */
   enum star_formation_law SF_law;
@@ -183,6 +183,11 @@ struct star_formation {
     double nH_threshold;
 
   } subgrid_thresh;
+
+  /* Number of stars to form per event -----------------------------------  */
+
+  /* Number of stars to form */
+  int num_stars_per_gas_particle;
 };
 
 /**
@@ -384,8 +389,8 @@ INLINE static void star_formation_compute_SFR_schmidt_law(
  * law based on Schaye and Dalla Vecchia (2008), the star formation
  * rate is calculated as:
  *
- * \dot{m}_\star = A (1 Msun / pc^-2)^-n m_gas (\gamma/G * f_g *
- *                 pressure)**((n-1)/2)
+ * \f$ \dot{m}_\star = A (1 Msun / pc^-2)^-n m_gas (\gamma/G * f_g *
+ *                   pressure)**((n-1)/2) \f$
  *
  * @param p #part.
  * @param xp the #xpart.
@@ -513,18 +518,35 @@ INLINE static int star_formation_should_convert_to_star(
 }
 
 /**
- * @brief Decides whether a new particle should be created or if the hydro
- * particle needs to be transformed.
+ * @brief Returns the number of new star particles to create per SF event.
  *
  * @param p The #part.
  * @param xp The #xpart.
  * @param starform The properties of the star formation model.
  *
- * @return 1 if a new spart needs to be created.
+ * @return The number of extra star particles to generate per gas particles.
+ *        (return 0 if the gas particle itself is to bhe converted)
  */
-INLINE static int star_formation_should_spawn_spart(
-    struct part* p, struct xpart* xp, const struct star_formation* starform) {
-  return 0;
+INLINE static int star_formation_number_spart_to_spawn(
+    const struct part* p, const struct xpart* xp,
+    const struct star_formation* starform) {
+  return starform->num_stars_per_gas_particle - 1;
+}
+
+/**
+ * @brief Returns the number of particles to convert per SF event.
+ *
+ * @param p The #part.
+ * @param xp The #xpart.
+ * @param starform The properties of the star formation model.
+ *
+ * @return The number of particles to generate per gas particles.
+ *        (This has to be 0 or 1)
+ */
+INLINE static int star_formation_number_spart_to_convert(
+    const struct part* p, const struct xpart* xp,
+    const struct star_formation* starform) {
+  return 1;
 }
 
 /**
@@ -579,10 +601,10 @@ INLINE static void star_formation_copy_properties(
     const int convert_part) {
 
   /* Store the current mass */
-  sp->mass = hydro_get_mass(p);
+  sp->mass = hydro_get_mass(p) / starform->num_stars_per_gas_particle;
 
   /* Store the current mass as the initial mass */
-  sp->mass_init = hydro_get_mass(p);
+  sp->mass_init = hydro_get_mass(p) / starform->num_stars_per_gas_particle;
 
   /* Store either the birth_scale_factor or birth_time depending  */
   if (with_cosmology) {
@@ -613,6 +635,34 @@ INLINE static void star_formation_copy_properties(
   sp->last_enrichment_time = sp->birth_time;
   sp->count_since_last_enrichment = -1;
   sp->number_of_heating_events = 0.;
+
+  /* If we are spawning more than 1 star per gas particle --> add some small
+   * displacement */
+  if (starform->num_stars_per_gas_particle > 1) {
+
+    const float max_displacement = 0.1;
+    const double delta_x =
+        2.f * random_unit_interval(sp->id, e->ti_current,
+                                   (enum random_number_type)0) -
+        1.f;
+    const double delta_y =
+        2.f * random_unit_interval(sp->id, e->ti_current,
+                                   (enum random_number_type)1) -
+        1.f;
+    const double delta_z =
+        2.f * random_unit_interval(sp->id, e->ti_current,
+                                   (enum random_number_type)2) -
+        1.f;
+
+    sp->x[0] += delta_x * max_displacement * p->h;
+    sp->x[1] += delta_y * max_displacement * p->h;
+    sp->x[2] += delta_z * max_displacement * p->h;
+
+    /* Copy the position to the gpart */
+    sp->gpart->x[0] = sp->x[0];
+    sp->gpart->x[1] = sp->x[1];
+    sp->gpart->x[2] = sp->x[2];
+  }
 }
 
 /**
@@ -849,6 +899,14 @@ INLINE static void starformation_init_backend(
   } else {
     error("Invalid SF threshold model: '%s'", temp_SF);
   }
+
+  /* Read the number of stars to form per gas particle */
+  starform->num_stars_per_gas_particle = parser_get_opt_param_int(
+      parameter_file, "EAGLEStarFormation:num_of_stars_per_gas_particle", 1);
+  if (starform->num_stars_per_gas_particle < 1)
+    error(
+        "The number of star particles formed per gas particle in a star "
+        "formation event must be >0 !!");
 }
 
 /**
@@ -920,6 +978,8 @@ INLINE static void starformation_print_backend(
 
   message("Running with a direct conversion density of: %e #/cm^3",
           starform->gas_density_direct_HpCM3);
+  message("Will form %d stars per gas particle.",
+          starform->num_stars_per_gas_particle);
 }
 
 /**
diff --git a/src/star_formation/EAGLE/star_formation_iact.h b/src/star_formation/EAGLE/star_formation_iact.h
index eb366c617b001ebfcfdb3f33a7163a8be67a14c6..a82c12a78950dd054e1642a0c835dd45115e4988 100644
--- a/src/star_formation/EAGLE/star_formation_iact.h
+++ b/src/star_formation/EAGLE/star_formation_iact.h
@@ -63,8 +63,8 @@ __attribute__((always_inline)) INLINE static void
 runner_iact_nonsym_star_formation(const float r2, const float dx[3],
                                   const float hi, const float hj,
                                   struct part *restrict pi,
-                                  const struct part *restrict pj, float a,
-                                  float H) {
+                                  const struct part *restrict pj, const float a,
+                                  const float H) {
 
   /* Nothing to do here. We do not need to compute any quantity in the hydro
      density loop for the EAGLE star formation model. */
diff --git a/src/star_formation/EAGLE/star_formation_logger.h b/src/star_formation/EAGLE/star_formation_logger.h
index ee87429c9932965d117494d401f0034654820add..c73d4eb803ac43b443b50da650421005e231c910 100644
--- a/src/star_formation/EAGLE/star_formation_logger.h
+++ b/src/star_formation/EAGLE/star_formation_logger.h
@@ -218,7 +218,7 @@ INLINE static void star_formation_logger_init_log_file(
       " (5)            (6)            (7)\n");
   fprintf(
       fp,
-      "#            Time             a            z        total M_stars  SFR "
+      "# Step       Time             a            z        total M_stars  SFR "
       "(active)  SFR*dt (active)  SFR (total)\n");
 }
 
diff --git a/src/star_formation/GEAR/star_formation.h b/src/star_formation/GEAR/star_formation.h
index d1c09bfa5daf05b1d981e5312e60250f13eadba8..40b95b2bd7f5871a91af436f0f4eabda792e35e0 100644
--- a/src/star_formation/GEAR/star_formation.h
+++ b/src/star_formation/GEAR/star_formation.h
@@ -33,7 +33,9 @@
 #include "part.h"
 #include "physical_constants.h"
 #include "random.h"
+#include "star_formation_setters.h"
 #include "star_formation_struct.h"
+#include "stars.h"
 #include "units.h"
 
 #define star_formation_need_update_dx_max 1
@@ -187,16 +189,16 @@ INLINE static int star_formation_should_convert_to_star(
 }
 
 /**
- * @brief Decides whether a new particle should be created or if the hydro
- * particle needs to be transformed.
+ * @brief Returns the number of new star particles to create per SF event.
  *
  * @param p The #part.
  * @param xp The #xpart.
  * @param starform The properties of the star formation model.
  *
- * @return 1 if a new spart needs to be created.
+ * @return The number of extra star particles to generate per gas particles.
+ *        (return 0 if the gas particle itself is to be converted)
  */
-INLINE static int star_formation_should_spawn_spart(
+INLINE static int star_formation_number_spart_to_spawn(
     struct part* p, struct xpart* xp, const struct star_formation* starform) {
 
   /* Check if we are splitting the particles or not */
@@ -209,6 +211,29 @@ INLINE static int star_formation_should_spawn_spart(
   return hydro_get_mass(p) > mass_min;
 }
 
+/**
+ * @brief Returns the number of particles to convert per SF event.
+ *
+ * @param p The #part.
+ * @param xp The #xpart.
+ * @param starform The properties of the star formation model.
+ *
+ * @return The number of particles to generate per gas particles.
+ *        (This has to be 0 or 1)
+ */
+INLINE static int star_formation_number_spart_to_convert(
+    const struct part* p, const struct xpart* xp,
+    const struct star_formation* starform) {
+
+  if (starform->n_stars_per_part == 1) {
+    return 1;
+  }
+
+  const float mass_min =
+      starform->min_mass_frac_plus_one * starform->mass_stars;
+  return hydro_get_mass(p) <= mass_min;
+}
+
 /**
  * @brief Update the SF properties of a particle that is not star forming.
  *
@@ -320,7 +345,7 @@ INLINE static void star_formation_copy_properties(
     const int convert_part) {
 
   /* Initialize the feedback */
-  feedback_init_after_star_formation(sp, e->feedback_props);
+  feedback_init_after_star_formation(sp, e->feedback_props, star_population);
 
   /* Store the current mass */
   const float mass_gas = hydro_get_mass(p);
diff --git a/src/star_formation/GEAR/star_formation_iact.h b/src/star_formation/GEAR/star_formation_iact.h
index 749b608068650a27cbe4c9a0ca4126d2740337f3..d6b51dd0e4b4fc811bc7eaa21d35f2cf94c4a864 100644
--- a/src/star_formation/GEAR/star_formation_iact.h
+++ b/src/star_formation/GEAR/star_formation_iact.h
@@ -38,8 +38,9 @@
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_star_formation(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {}
 
 /**
  * @brief do star_formation computation after the runner_iact_density (non
@@ -55,9 +56,10 @@ __attribute__((always_inline)) INLINE static void runner_iact_star_formation(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_star_formation(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_star_formation(const float r2, const float dx[3],
+                                  const float hi, const float hj,
                                   struct part *restrict pi,
-                                  const struct part *restrict pj, float a,
-                                  float H) {}
+                                  const struct part *restrict pj, const float a,
+                                  const float H) {}
 
 #endif /* SWIFT_GEAR_STAR_FORMATION_IACT_H */
diff --git a/src/star_formation/GEAR/star_formation_io.h b/src/star_formation/GEAR/star_formation_io.h
index cb0c1b8871b8cd4f5d375a502b0a40bc1c466344..6ce240e30847e5162beb9ba6521e4323cfe4a58d 100644
--- a/src/star_formation/GEAR/star_formation_io.h
+++ b/src/star_formation/GEAR/star_formation_io.h
@@ -72,19 +72,19 @@ __attribute__((always_inline)) INLINE static int
 star_formation_write_sparticles(const struct spart* sparts,
                                 struct io_props* list) {
 
-  list[0] = io_make_output_field(
+  list[0] = io_make_physical_output_field(
       "BirthDensities", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, sparts,
-      sf_data.birth_density,
+      sf_data.birth_density, /*can convert to comoving=*/0,
       "Physical densities at the time of birth of the gas particles that "
       "turned into stars (note that "
       "we store the physical density at the birth redshift, no conversion is "
       "needed)");
 
-  list[1] =
-      io_make_output_field("BirthTemperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE,
-                           0.f, sparts, sf_data.birth_temperature,
-                           "Temperatures at the time of birth of the gas "
-                           "particles that turned into stars");
+  list[1] = io_make_physical_output_field(
+      "BirthTemperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE, 0.f, sparts,
+      sf_data.birth_temperature, /*can convert to comoving=*/0,
+      "Temperatures at the time of birth of the gas "
+      "particles that turned into stars");
 
   list[2] = io_make_output_field("BirthMasses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                                  sparts, sf_data.birth_mass,
@@ -137,11 +137,11 @@ INLINE static void starformation_init_backend(
 
   /* Maximum gas temperature for star formation */
   starform->maximal_temperature = parser_get_param_double(
-      parameter_file, "GEARStarFormation:maximal_temperature");
+      parameter_file, "GEARStarFormation:maximal_temperature_K");
 
   /* Minimal gas density for star formation */
   starform->density_threshold = parser_get_param_double(
-      parameter_file, "GEARStarFormation:density_threshold");
+      parameter_file, "GEARStarFormation:density_threshold_Hpcm3");
 
   /* Number of stars per particles */
   starform->n_stars_per_part = parser_get_param_double(
@@ -162,8 +162,10 @@ INLINE static void starformation_init_backend(
   starform->maximal_temperature /=
       units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
 
-  starform->density_threshold /=
-      units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
+  const double m_p_cgs = phys_const->const_proton_mass *
+                         units_cgs_conversion_factor(us, UNIT_CONV_MASS);
+  starform->density_threshold *=
+      m_p_cgs / units_cgs_conversion_factor(us, UNIT_CONV_DENSITY);
 
   /* Initialize the mass of the stars to 0 for the stats computation */
   starform->mass_stars = 0;
diff --git a/src/star_formation/GEAR/star_formation_setters.h b/src/star_formation/GEAR/star_formation_setters.h
new file mode 100644
index 0000000000000000000000000000000000000000..60168a32d8501a847ec53b888bf6ad77f4903552
--- /dev/null
+++ b/src/star_formation/GEAR/star_formation_setters.h
@@ -0,0 +1,101 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ *******************************************************************************/
+#ifndef SWIFT_GEAR_STAR_FORMATION_SETTERS_H
+#define SWIFT_GEAR_STAR_FORMATION_SETTERS_H
+
+#include "star_formation_struct.h"
+
+/**
+ * @file src/star_formation/GEAR/star_formation_setters.h
+ * @brief Setters functions for GEAR star formation scheme to avoid exposing
+ * implementation details to the outer world. Keep the code clean and lean.
+ */
+
+/**
+ * @brief Set the birth density of a star particle.
+ *
+ * @param sp The #spart.
+ * @param birth_density Birth density of the star.
+ */
+
+__attribute__((always_inline)) INLINE void
+star_formation_set_spart_birth_density(struct spart *restrict sp,
+                                       const float birth_density) {
+  sp->sf_data.birth_density = birth_density;
+}
+
+/**
+ * @brief Set the birth temperature of a star particle.
+ *
+ * @param sp The #spart.
+ * @param birth_temperature Birth temperature of the star.
+ */
+
+__attribute__((always_inline)) INLINE void
+star_formation_set_spart_birth_temperature(struct spart *restrict sp,
+                                           const float birth_temperature) {
+  sp->sf_data.birth_temperature = birth_temperature;
+}
+
+/**
+ * @brief Set the birth mass of a star particle.
+ *
+ * @param sp The #spart.
+ * @param birth_mass Birth mass of the star.
+ */
+
+__attribute__((always_inline)) INLINE void star_formation_set_spart_birth_mass(
+    struct spart *restrict sp, const float birth_mass) {
+  sp->sf_data.birth_mass = birth_mass;
+}
+
+/**
+ * @brief Set the id of the particle creating the star particle.
+ *
+ * @param sp The #spart.
+ * @param progenitor_id The id of the particle creating sp.
+ */
+
+__attribute__((always_inline)) INLINE void
+star_formation_set_spart_progenitor_id(struct spart *restrict sp,
+                                       const long long progenitor_id) {
+  sp->sf_data.progenitor_id = progenitor_id;
+}
+
+/**
+ * @brief Set the birth time/scale-factor of a star particle.
+ *
+ * @param sp The #spart.
+ * @param birth_time Birth time of the star.
+ * @param birth_scale_factor Birth scale-factor of the star.
+ * @param with_cosmology If we run with cosmology.
+ */
+
+__attribute__((always_inline)) INLINE void
+star_formation_set_spart_birth_time_or_scale_factor(
+    struct spart *restrict sp, const float birth_time,
+    const float birth_scale_factor, const int with_cosmology) {
+  if (with_cosmology) {
+    sp->birth_scale_factor = birth_scale_factor;
+  } else {
+    sp->birth_time = birth_time;
+  }
+}
+
+#endif /* SWIFT_GEAR_STAR_FORMATION_SETTERS_H */
diff --git a/src/star_formation/QLA/star_formation.h b/src/star_formation/QLA/star_formation.h
index 052793d293866e89ff97790d0427f07f335f3f1e..8a0d7d32f53ab1e8f13683915c669b21ed3c01c8 100644
--- a/src/star_formation/QLA/star_formation.h
+++ b/src/star_formation/QLA/star_formation.h
@@ -318,22 +318,38 @@ star_formation_no_spart_available(const struct engine* e, const struct part* p,
                                   const struct xpart* xp) {
   /* Nothing to do since we just turn gas particles into DM. */
 }
-
 /**
- * @brief Decides whether a new particle should be created or if the hydro
- * particle needs to be transformed.
+ * @brief Returns the number of new star particles to create per SF event.
  *
  * @param p The #part.
  * @param xp The #xpart.
  * @param starform The properties of the star formation model.
  *
- * @return 1 if a new spart needs to be created.
+ * @return The number of extra star particles to generate per gas particles.
+ *        (return 0 if the gas particle itself is to be converted)
  */
-INLINE static int star_formation_should_spawn_spart(
-    struct part* p, struct xpart* xp, const struct star_formation* starform) {
+INLINE static int star_formation_number_spart_to_spawn(
+    const struct part* p, const struct xpart* xp,
+    const struct star_formation* starform) {
   return 0;
 }
 
+/**
+ * @brief Returns the number of particles to convert per SF event.
+ *
+ * @param p The #part.
+ * @param xp The #xpart.
+ * @param starform The properties of the star formation model.
+ *
+ * @return The number of particles to generate per gas particles.
+ *        (This has to be 0 or 1)
+ */
+INLINE static int star_formation_number_spart_to_convert(
+    const struct part* p, const struct xpart* xp,
+    const struct star_formation* starform) {
+  return 1;
+}
+
 /**
  * @brief Compute some information for the star formation model based
  * on all the particles that were read in.
diff --git a/src/star_formation/QLA/star_formation_iact.h b/src/star_formation/QLA/star_formation_iact.h
index 3dfe77fc1531ddca236678c4dc2c07466942d9e1..f33873bf7485676183bfc567c587908f8c782d82 100644
--- a/src/star_formation/QLA/star_formation_iact.h
+++ b/src/star_formation/QLA/star_formation_iact.h
@@ -38,8 +38,9 @@
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_star_formation(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {
 
   /* Nothing to do here. We do not need to compute any quantity in the hydro
      density loop for the QLA star formation model. */
@@ -59,10 +60,11 @@ __attribute__((always_inline)) INLINE static void runner_iact_star_formation(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_star_formation(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_star_formation(const float r2, const float dx[3],
+                                  const float hi, const float hj,
                                   struct part *restrict pi,
-                                  const struct part *restrict pj, float a,
-                                  float H) {
+                                  const struct part *restrict pj, const float a,
+                                  const float H) {
 
   /* Nothing to do here. We do not need to compute any quantity in the hydro
      density loop for the QLA star formation model. */
diff --git a/src/star_formation/none/star_formation.h b/src/star_formation/none/star_formation.h
index db6ed8658afac741ca26c86eefd4b24572976a6b..7e1a8c8cfbd45259670990ff9b62bea16310de3f 100644
--- a/src/star_formation/none/star_formation.h
+++ b/src/star_formation/none/star_formation.h
@@ -106,17 +106,34 @@ INLINE static int star_formation_should_convert_to_star(
 }
 
 /**
- * @brief Decides whether a new particle should be created or if the hydro
- * particle needs to be transformed.
+ * @brief Returns the number of new star particles to create per SF event.
  *
  * @param p The #part.
  * @param xp The #xpart.
  * @param starform The properties of the star formation model.
  *
- * @return 1 if a new spart needs to be created.
+ * @return The number of extra star particles to generate per gas particles.
+ *        (return 0 if the gas particle itself is to be converted)
  */
-INLINE static int star_formation_should_spawn_spart(
-    struct part* p, struct xpart* xp, const struct star_formation* starform) {
+INLINE static int star_formation_number_spart_to_spawn(
+    const struct part* p, const struct xpart* xp,
+    const struct star_formation* starform) {
+  return 0;
+}
+
+/**
+ * @brief Returns the number of particles to convert per SF event.
+ *
+ * @param p The #part.
+ * @param xp The #xpart.
+ * @param starform The properties of the star formation model.
+ *
+ * @return The number of particles to generate per gas particles.
+ *        (This has to be 0 or 1)
+ */
+INLINE static int star_formation_number_spart_to_convert(
+    const struct part* p, const struct xpart* xp,
+    const struct star_formation* starform) {
   return 0;
 }
 
diff --git a/src/star_formation/none/star_formation_iact.h b/src/star_formation/none/star_formation_iact.h
index 6731f1a24a96fe6881e04e088673c2ec159bd82e..461cf8f3362efd60fa4f41c796194d6ace072a9c 100644
--- a/src/star_formation/none/star_formation_iact.h
+++ b/src/star_formation/none/star_formation_iact.h
@@ -38,8 +38,9 @@
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_star_formation(
-    float r2, const float *dx, float hi, float hj, struct part *restrict pi,
-    struct part *restrict pj, float a, float H) {}
+    const float r2, const float dx[3], const float hi, const float hj,
+    struct part *restrict pi, struct part *restrict pj, const float a,
+    const float H) {}
 
 /**
  * @brief do star_formation computation after the runner_iact_density (non
@@ -55,9 +56,10 @@ __attribute__((always_inline)) INLINE static void runner_iact_star_formation(
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_star_formation(float r2, const float *dx, float hi, float hj,
+runner_iact_nonsym_star_formation(const float r2, const float dx[3],
+                                  const float hi, const float hj,
                                   struct part *restrict pi,
-                                  const struct part *restrict pj, float a,
-                                  float H) {}
+                                  const struct part *restrict pj, const float a,
+                                  const float H) {}
 
 #endif /* SWIFT_NONE_STAR_FORMATION_IACT_H */
diff --git a/src/stars.h b/src/stars.h
index 68b8d7fa52e27238db642ca8b7d1e1ea1923401a..539f9654c102c2509b445c00fc190b6dfc38a68e 100644
--- a/src/stars.h
+++ b/src/stars.h
@@ -35,6 +35,7 @@
 #elif defined(STARS_GEAR)
 #include "./stars/GEAR/stars.h"
 #include "./stars/GEAR/stars_iact.h"
+#include "./stars/GEAR/stars_stellar_type.h"
 #else
 #error "Invalid choice of star model"
 #endif
diff --git a/src/stars/Basic/stars_iact.h b/src/stars/Basic/stars_iact.h
index 90e9a0549ac440ff8819340b14c9af0610215ad8..dba76d5f9b9591c98add428c22f6a08b4c4e2898 100644
--- a/src/stars/Basic/stars_iact.h
+++ b/src/stars/Basic/stars_iact.h
@@ -33,7 +33,7 @@
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_stars_density(const float r2, const float *dx,
+runner_iact_nonsym_stars_density(const float r2, const float dx[3],
                                  const float hi, const float hj,
                                  struct spart *restrict si,
                                  const struct part *restrict pj, const float a,
@@ -76,7 +76,7 @@ runner_iact_nonsym_stars_density(const float r2, const float *dx,
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_stars_feedback(const float r2, const float *dx,
+runner_iact_nonsym_stars_feedback(const float r2, const float dx[3],
                                   const float hi, const float hj,
                                   struct spart *restrict si,
                                   struct part *restrict pj, const float a,
diff --git a/src/stars/Basic/stars_io.h b/src/stars/Basic/stars_io.h
index 54ad764954bd42d199ae0d67550cbedb3f855b86..6b03f75685b03e90f0358687700186c425e192f5 100644
--- a/src/stars/Basic/stars_io.h
+++ b/src/stars/Basic/stars_io.h
@@ -127,7 +127,7 @@ INLINE static void stars_write_particles(const struct spart *sparts,
                                          int with_cosmology) {
 
   /* Say how much we want to write */
-  *num_fields = 5;
+  *num_fields = 6;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_spart(
@@ -142,9 +142,9 @@ INLINE static void stars_write_particles(const struct spart *sparts,
   list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                                  sparts, mass, "Masses of the particles");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           sparts, id, "Unique ID of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, sparts, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
 
   list[4] = io_make_output_field(
       "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, sparts, h,
diff --git a/src/stars/Basic/stars_part.h b/src/stars/Basic/stars_part.h
index 713fe0e281c65ad4e484e42682f83c16b88b1cda..283fe9d6fcc3884694580ee49383184bf9c09a82 100644
--- a/src/stars/Basic/stars_part.h
+++ b/src/stars/Basic/stars_part.h
@@ -81,6 +81,9 @@ struct spart {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Star formation struct */
   struct star_formation_spart_data sf_data;
 
diff --git a/src/stars/EAGLE/stars_io.h b/src/stars/EAGLE/stars_io.h
index 1c499624a67e995e106d4c89225a1e076d8ec21c..5699072b04fe332dd1cb516de8c63fb57703b3dc 100644
--- a/src/stars/EAGLE/stars_io.h
+++ b/src/stars/EAGLE/stars_io.h
@@ -50,9 +50,13 @@ INLINE static void stars_read_particles(struct spart *sparts,
                                 UNIT_CONV_LENGTH, sparts, h);
   list[5] = io_make_input_field("Masses", FLOAT, 1, COMPULSORY, UNIT_CONV_MASS,
                                 sparts, mass_init);
-  list[6] =
-      io_make_input_field_default("StellarFormationTime", FLOAT, 1, OPTIONAL,
-                                  UNIT_CONV_NO_UNITS, sparts, birth_time, -1.);
+
+  /* For the birth time, -1 means the stars remain inactive feedback-wise
+     througout the run */
+  const float default_birth_time = -1.f;
+  list[6] = io_make_input_field_default("StellarFormationTime", FLOAT, 1,
+                                        OPTIONAL, UNIT_CONV_NO_UNITS, sparts,
+                                        birth_time, default_birth_time);
   list[7] = io_make_input_field("BirthDensities", FLOAT, 1, OPTIONAL,
                                 UNIT_CONV_DENSITY, sparts, birth_density);
   list[8] =
@@ -160,9 +164,9 @@ INLINE static void stars_write_particles(const struct spart *sparts,
                                  "Masses of the particles at the current point "
                                  "in time (i.e. after stellar losses");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           sparts, id, "Unique ID of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, sparts, id,
+      /*can convert to comoving=*/0, "Unique ID of the particles");
 
   list[4] = io_make_output_field(
       "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, sparts, h,
@@ -173,9 +177,10 @@ INLINE static void stars_write_particles(const struct spart *sparts,
                                  "Masses of the star particles at birth time");
 
   if (with_cosmology) {
-    list[6] = io_make_output_field(
+    list[6] = io_make_physical_output_field(
         "BirthScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, sparts,
-        birth_scale_factor, "Scale-factors at which the stars were born");
+        birth_scale_factor, /*can convert to comoving=*/0,
+        "Scale-factors at which the stars were born");
   } else {
     list[6] = io_make_output_field("BirthTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f,
                                    sparts, birth_time,
@@ -192,18 +197,18 @@ INLINE static void stars_write_particles(const struct spart *sparts,
       number_of_SNII_events,
       "Number of SNII energy injection events the stars went through.");
 
-  list[9] = io_make_output_field(
-      "BirthDensities", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, sparts, birth_density,
+  list[9] = io_make_physical_output_field(
+      "BirthDensities", FLOAT, 1, UNIT_CONV_DENSITY, -3.f, sparts,
+      birth_density, /*can convert to comoving=*/0,
       "Physical densities at the time of birth of the gas particles that "
-      "turned into stars (note that "
-      "we store the physical density at the birth redshift, no conversion is "
-      "needed)");
-
-  list[10] =
-      io_make_output_field("BirthTemperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE,
-                           0.f, sparts, birth_temperature,
-                           "Temperatures at the time of birth of the gas "
-                           "particles that turned into stars");
+      "turned into stars (note that we store the physical density at the birth "
+      "redshift, no conversion is needed)");
+
+  list[10] = io_make_physical_output_field(
+      "BirthTemperatures", FLOAT, 1, UNIT_CONV_TEMPERATURE, 0.f, sparts,
+      birth_temperature, /*can convert to comoving=*/0,
+      "Temperatures at the time of birth of the gas "
+      "particles that turned into stars");
 
   list[11] = io_make_output_field(
       "FeedbackNumberOfHeatingEvents", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
diff --git a/src/stars/EAGLE/stars_part.h b/src/stars/EAGLE/stars_part.h
index 22091ecc4f6c43928993b6eb8ff43ed9d9e7cd1e..642648f6254c1bed076bf68b19625f1f8c79e7ab 100644
--- a/src/stars/EAGLE/stars_part.h
+++ b/src/stars/EAGLE/stars_part.h
@@ -124,6 +124,9 @@ struct spart {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Number of time-steps since the last enrichment step */
   char count_since_last_enrichment;
 
diff --git a/src/stars/GEAR/stars.h b/src/stars/GEAR/stars.h
index 0645f56e7d97ea277f23052dde9c2f634bb501f8..86924a5479f142564c2603a06ca66a2f61bcb411 100644
--- a/src/stars/GEAR/stars.h
+++ b/src/stars/GEAR/stars.h
@@ -39,7 +39,57 @@ __attribute__((always_inline)) INLINE static float stars_compute_timestep(
     const int with_cosmology, const struct cosmology* cosmo,
     const double time) {
 
-  return FLT_MAX;
+  /* Background star particles have no time-step limits */
+  if (sp->birth_time == -1.) {
+    return FLT_MAX;
+  }
+
+  /* Star age (in internal units) */
+  double star_age;
+  if (with_cosmology) {
+
+    /* Deal with rounding issues */
+    if (sp->birth_scale_factor >= cosmo->a) {
+      star_age = 0.;
+    } else {
+      star_age = cosmology_get_delta_time_from_scale_factors(
+          cosmo, sp->birth_scale_factor, cosmo->a);
+    }
+  } else {
+    star_age = time - sp->birth_time;
+  }
+
+  /* Note (01.08.2024):
+     The following lines come from the EAGLE model. However, in GEAR, a star
+     particle can now be a stellar population, a single star on a part of a
+     population (continuous IMF).
+     The two populations types are similar in behaviour and I think we can use
+     the code below as-is. However, the single stars must be treated in a
+     different way. They only have one SN feedback. Thus, they can be 'old' only
+     after this explosion. The question is how to do that...
+     Maybe we should flag the stars as being old or young? The discrete stars
+     then will only become old after their SN feedback.
+
+     Notice however that this require knowledge of the star_type, which is
+     currently in the feedback module. I planned to move this from the feedback
+     to the stars module (it makes more sense to be with the stars), but this
+     require care because the sink module also need to know about this
+     star_type. Moving the star type to the stars module simplifies the above
+     since we would know the type of the star. Hence, no flag is needed.
+
+     The purpose of taking care about the discrete stars is to avoid them
+     having small timesteps when they are already dead. They won't do anything
+     anymore so they don't need to be waken up often.
+  */
+
+  /* What age category are we in? */
+  if (star_age > stars_properties->age_threshold_unlimited) {
+    return FLT_MAX;
+  } else if (star_age > stars_properties->age_threshold) {
+    return stars_properties->max_time_step_old;
+  } else {
+    return stars_properties->max_time_step_young;
+  }
 }
 
 /**
@@ -99,6 +149,9 @@ __attribute__((always_inline)) INLINE static void stars_first_init_spart(
 
   sp->time_bin = 0;
 
+  if (stars_properties->overwrite_birth_time)
+    sp->birth_time = stars_properties->spart_first_init_birth_time;
+
   stars_init_spart(sp);
 }
 
diff --git a/src/stars/GEAR/stars_iact.h b/src/stars/GEAR/stars_iact.h
index 8c4f4e1196a5bd12f0e6f2cb7dc2fc7e88f23c87..456aeba25d534ebd3f993aa9fd7ad7bb9d737255 100644
--- a/src/stars/GEAR/stars_iact.h
+++ b/src/stars/GEAR/stars_iact.h
@@ -32,7 +32,7 @@
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_stars_density(const float r2, const float *dx,
+runner_iact_nonsym_stars_density(const float r2, const float dx[3],
                                  const float hi, const float hj,
                                  struct spart *restrict si,
                                  const struct part *restrict pj, const float a,
@@ -76,7 +76,7 @@ runner_iact_nonsym_stars_density(const float r2, const float *dx,
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_stars_feedback(const float r2, const float *dx,
+runner_iact_nonsym_stars_feedback(const float r2, const float dx[3],
                                   const float hi, const float hj,
                                   struct spart *restrict si,
                                   struct part *restrict pj, const float a,
diff --git a/src/stars/GEAR/stars_io.h b/src/stars/GEAR/stars_io.h
index 041551d96c2c545ce8bb51eb2fd89e0e16e27362..e24934b6ae8695026889490675e1c9f1d3e33274 100644
--- a/src/stars/GEAR/stars_io.h
+++ b/src/stars/GEAR/stars_io.h
@@ -21,6 +21,7 @@
 
 #include "io_properties.h"
 #include "kick.h"
+#include "stars/GEAR/stars_stellar_type.h"
 #include "stars_part.h"
 
 /**
@@ -35,7 +36,7 @@ INLINE static void stars_read_particles(struct spart *sparts,
                                         int *num_fields) {
 
   /* Say how much we want to read */
-  *num_fields = 6;
+  *num_fields = 7;
 
   /* List what we want to read */
   list[0] = io_make_input_field("Coordinates", DOUBLE, 3, COMPULSORY,
@@ -50,6 +51,12 @@ INLINE static void stars_read_particles(struct spart *sparts,
                                 UNIT_CONV_LENGTH, sparts, h);
   list[5] = io_make_input_field("BirthTime", FLOAT, 1, OPTIONAL, UNIT_CONV_MASS,
                                 sparts, birth_time);
+
+  /* By default, stars are set to star_population */
+  const int default_star_type = 0;
+  list[6] = io_make_input_field_default("StellarParticleType", INT, 1, OPTIONAL,
+                                        UNIT_CONV_NO_UNITS, sparts, star_type,
+                                        default_star_type);
 }
 
 INLINE static void convert_spart_pos(const struct engine *e,
@@ -129,7 +136,7 @@ INLINE static void stars_write_particles(const struct spart *sparts,
                                          const int with_cosmology) {
 
   /* Say how much we want to write */
-  *num_fields = 7;
+  *num_fields = 8;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_spart(
@@ -144,18 +151,19 @@ INLINE static void stars_write_particles(const struct spart *sparts,
   list[2] = io_make_output_field("Masses", FLOAT, 1, UNIT_CONV_MASS, 0.f,
                                  sparts, mass, "Masses of the particles");
 
-  list[3] =
-      io_make_output_field("ParticleIDs", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f,
-                           sparts, id, "Unique IDs of the particles");
+  list[3] = io_make_physical_output_field(
+      "ParticleIDs", LONGLONG, 1, UNIT_CONV_NO_UNITS, 0.f, sparts, id,
+      /*can convert to comoving=*/0, "Unique IDs of the particles");
 
   list[4] = io_make_output_field(
       "SmoothingLengths", FLOAT, 1, UNIT_CONV_LENGTH, 1.f, sparts, h,
       "Co-moving smoothing lengths (FWHM of the kernel) of the particles");
 
   if (with_cosmology) {
-    list[5] = io_make_output_field(
+    list[5] = io_make_physical_output_field(
         "BirthScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, sparts,
-        birth_scale_factor, "Scale-factors at which the stars were born");
+        birth_scale_factor, /*can convert to comoving=*/0,
+        "Scale-factors at which the stars were born");
   } else {
     list[5] = io_make_output_field("BirthTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f,
                                    sparts, birth_time,
@@ -166,6 +174,12 @@ INLINE static void stars_write_particles(const struct spart *sparts,
       "Potentials", FLOAT, 1, UNIT_CONV_POTENTIAL, -1.f, sparts,
       convert_spart_potential, "Gravitational potentials of the particles");
 
+  list[7] =
+      io_make_output_field("StellarParticleTypes", CHAR, 1, UNIT_CONV_NO_UNITS,
+                           0.f, sparts, star_type,
+                           "Type of stellar particle: 0=single star ; 1=stellar"
+                           " cont. IMF part.  ; 2=normal");
+
 #ifdef DEBUG_INTERACTIONS_STARS
 
   list += *num_fields;
@@ -229,6 +243,44 @@ INLINE static void stars_props_init(struct stars_props *sp,
     sp->log_max_h_change = p->log_max_h_change;
   else
     sp->log_max_h_change = logf(powf(max_volume_change, hydro_dimension_inv));
+
+  /* Maximal time-step lengths */
+  const double max_time_step_young_Myr = parser_get_opt_param_float(
+      params, "Stars:max_timestep_young_Myr", FLT_MAX);
+  const double max_time_step_old_Myr =
+      parser_get_opt_param_float(params, "Stars:max_timestep_old_Myr", FLT_MAX);
+  const double age_threshold_Myr = parser_get_opt_param_float(
+      params, "Stars:timestep_age_threshold_Myr", FLT_MAX);
+  const double age_threshold_unlimited_Myr = parser_get_opt_param_float(
+      params, "Stars:timestep_age_threshold_unlimited_Myr", 0.);
+
+  /* Check for consistency */
+  if (age_threshold_unlimited_Myr != 0. && age_threshold_Myr != FLT_MAX) {
+    if (age_threshold_unlimited_Myr < age_threshold_Myr)
+      error(
+          "The age threshold for unlimited stellar time-step sizes (%e Myr) is "
+          "smaller than the transition threshold from young to old ages (%e "
+          "Myr)",
+          age_threshold_unlimited_Myr, age_threshold_Myr);
+  }
+
+  /* Convert to internal units */
+  const double Myr_internal_units = 1e6 * phys_const->const_year;
+  sp->max_time_step_young = max_time_step_young_Myr * Myr_internal_units;
+  sp->max_time_step_old = max_time_step_old_Myr * Myr_internal_units;
+  sp->age_threshold = age_threshold_Myr * Myr_internal_units;
+  sp->age_threshold_unlimited =
+      age_threshold_unlimited_Myr * Myr_internal_units;
+
+  /* Do we want to overwrite the stars' birth properties? */
+  sp->overwrite_birth_time =
+      parser_get_opt_param_int(params, "Stars:overwrite_birth_time", 0);
+
+  /* Read birth time to set all stars in ICs */
+  if (sp->overwrite_birth_time) {
+    sp->spart_first_init_birth_time =
+        parser_get_param_float(params, "Stars:birth_time");
+  }
 }
 
 /**
@@ -252,6 +304,10 @@ INLINE static void stars_props_print(const struct stars_props *sp) {
 
   message("Maximal iterations in ghost task set to %d",
           sp->max_smoothing_iterations);
+
+  if (sp->overwrite_birth_time)
+    message("Stars' birth time read from the ICs will be overwritten to %f",
+            sp->spart_first_init_birth_time);
 }
 
 #if defined(HAVE_HDF5)
diff --git a/src/stars/GEAR/stars_part.h b/src/stars/GEAR/stars_part.h
index d9d73c1f3e2da40aeef359e576a3d9b298b10a5b..9be7ce07cbb70c63406238dbb565ff9c29a3e5c1 100644
--- a/src/stars/GEAR/stars_part.h
+++ b/src/stars/GEAR/stars_part.h
@@ -28,6 +28,7 @@
 #include "particle_splitting_struct.h"
 #include "rt_struct.h"
 #include "star_formation_struct.h"
+#include "stars_stellar_type.h"
 #include "tracers_struct.h"
 
 /**
@@ -81,6 +82,8 @@ struct spart {
     float birth_scale_factor;
   };
 
+  enum stellar_type star_type;
+
   /*! Star formation struct */
   struct star_formation_spart_data sf_data;
 
@@ -107,6 +110,9 @@ struct spart {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
 #ifdef SWIFT_DEBUG_CHECKS
 
   /* Time of the last drift */
@@ -155,6 +161,25 @@ struct stars_props {
 
   /*! Maximal change of h over one time-step */
   float log_max_h_change;
+
+  /*! Maximal time-step length of young stars (internal units) */
+  double max_time_step_young;
+
+  /*! Maximal time-step length of old stars (internal units) */
+  double max_time_step_old;
+
+  /*! Age threshold for the young/old transition (internal units) */
+  double age_threshold;
+
+  /*! Age threshold for the transition to unlimited time-step size (internal
+   * units) */
+  double age_threshold_unlimited;
+
+  /*! Are we overwriting the stars' birth time read from the ICs? */
+  int overwrite_birth_time;
+
+  /*! Value to set birth time of stars read from ICs */
+  float spart_first_init_birth_time;
 };
 
 #endif /* SWIFT_GEAR_STAR_PART_H */
diff --git a/src/stars/GEAR/stars_stellar_type.h b/src/stars/GEAR/stars_stellar_type.h
new file mode 100644
index 0000000000000000000000000000000000000000..ed66ad63b3c3eaa7511cc1013dbd3b0d2899bd85
--- /dev/null
+++ b/src/stars/GEAR/stars_stellar_type.h
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2024 Darwin Roduit (darwin.roduit@alumni.epfl.ch)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_STARS_GEAR_STELLAR_TYPE_H
+#define SWIFT_STARS_GEAR_STELLAR_TYPE_H
+
+/**
+ * @file src/stars/GEAR/stars_stellar_type.h
+ * @brief header file concerning the stellar type of the star particle.
+ **/
+
+/**
+ * @brief The stellar type.
+ *
+ * Star particles can represent a single star ("single_star"), a stellar
+ * population from a continuous IMF or a stellar population from a whole IMF.
+ */
+enum stellar_type {
+  single_star = 0,                /* particle representing a single star */
+  star_population_continuous_IMF, /* particle representing a population of the
+                                     continuous part of the IMF */
+  star_population, /* particle representing a population with the whole IMF */
+  stellar_type_count
+};
+
+#endif /* SWIFT_STARS_GEAR_STELLAR_TYPE_H */
diff --git a/src/stars/None/stars_iact.h b/src/stars/None/stars_iact.h
index 266e7897db17ff7632972915de609b8436b6674d..90c04f40d6c5d55488fb7fc132819ce8af3fa37c 100644
--- a/src/stars/None/stars_iact.h
+++ b/src/stars/None/stars_iact.h
@@ -34,7 +34,7 @@
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_stars_density(const float r2, const float *dx,
+runner_iact_nonsym_stars_density(const float r2, const float dx[3],
                                  const float hi, const float hj,
                                  struct spart *restrict si,
                                  const struct part *restrict pj, const float a,
@@ -55,7 +55,7 @@ runner_iact_nonsym_stars_density(const float r2, const float *dx,
  * @param H Current Hubble parameter.
  */
 __attribute__((always_inline)) INLINE static void
-runner_iact_nonsym_stars_feedback(const float r2, const float *dx,
+runner_iact_nonsym_stars_feedback(const float r2, const float dx[3],
                                   const float hi, const float hj,
                                   struct spart *restrict si,
                                   struct part *restrict pj, const float a,
diff --git a/src/stars/None/stars_part.h b/src/stars/None/stars_part.h
index 2400407ce25a60b504c31bb782730a7dfe6521fc..bf1ae8ee1f2c30416fd0db6585be6fedff2149be 100644
--- a/src/stars/None/stars_part.h
+++ b/src/stars/None/stars_part.h
@@ -58,6 +58,9 @@ struct spart {
   /*! Particle time bin */
   timebin_t time_bin;
 
+  /*! Tree-depth at which size / 2 <= h * gamma < size */
+  char depth_h;
+
   /*! Tracer structure */
   struct tracers_spart_data tracers_data;
 
diff --git a/src/swift.h b/src/swift.h
index 9cee769a6e9d4e92a5a13982795b3a98df5ec18c..323be34ed39f5232ff756dd888f13f237d1214b8 100644
--- a/src/swift.h
+++ b/src/swift.h
@@ -24,6 +24,7 @@
 
 /* Local headers. */
 #include "active.h"
+#include "adaptive_softening.h"
 #include "atomic.h"
 #include "black_holes_properties.h"
 #include "cache.h"
@@ -46,6 +47,7 @@
 #include "feedback.h"
 #include "feedback_properties.h"
 #include "fof.h"
+#include "forcing.h"
 #include "gravity.h"
 #include "gravity_derivatives.h"
 #include "gravity_properties.h"
@@ -85,6 +87,7 @@
 #include "scheduler.h"
 #include "serial_io.h"
 #include "single_io.h"
+#include "sink_iact.h"
 #include "sink_properties.h"
 #include "space.h"
 #include "star_formation.h"
diff --git a/src/intrinsics.h b/src/swift_intrinsics.h
similarity index 100%
rename from src/intrinsics.h
rename to src/swift_intrinsics.h
diff --git a/src/swift_lustre_api.c b/src/swift_lustre_api.c
new file mode 100644
index 0000000000000000000000000000000000000000..93dc8b5d0b1e77c9a27f6f0b2336d9f82f442848
--- /dev/null
+++ b/src/swift_lustre_api.c
@@ -0,0 +1,555 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2025 Peter W. Draper (p.w.draper@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+/* Config parameters. */
+#include <config.h>
+
+/* Standard includes. */
+#include <errno.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/* Local includes. */
+#include "error.h"
+#include "swift_lustre_api.h"
+
+/* Lustre API */
+#ifdef HAVE_LUSTREAPI
+#include <lustre/lustre_user.h>
+#include <lustre/lustreapi.h>
+#endif
+
+/* Number of OSTs to pre-allocate space for. */
+#define PREALLOC (100)
+
+/* Bytes in a TiB */
+#define TiB (1024.0 * 1024.0 * 1024.0)
+
+/* Bytes in a MiB */
+#define MiB (1024.0 * 1024.0)
+
+/**
+ * @brief Allocate storage for a number of OSTs in a OST scan storage struct.
+ *
+ * Note does not reset count or fullcount. Zero these if you want an
+ * empty struct.
+ *
+ * @param ost_infos pointer to the storage structure.
+ * @param size number of OSTs to make space for.
+ */
+void swift_ost_store_alloc(struct swift_ost_store *ost_infos, int size) {
+#ifdef HAVE_LUSTREAPI
+  ost_infos->size = size;
+  ost_infos->infos =
+      (struct swift_ost_info *)malloc(sizeof(struct swift_ost_info) * size);
+  if (ost_infos->infos == NULL)
+    error("Failed to allocate space for an OST scan");
+  memset(ost_infos->infos, 0, sizeof(struct swift_ost_info) * size);
+#endif
+}
+
+/**
+ * @brief Create a copy of an OST scan storage struct.
+ *
+ * @param ost_infos_src pointer to the storage structure to copy
+ * @param ost_infos_dst pointer to a storage structure to populate with
+ *                      the copy. Assumed to have no OST space so not
+ *                      used or initialized.
+ */
+void swift_ost_store_copy(struct swift_ost_store *ost_infos_src,
+                          struct swift_ost_store *ost_infos_dst) {
+#ifdef HAVE_LUSTREAPI
+  ost_infos_dst->size = ost_infos_src->fullcount; /* Used size. */
+  ost_infos_dst->count = ost_infos_src->count;
+  ost_infos_dst->fullcount = ost_infos_src->fullcount;
+  ost_infos_dst->infos = (struct swift_ost_info *)malloc(
+      sizeof(struct swift_ost_info) * ost_infos_dst->size);
+  if (ost_infos_dst->infos == NULL)
+    error("Failed to allocate space for an OST scan copy");
+  memcpy(ost_infos_dst->infos, ost_infos_src->infos,
+         sizeof(struct swift_ost_info) * ost_infos_dst->size);
+#endif
+}
+
+/**
+ * @brief Initialize an OST scan storage structure.
+ *
+ * @param ost_infos pointer to the storage structure.
+ */
+void swift_ost_store_init(struct swift_ost_store *ost_infos) {
+#ifdef HAVE_LUSTREAPI
+  swift_ost_store_alloc(ost_infos, PREALLOC);
+  ost_infos->count = 0;
+  ost_infos->fullcount = 0;
+#endif
+}
+
+/**
+ * @brief Release any storage associated with an OST scan storage structure.
+ *
+ * @param ost_infos pointer to the storage structure.
+ */
+void swift_ost_store_free(struct swift_ost_store *ost_infos) {
+#ifdef HAVE_LUSTREAPI
+  free(ost_infos->infos);
+  ost_infos->infos = NULL;
+  ost_infos->count = 0;
+  ost_infos->fullcount = 0;
+  ost_infos->size = 0;
+#endif
+}
+
+/**
+ * @brief Write about an OST storage structure to a given FILE.
+ *
+ * @param file FILE stream to write output to.
+ * @param ost_infos pointer to the storage structure.
+ */
+void swift_ost_store_write(FILE *file, struct swift_ost_store *ost_infos) {
+#ifdef HAVE_LUSTREAPI
+  fprintf(file, "# %5s %21s %21s %21s\n", "Index", "Size (MiB)", "Used (MiB)",
+          "Free (MiB)");
+  size_t ssum = 0;
+  size_t usum = 0;
+  size_t smin = ost_infos->infos[0].size;
+  size_t smax = 0;
+  size_t umin = ost_infos->infos[0].used;
+  size_t umax = 0;
+
+  for (int i = 0; i < ost_infos->count; i++) {
+    int msize = (int)(ost_infos->infos[i].size / MiB);
+    int mused = (int)(ost_infos->infos[i].used / MiB);
+    fprintf(file, "# %5d %21d %21d %21d\n", ost_infos->infos[i].index, msize,
+            mused, msize - mused);
+
+    ssum += ost_infos->infos[i].size;
+    usum += ost_infos->infos[i].used;
+
+    if (ost_infos->infos[i].size > smax) smax = ost_infos->infos[i].size;
+    if (ost_infos->infos[i].size < smin) smin = ost_infos->infos[i].size;
+
+    if (ost_infos->infos[i].used > umax) umax = ost_infos->infos[i].used;
+    if (ost_infos->infos[i].used < umin) umin = ost_infos->infos[i].used;
+  }
+  if (ost_infos->count == ost_infos->fullcount) {
+    /* Size is for used OSTs not all, so don't report as misleading. */
+    fprintf(file,
+            "# Filesystem size:%.2f TiB used:%.2f TiB free:%.2f TiB %.2f%%\n",
+            ssum / TiB, usum / TiB, (ssum - usum) / TiB,
+            100.0 * (double)(ssum - usum) / (double)ssum);
+    fprintf(file, "# Min/max size: %.2f/%.2f TiB Min/max used: %.2f/%.2f TiB\n",
+            smin / TiB, smax / TiB, umin / TiB, umax / TiB);
+  } else {
+    fprintf(file, "#\n");
+  }
+#endif
+}
+
+/**
+ * @brief Print information about OST storage structure
+ *
+ * @param ost_infos pointer to the storage structure.
+ * @param verbose if non zero additional information will be written
+ *                to stdout.
+ */
+void swift_ost_store_print(struct swift_ost_store *ost_infos, int verbose) {
+#ifdef HAVE_LUSTREAPI
+  message("#  OSTs, using %d of %d", ost_infos->count, ost_infos->fullcount);
+  if (verbose) swift_ost_store_write(stdout, ost_infos);
+#endif
+}
+
+#ifdef HAVE_LUSTREAPI
+/**
+ * @brief Store information about an OST.
+ *
+ * @param ost_infos pointer to the storage structure.
+ * @param index the index, zero based.
+ * @param size the total size in bytes.
+ * @param used the number of bytes used.
+ */
+static void swift_ost_store(struct swift_ost_store *ost_infos, int index,
+                            size_t size, size_t used) {
+  /* Add extra space if needed. Note not thread safe. */
+  if (ost_infos->fullcount == ost_infos->size - 1) {
+    size_t newsize = ost_infos->size + PREALLOC;
+    struct swift_ost_info *newinfos = (struct swift_ost_info *)malloc(
+        sizeof(struct swift_ost_info) * newsize);
+    if (newinfos == NULL) error("Failed to allocate space for OST information");
+    memset(newinfos, 0, sizeof(struct swift_ost_info) * newsize);
+    memcpy(newinfos, ost_infos->infos,
+           sizeof(struct swift_ost_info) * ost_infos->size);
+    free(ost_infos->infos);
+    ost_infos->infos = newinfos;
+    ost_infos->size = newsize;
+  }
+  int count = ost_infos->count++;
+  ost_infos->infos[count].index = index;
+  ost_infos->infos[count].size = size;
+  ost_infos->infos[count].used = used;
+  ost_infos->fullcount = ost_infos->count;
+}
+#endif
+
+/**
+ * @brief Scan the OSTs associated with a lustre file system given a path.
+ *
+ * On exit the ost_infos struct will be populated with the
+ * the number of OSTs found and details of the size and used bytes in each
+ * OST.
+ *
+ * @param path a directory on the lustre file system, ideally the mount point.
+ * @param ost_infos pointer to the storage structure.
+ *
+ * @return 0 on success, otherwise an error will have been reported to stdout.
+ * If an error occurs the store will never be changed.
+ */
+int swift_ost_scan(const char *path, struct swift_ost_store *ost_infos) {
+
+  int rc = 0;
+#ifdef HAVE_LUSTREAPI
+  char mntdir[PATH_MAX] = {0};
+  char fsname[PATH_MAX] = {0};
+  char cpath[PATH_MAX] = {0};
+
+  /* Check this path exists. */
+  if (!realpath(path, cpath)) {
+    rc = errno;
+    message("Not a filesystem path '%s': %s", path, strerror(rc));
+  } else {
+
+    /* Parse the path into the mount point and file system name. */
+    if (llapi_search_mounts(cpath, 0, mntdir, fsname) == 0) {
+      if (mntdir[0] != '\0') {
+        struct obd_statfs stat_buf;
+        struct obd_uuid uuid_buf;
+
+        /* Loop while OSTs are located. */
+        for (int index = 0;; index++) {
+          memset(&stat_buf, 0, sizeof(struct obd_statfs));
+          memset(&uuid_buf, 0, sizeof(struct obd_uuid));
+
+          rc = llapi_obd_statfs(mntdir, LL_STATFS_LOV, index, &stat_buf,
+                                &uuid_buf);
+          rc = -rc;
+          if (rc == ENODEV || rc == EAGAIN || rc == EINVAL || rc == EFAULT) {
+            /* Nothing we can query here, so time to stop search. */
+            break;
+          }
+
+          /* Inactive devices are empty. */
+          if (rc == ENODATA) {
+            swift_ost_store(ost_infos, index, 0, 0);
+          } else {
+            size_t used =
+                (stat_buf.os_blocks - stat_buf.os_bfree) * stat_buf.os_bsize;
+            size_t total = stat_buf.os_blocks * stat_buf.os_bsize;
+            swift_ost_store(ost_infos, index, total, used);
+          }
+        }
+        rc = 0;
+
+      } else {
+        message("No lustre mount point found for path: %s", path);
+        rc = 1;
+      }
+    } else {
+      message("Failed to locate a lustre mount point using path: %s", path);
+      rc = 1;
+    }
+  }
+#endif
+  return rc;
+}
+
+#ifdef HAVE_LUSTREAPI
+/** Comparison function for OST free space. */
+static int ostcmp(const void *p1, const void *p2) {
+  const struct swift_ost_info *i1 = (const struct swift_ost_info *)p1;
+  const struct swift_ost_info *i2 = (const struct swift_ost_info *)p2;
+
+  /* size_t ints so some care is needed to return an int. */
+  size_t f1 = i1->size - i1->used;
+  size_t f2 = i2->size - i2->used;
+  if (f1 < f2) return 1;
+  if (f1 > f2) return -1;
+  return 0;
+}
+#endif
+
+/**
+ * @brief Sort the OSTs into decreasing free space culling those that do not
+ * meet a free space threshold.
+ *
+ * @param ost_infos pointer to populated storage structure.
+ * @param minfree the number of MiB that the OST should be capable of
+ *                storing. Zero for no effect.
+ */
+void swift_ost_cull(struct swift_ost_store *ost_infos, int minfree) {
+#ifdef HAVE_LUSTREAPI
+  /* Sort by free space. */
+  qsort(ost_infos->infos, ost_infos->count, sizeof(struct swift_ost_info),
+        ostcmp);
+
+  /* And cull if needed. */
+  if (minfree > 0) {
+    size_t bytesfree = minfree * (size_t)MiB;
+
+    /* Always keep at least one! */
+    for (int i = 1; i < ost_infos->count; i++) {
+      struct swift_ost_info *curr = &ost_infos->infos[i];
+      if ((curr->size - curr->used) < bytesfree) {
+
+        /* Throw the rest away. Note fullcount now decoupled. */
+        ost_infos->count = i;
+      }
+    }
+  }
+#endif
+}
+
+/**
+ * @brief Get the next OST in an incrementing sequence.
+ *
+ * @param ost_infos pointer to populated storage structure.
+ * @param arrayindex the last used array index, start with 0.
+ *                   This will be wrapped as needed use as input for next
+ *                   call.
+ * @param count number of OSTs that will be used to stripe, that is the
+ *              increment, usually 1. Only makes sense if the OST list is not
+ *              culled as this implicitly assumes OSTs are in index order.
+ * @return the selected OST index.
+ */
+int swift_ost_next(struct swift_ost_store *ost_infos, int *arrayindex,
+                   int count) {
+#ifdef HAVE_LUSTREAPI
+  int index = (*arrayindex % ost_infos->count);
+  *arrayindex = index + count;
+  return ost_infos->infos[index].index;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Remove an OST by index from the store.
+ *
+ * @param ost_infos pointer to populated storage structure.
+ * @param index index of the OST to remove.
+ */
+void swift_ost_remove(struct swift_ost_store *ost_infos, int index) {
+
+#ifdef HAVE_LUSTREAPI
+  /* Find the array index. */
+  int arrayindex = -1;
+  for (int i = 0; i < ost_infos->fullcount; i++) {
+    if (ost_infos->infos[i].index == index) {
+      arrayindex = i;
+      break;
+    }
+  }
+
+  /* Do nothing if not found or we have the end array index. */
+  if ((arrayindex != -1) && arrayindex != (ost_infos->fullcount - 1)) {
+
+    /* Copy remaining infos down one place. Overlapping.. */
+    memmove(&ost_infos->infos[arrayindex], &ost_infos->infos[arrayindex + 1],
+            (ost_infos->fullcount - arrayindex - 1) *
+                sizeof(struct swift_ost_info));
+    if (arrayindex < ost_infos->count) ost_infos->count = ost_infos->count - 1;
+    ost_infos->fullcount = ost_infos->fullcount - 1;
+
+  } else if (arrayindex == ost_infos->fullcount - 1) {
+
+    /* End array index, just adjust counts. */
+    if (arrayindex < ost_infos->count) ost_infos->count = ost_infos->count - 1;
+    ost_infos->fullcount = ost_infos->fullcount - 1;
+  }
+#endif
+}
+
+/**
+ * @brief Create a file with a given OST index and number of OSTs to stripe.
+ *
+ * @param filename name of the file to create.
+ * @param offset index of the first OST used with this file.
+ * @param count number of OSTs to stripe this file over.
+ * @param usedoffset the offset actually used by file.
+ *
+ * @return non-zero if there are problems creating the file.
+ */
+int swift_create_striped_file(const char *filename, int offset, int count,
+                              int *usedoffset) {
+  int rc = 0;
+
+#ifdef HAVE_LUSTREAPI
+  *usedoffset = offset;
+  rc = llapi_file_create(filename, 0 /* Default block size */, offset, count,
+                         LLAPI_LAYOUT_RAID0 /* Pattern default */);
+  if (rc != 0) {
+    rc = -rc;
+    message("Cannot create file %s : %s", filename, strerror(rc));
+  } else {
+
+    /* Recover the file offset of first OST in case it is changed from
+     * operational reasons. */
+    /* Yuk, needs extra space for array os lov_user_ost_data. */
+    size_t sizelum = sizeof(struct lov_user_md) +
+                     LOV_MAX_STRIPE_COUNT * sizeof(struct lov_user_ost_data);
+    struct lov_user_md *lum = (struct lov_user_md *)malloc(sizelum);
+
+    rc = llapi_file_get_stripe(filename, lum);
+    rc = -rc;
+    if (rc == 0) {
+      *usedoffset = lum->lmm_objects[0].l_ost_idx;
+    } else {
+      /* Shouldn't be fatal. */
+      *usedoffset = offset;
+    }
+    free(lum);
+  }
+#endif
+  return rc;
+}
+
+/**
+ * @brief Scan for the available OSTs for a given file path.
+ *
+ * The OSTs will be sorted by free space on exit and may be further selected
+ * to remove OSTs that are too full for use or cannot be written to. If too
+ * many OSTs are rejected it will be considered to be a parameter error and
+ * all OSTs, sorted by free space, will be returned.
+ * We don't want to flood the OSTs with RPC calls so only one MPI rank
+ * should make this call.
+ *
+ * @param ost_infos pointer to empty OST storage struct. Will contain the
+ *                  selected free-space ordered OSTs found on exit. The OST
+ *                  count will remain at zero if anything fails. Note this
+ *                  will need to be freed as usual regardless.
+ * @param filepath  path to a file on the lustre file system. Must not exist.
+ *                  The containing directory must exist and be part of the
+ *                  lustre file system.
+ * @param minfree minimum free space to allow in MiB. -1 for use a guess based
+ * on size of the current process, 0 to disable selection.
+ * @param writetest whether to check if the OSTs are writable. If used
+ *                  the path must be that of a non-existent file on the
+ *                  file system that is writable by the process.
+ * @param verbose if true information about the OSTs and the selections made
+ *                will be output.
+ *
+ */
+void swift_ost_select(struct swift_ost_store *ost_infos, const char *filepath,
+                      int minfree, int writetest, int verbose) {
+
+  /* Initialise the struct. */
+  swift_ost_store_init(ost_infos);
+
+  /* Get directory of filepath. */
+  char *filepathc = strdup(filepath);
+  char *dirp = dirname(filepathc);
+
+  /* Scan for all OSTs. */
+  int rc = swift_ost_scan(dirp, ost_infos);
+  free(dirp);
+
+  /* If does not succeed we do nothing, probably not a lustre mount. */
+  if (rc == 0) {
+    if (verbose) swift_ost_store_print(ost_infos, 1);
+
+    /* Make a copy so we can undo any changes. */
+    struct swift_ost_store ost_infos_full;
+    swift_ost_store_copy(ost_infos, &ost_infos_full);
+
+    /* Cull these so we do not use OSTs with too little free space. Also sorts
+     * into most free space order. If given a value use that, otherwise we use
+     * the resident set size of the process, dumps and restarts are always
+     * smaller than that. */
+    if (minfree != 0) {
+      if (minfree < 0) {
+
+        /* No guarantee this will work, hopefully will return 0 in those cases
+         * and we do nothing. */
+        long size, resident, shared, text, library, data, dirty;
+        memuse_use(&size, &resident, &shared, &text, &data, &library, &dirty);
+
+        /* KiB into MiB. */
+        minfree = (int)(resident / 1024.0);
+      }
+
+      /* And cull and sort. */
+      swift_ost_cull(ost_infos, minfree);
+      if (verbose)
+        message("Rejected %d OSTs using free space threshold %d (MiB)",
+                ost_infos->fullcount - ost_infos->count, minfree);
+    }
+
+    if (writetest != 0) {
+      /* Test writing to all OSTs and remove any that are not writable.  We do
+       * this by creating our file on every OST and checking it was created on
+       * it. */
+      int usedindex = 0;
+      int removed = 0;
+      for (int i = ost_infos->count - 1; i >= 0; i--) {
+        usedindex = ost_infos->infos[i].index;
+        rc = swift_create_striped_file(filepath, ost_infos->infos[i].index, 1,
+                                       &usedindex);
+
+        if (rc != 0) {
+          /* Failed so not likely to succeed next time. Probably file
+           * exists, there is nothing we should do about that, the existing
+           * stripe will be reused, along with the space of the existing file.
+           */
+          message("Failed testing file creation on OSTs, aborting test");
+          break;
+        }
+
+        if (usedindex != ost_infos->infos[i].index) {
+          /* Differing OST indices, so not what we asked for, bye. */
+          swift_ost_remove(ost_infos, ost_infos->infos[i].index);
+          removed++;
+        }
+        unlink(filepath);
+      }
+      if (verbose) message("Rejected %d OSTs as readonly", removed);
+    }
+
+    /* Safety first. If we have too few OSTs left after the above we will
+     * make the choice to do nothing. */
+    if ((ost_infos->fullcount * 0.25 > ost_infos->count) ||
+        ost_infos->count < 2) {
+      message("Too many OSTs have been rejected (%d of %d).",
+              ost_infos->fullcount - ost_infos->count, ost_infos->fullcount);
+      message("Assuming OST rejection is flawed and skipping.");
+      swift_ost_store_copy(&ost_infos_full, ost_infos);
+
+      /* Still good to use a sorted list. */
+      swift_ost_cull(ost_infos, 0);
+    }
+    swift_ost_store_free(&ost_infos_full);
+    if (verbose) swift_ost_store_print(ost_infos, 1);
+
+  } else {
+
+    /* If the scan failed we do nothing, this is probably not a lustre mount. */
+    message("Lustre OST scan failed, is this a lustre mount?");
+  }
+}
diff --git a/src/swift_lustre_api.h b/src/swift_lustre_api.h
new file mode 100644
index 0000000000000000000000000000000000000000..a625a6223164af86a006a0ea08bbd5721a18ad29
--- /dev/null
+++ b/src/swift_lustre_api.h
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2025 Peter W. Draper (p.w.draper@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_LUSTRE_API_H
+#define SWIFT_LUSTRE_API_H
+
+/* For size_t and FILE. */
+#include <stdlib.h>
+
+/* Structure to store information about an OST. */
+struct swift_ost_info {
+  int index;   /* OST index */
+  size_t size; /* Size in bytes */
+  size_t used; /* Used in bytes */
+};
+
+/* Structure to store a scan of all the OSTs for a mount point. */
+struct swift_ost_store {
+  struct swift_ost_info *infos;
+  int count;     /* Count of active OSTs */
+  int fullcount; /* Count of OSTs available (only different when culled) */
+  int size;      /* Space available for storing OST infos */
+};
+
+/* Public functions. */
+
+/* OST scanning and selection. */
+void swift_ost_select(struct swift_ost_store *ost_infos, const char *path,
+                      int minfree, int writetest, int verbose);
+int swift_ost_scan(const char *path, struct swift_ost_store *ost_infos);
+void swift_ost_cull(struct swift_ost_store *ost_infos, int minfree);
+void swift_ost_remove(struct swift_ost_store *ost_infos, int index);
+int swift_ost_next(struct swift_ost_store *ost_infos, int *arrayindex,
+                   int count);
+
+/* OST store. */
+void swift_ost_store_init(struct swift_ost_store *ost_infos);
+void swift_ost_store_alloc(struct swift_ost_store *ost_infos, int size);
+void swift_ost_store_copy(struct swift_ost_store *ost_infos_src,
+                          struct swift_ost_store *ost_infos_dst);
+void swift_ost_store_free(struct swift_ost_store *ost_infos);
+void swift_ost_store_print(struct swift_ost_store *ost_infos, int verbose);
+void swift_ost_store_write(FILE *file, struct swift_ost_store *ost_infos);
+
+/* File striping. */
+int swift_create_striped_file(const char *filename, int offset, int count,
+                              int *usedoffset);
+
+#endif /* SWIFT_LUSTRE_API_H */
diff --git a/src/symmetric_matrix.h b/src/symmetric_matrix.h
new file mode 100644
index 0000000000000000000000000000000000000000..88c82df542881a43e6332892f82d2dfb9e94e69a
--- /dev/null
+++ b/src/symmetric_matrix.h
@@ -0,0 +1,167 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023  Matthieu Schaller (schaller@strw.leidenuniv.nl)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_SYMMETRIC_MATRIX_H
+#define SWIFT_SYMMETRIC_MATRIX_H
+
+/* Local includes */
+#include "dimension.h"
+#include "error.h"
+
+/**
+ * @brief Symmetric matrix definition in 3D.
+ *
+ * The matrix elements can be accessed as an array or via their "coordinates".
+ * Replicated elements are not stored.
+ */
+struct sym_matrix {
+
+  union {
+    struct {
+      float elements[6];
+    };
+    struct {
+      float xx;
+      float yy;
+      float zz;
+      float xy;
+      float xz;
+      float yz;
+    };
+  };
+};
+
+/**
+ * @brief Zero the matrix
+ */
+__attribute__((always_inline)) INLINE static void zero_sym_matrix(
+    struct sym_matrix *M) {
+  for (int i = 0; i < 6; ++i) M->elements[i] = 0.f;
+}
+
+/**
+ * @brief Construct a 3x3 array from a symmetric matrix.
+ */
+__attribute__((always_inline)) INLINE static void get_matrix_from_sym_matrix(
+    float out[3][3], const struct sym_matrix *in) {
+
+  out[0][0] = in->xx;
+  out[0][1] = in->xy;
+  out[0][2] = in->xz;
+  out[1][0] = in->xy;
+  out[1][1] = in->yy;
+  out[1][2] = in->yz;
+  out[2][0] = in->xz;
+  out[2][1] = in->yz;
+  out[2][2] = in->zz;
+}
+
+/**
+ * @brief Construct a symmetric matrix from a 3x3 array.
+ *
+ * No check is performed to verify the input 3x3 array is indeed symmetric.
+ */
+__attribute__((always_inline)) INLINE static void get_sym_matrix_from_matrix(
+    struct sym_matrix *out, const float in[3][3]) {
+  out->xx = in[0][0];
+  out->yy = in[1][1];
+  out->zz = in[2][2];
+  out->xy = in[0][1];
+  out->xz = in[0][2];
+  out->yz = in[1][2];
+}
+
+/**
+ * @brief Compute the product of a symmetric matrix and a vector.
+ */
+__attribute__((always_inline)) INLINE static void sym_matrix_multiply_by_vector(
+    float out[3], const struct sym_matrix *M, const float v[3]) {
+
+  out[0] = M->xx * v[0] + M->xy * v[1] + M->xz * v[2];
+  out[1] = M->xy * v[0] + M->yy * v[1] + M->yz * v[2];
+  out[2] = M->xz * v[0] + M->yz * v[1] + M->zz * v[2];
+}
+
+/**
+ * @brief Multiply two symmetric matrices in two operations, ABA.
+ */
+__attribute__((always_inline)) INLINE static void sym_matrix_multiplication_ABA(
+    struct sym_matrix *M_out, const struct sym_matrix *A,
+    const struct sym_matrix *B) {
+
+  float BA_array[3][3] = {0};
+  float A_array[3][3], B_array[3][3];
+  get_matrix_from_sym_matrix(A_array, A);
+  get_matrix_from_sym_matrix(B_array, B);
+  for (int i = 0; i < 3; i++) {
+    for (int j = 0; j < 3; j++) {
+      for (int k = 0; k < 3; k++) {
+        BA_array[i][j] += B_array[i][k] * A_array[k][j];
+      }
+    }
+  }
+
+  M_out->xx = (A->xx * BA_array[0][0] + A->xy * BA_array[1][0] +
+               A->xz * BA_array[2][0]);
+  M_out->yy = (A->xy * BA_array[0][1] + A->yy * BA_array[1][1] +
+               A->yz * BA_array[2][1]);
+  M_out->zz = (A->xz * BA_array[0][2] + A->yz * BA_array[1][2] +
+               A->zz * BA_array[2][2]);
+  M_out->xy = (A->xy * BA_array[0][0] + A->yy * BA_array[1][0] +
+               A->yz * BA_array[2][0]);
+  M_out->xz = (A->xz * BA_array[0][0] + A->yz * BA_array[1][0] +
+               A->zz * BA_array[2][0]);
+  M_out->yz = (A->xz * BA_array[0][1] + A->yz * BA_array[1][1] +
+               A->zz * BA_array[2][1]);
+}
+
+/**
+ * @brief Print a symmetric matrix.
+ */
+__attribute__((always_inline)) INLINE static void sym_matrix_print(
+    const struct sym_matrix *M) {
+  message("|%.4f %.4f %.4f|", M->xx, M->xy, M->xz);
+  message("|%.4f %.4f %.4f|", M->xy, M->yy, M->yz);
+  message("|%.4f %.4f %.4f|", M->xz, M->yz, M->zz);
+}
+
+/**
+ * @brief Compute the inverse of a symmetric matrix and a vector.
+ *
+ * Returned as a symmetric matrix
+ */
+__attribute__((always_inline)) INLINE static void sym_matrix_invert(
+    struct sym_matrix *M_inv, const struct sym_matrix *M) {
+
+  float M_inv_matrix[3][3];
+  get_matrix_from_sym_matrix(M_inv_matrix, M);
+  int res = invert_dimension_by_dimension_matrix(M_inv_matrix);
+  if (res) {
+    sym_matrix_print(M);
+    error("Error inverting matrix");
+  }
+
+  M_inv->xx = M_inv_matrix[0][0];
+  M_inv->yy = M_inv_matrix[1][1];
+  M_inv->zz = M_inv_matrix[2][2];
+  M_inv->xy = M_inv_matrix[0][1];
+  M_inv->xz = M_inv_matrix[0][2];
+  M_inv->yz = M_inv_matrix[1][2];
+}
+
+#endif /* SWIFT_SYMMETRIC_MATRIX_H */
diff --git a/src/task.c b/src/task.c
index 0fff5fd03b88f9ed86ca8ae00ee0e474ad56627e..9a4c7f2cb2311bcd5e88e9f67b07078b4f52b067 100644
--- a/src/task.c
+++ b/src/task.c
@@ -110,8 +110,11 @@ const char *taskID_names[task_type_count] = {
     "bh_swallow_ghost3",
     "fof_self",
     "fof_pair",
+    "fof_attach_self",
+    "fof_attach_pair",
     "neutrino_weight",
     "sink_in",
+    "sink_density_ghost",
     "sink_ghost1",
     "sink_ghost2",
     "sink_out",
@@ -150,6 +153,7 @@ const char *subtaskID_names[task_subtype_count] = {
     "stars_prep2",
     "stars_feedback",
     "sf_counts",
+    "grav_counts",
     "bpart_rho",
     "bpart_feedback",
     "bh_density",
@@ -157,6 +161,7 @@ const char *subtaskID_names[task_subtype_count] = {
     "do_gas_swallow",
     "do_bh_swallow",
     "bh_feedback",
+    "sink_density",
     "sink_do_sink_swallow",
     "sink_swallow",
     "sink_do_gas_swallow",
@@ -185,22 +190,22 @@ MPI_Comm subtaskMPI_comms[task_subtype_count];
  * @param ARRAY is the array of this specific type.
  * @param COUNT is the number of elements in the array.
  */
-#define TASK_CELL_OVERLAP(TYPE, ARRAY, COUNT)                           \
-  __attribute__((always_inline))                                        \
-  INLINE static size_t task_cell_overlap_##TYPE(                        \
-      const struct cell *restrict ci, const struct cell *restrict cj) { \
-                                                                        \
-    if (ci == NULL || cj == NULL) return 0;                             \
-                                                                        \
-    if (ci->ARRAY <= cj->ARRAY &&                                       \
-        ci->ARRAY + ci->COUNT >= cj->ARRAY + cj->COUNT) {               \
-      return cj->COUNT;                                                 \
-    } else if (cj->ARRAY <= ci->ARRAY &&                                \
-               cj->ARRAY + cj->COUNT >= ci->ARRAY + ci->COUNT) {        \
-      return ci->COUNT;                                                 \
-    }                                                                   \
-                                                                        \
-    return 0;                                                           \
+#define TASK_CELL_OVERLAP(TYPE, ARRAY, COUNT)                    \
+  __attribute__((always_inline)) INLINE static size_t            \
+      task_cell_overlap_##TYPE(const struct cell *restrict ci,   \
+                               const struct cell *restrict cj) { \
+                                                                 \
+    if (ci == NULL || cj == NULL) return 0;                      \
+                                                                 \
+    if (ci->ARRAY <= cj->ARRAY &&                                \
+        ci->ARRAY + ci->COUNT >= cj->ARRAY + cj->COUNT) {        \
+      return cj->COUNT;                                          \
+    } else if (cj->ARRAY <= ci->ARRAY &&                         \
+               cj->ARRAY + cj->COUNT >= ci->ARRAY + ci->COUNT) { \
+      return ci->COUNT;                                          \
+    }                                                            \
+                                                                 \
+    return 0;                                                    \
   }
 
 TASK_CELL_OVERLAP(part, hydro.parts, hydro.count);
@@ -245,6 +250,7 @@ __attribute__((always_inline)) INLINE static enum task_actions task_acts_on(
       break;
 
     case task_type_drift_sink:
+    case task_type_sink_density_ghost:
       return task_action_sink;
       break;
 
@@ -290,6 +296,7 @@ __attribute__((always_inline)) INLINE static enum task_actions task_acts_on(
           return task_action_bpart;
           break;
 
+        case task_subtype_sink_density:
         case task_subtype_sink_do_gas_swallow:
         case task_subtype_sink_do_sink_swallow:
         case task_subtype_sink_swallow:
@@ -320,6 +327,8 @@ __attribute__((always_inline)) INLINE static enum task_actions task_acts_on(
     case task_type_csds:
     case task_type_fof_self:
     case task_type_fof_pair:
+    case task_type_fof_attach_self:
+    case task_type_fof_attach_pair:
     case task_type_timestep:
     case task_type_timestep_limiter:
     case task_type_timestep_sync:
@@ -570,16 +579,13 @@ void task_unlock(struct task *t) {
         cell_gunlocktree(ci);
         cell_munlocktree(ci);
 #endif
-      } else if (subtype == task_subtype_sink_swallow) {
+      } else if ((subtype == task_subtype_sink_density) ||
+                 (subtype == task_subtype_sink_swallow) ||
+                 (subtype == task_subtype_sink_do_gas_swallow)) {
         cell_sink_unlocktree(ci);
         cell_unlocktree(ci);
       } else if (subtype == task_subtype_sink_do_sink_swallow) {
         cell_sink_unlocktree(ci);
-        cell_gunlocktree(ci);
-      } else if (subtype == task_subtype_sink_do_gas_swallow) {
-        cell_unlocktree(ci);
-        cell_sink_unlocktree(ci);
-        cell_gunlocktree(ci);
       } else if ((subtype == task_subtype_stars_density) ||
                  (subtype == task_subtype_stars_prep1) ||
                  (subtype == task_subtype_stars_prep2) ||
@@ -612,7 +618,9 @@ void task_unlock(struct task *t) {
         cell_munlocktree(ci);
         cell_munlocktree(cj);
 #endif
-      } else if (subtype == task_subtype_sink_swallow) {
+      } else if ((subtype == task_subtype_sink_density) ||
+                 (subtype == task_subtype_sink_swallow) ||
+                 (subtype == task_subtype_sink_do_gas_swallow)) {
         cell_sink_unlocktree(ci);
         cell_sink_unlocktree(cj);
         cell_unlocktree(ci);
@@ -620,15 +628,6 @@ void task_unlock(struct task *t) {
       } else if (subtype == task_subtype_sink_do_sink_swallow) {
         cell_sink_unlocktree(ci);
         cell_sink_unlocktree(cj);
-        cell_gunlocktree(ci);
-        cell_gunlocktree(cj);
-      } else if (subtype == task_subtype_sink_do_gas_swallow) {
-        cell_sink_unlocktree(ci);
-        cell_sink_unlocktree(cj);
-        cell_unlocktree(ci);
-        cell_unlocktree(cj);
-        cell_gunlocktree(ci);
-        cell_gunlocktree(cj);
       } else if ((subtype == task_subtype_stars_density) ||
                  (subtype == task_subtype_stars_prep1) ||
                  (subtype == task_subtype_stars_prep2) ||
@@ -679,6 +678,17 @@ void task_unlock(struct task *t) {
 #endif
       break;
 
+    case task_type_fof_self:
+    case task_type_fof_attach_self:
+      cell_gunlocktree(ci);
+      break;
+
+    case task_type_fof_pair:
+    case task_type_fof_attach_pair:
+      cell_gunlocktree(ci);
+      cell_gunlocktree(cj);
+      break;
+
     case task_type_star_formation:
       cell_unlocktree(ci);
       cell_sunlocktree(ci);
@@ -803,15 +813,9 @@ int task_lock(struct task *t) {
           return 0;
         }
 #endif
-      } else if (subtype == task_subtype_sink_do_sink_swallow) {
-        if (ci->sinks.hold) return 0;
-        if (ci->grav.phold) return 0;
-        if (cell_sink_locktree(ci) != 0) return 0;
-        if (cell_glocktree(ci) != 0) {
-          cell_sink_unlocktree(ci);
-          return 0;
-        }
-      } else if (subtype == task_subtype_sink_swallow) {
+      } else if ((subtype == task_subtype_sink_density) ||
+                 (subtype == task_subtype_sink_swallow) ||
+                 (subtype == task_subtype_sink_do_gas_swallow)) {
         if (ci->sinks.hold) return 0;
         if (ci->hydro.hold) return 0;
         if (cell_sink_locktree(ci) != 0) return 0;
@@ -819,20 +823,9 @@ int task_lock(struct task *t) {
           cell_sink_unlocktree(ci);
           return 0;
         }
-      } else if (subtype == task_subtype_sink_do_gas_swallow) {
+      } else if (subtype == task_subtype_sink_do_sink_swallow) {
         if (ci->sinks.hold) return 0;
-        if (ci->grav.phold) return 0;
-        if (ci->hydro.hold) return 0;
         if (cell_sink_locktree(ci) != 0) return 0;
-        if (cell_locktree(ci) != 0) {
-          cell_sink_unlocktree(ci);
-          return 0;
-        }
-        if (cell_glocktree(ci) != 0) {
-          cell_sink_unlocktree(ci);
-          cell_unlocktree(ci);
-          return 0;
-        }
       } else if ((subtype == task_subtype_stars_density) ||
                  (subtype == task_subtype_stars_prep1) ||
                  (subtype == task_subtype_stars_prep2) ||
@@ -890,8 +883,9 @@ int task_lock(struct task *t) {
           return 0;
         }
 #endif
-      } else if (subtype == task_subtype_sink_swallow) {
-        /* Lock the sinks and the gas particles in both cells */
+      } else if ((subtype == task_subtype_sink_density) ||
+                 (subtype == task_subtype_sink_swallow) ||
+                 (subtype == task_subtype_sink_do_gas_swallow)) {
         if (ci->sinks.hold || cj->sinks.hold) return 0;
         if (ci->hydro.hold || cj->hydro.hold) return 0;
         if (cell_sink_locktree(ci) != 0) return 0;
@@ -910,62 +904,13 @@ int task_lock(struct task *t) {
           cell_unlocktree(ci);
           return 0;
         }
-      } else if (subtype == task_subtype_sink_do_gas_swallow) {
-        /* Lock the sinks and the gas particles in both cells */
-        if (ci->sinks.hold || cj->sinks.hold) return 0;
-        if (ci->hydro.hold || cj->hydro.hold) return 0;
-        if (ci->grav.phold || cj->grav.phold) return 0;
-        if (cell_sink_locktree(ci) != 0) return 0;
-        if (cell_sink_locktree(cj) != 0) {
-          cell_sink_unlocktree(ci);
-          return 0;
-        }
-        if (cell_locktree(ci) != 0) {
-          cell_sink_unlocktree(ci);
-          cell_sink_unlocktree(cj);
-          return 0;
-        }
-        if (cell_locktree(cj) != 0) {
-          cell_sink_unlocktree(ci);
-          cell_sink_unlocktree(cj);
-          cell_unlocktree(ci);
-          return 0;
-        }
-        if (cell_glocktree(ci) != 0) {
-          cell_sink_unlocktree(ci);
-          cell_sink_unlocktree(cj);
-          cell_unlocktree(ci);
-          cell_unlocktree(cj);
-          return 0;
-        }
-        if (cell_glocktree(cj) != 0) {
-          cell_sink_unlocktree(ci);
-          cell_sink_unlocktree(cj);
-          cell_unlocktree(ci);
-          cell_unlocktree(cj);
-          cell_gunlocktree(ci);
-          return 0;
-        }
       } else if (subtype == task_subtype_sink_do_sink_swallow) {
-        /* Lock the sink and the dm particles in both cells */
         if (ci->sinks.hold || cj->sinks.hold) return 0;
-        if (ci->grav.phold || cj->grav.phold) return 0;
         if (cell_sink_locktree(ci) != 0) return 0;
         if (cell_sink_locktree(cj) != 0) {
           cell_sink_unlocktree(ci);
           return 0;
         }
-        if (cell_glocktree(ci) != 0) {
-          cell_sink_unlocktree(ci);
-          cell_sink_unlocktree(cj);
-          return 0;
-        }
-        if (cell_glocktree(cj) != 0) {
-          cell_sink_unlocktree(ci);
-          cell_sink_unlocktree(cj);
-          cell_gunlocktree(ci);
-          return 0;
-        }
       } else if ((subtype == task_subtype_stars_density) ||
                  (subtype == task_subtype_stars_prep1) ||
                  (subtype == task_subtype_stars_prep2) ||
@@ -1072,6 +1017,24 @@ int task_lock(struct task *t) {
 #endif
       break;
 
+    case task_type_fof_self:
+    case task_type_fof_attach_self:
+      /* Lock the gpart as this this what we act on */
+      if (ci->grav.phold) return 0;
+      if (cell_glocktree(ci) != 0) return 0;
+      break;
+
+    case task_type_fof_pair:
+    case task_type_fof_attach_pair:
+      /* Lock the gpart as this this what we act on */
+      if (ci->grav.phold || cj->grav.phold) return 0;
+      if (cell_glocktree(ci) != 0) return 0;
+      if (cell_glocktree(cj) != 0) {
+        cell_gunlocktree(ci);
+        return 0;
+      }
+      break;
+
     case task_type_star_formation:
       /* Lock the gas, gravity and star particles */
       if (ci->hydro.hold || ci->stars.hold || ci->grav.phold) return 0;
@@ -1088,7 +1051,7 @@ int task_lock(struct task *t) {
       break;
 
     case task_type_star_formation_sink:
-      /* Lock the gas, gravity and star particles */
+      /* Lock the sinks, gravity and star particles */
       if (ci->sinks.hold || ci->stars.hold || ci->grav.phold) return 0;
       if (cell_sink_locktree(ci) != 0) return 0;
       if (cell_slocktree(ci) != 0) {
@@ -1103,7 +1066,7 @@ int task_lock(struct task *t) {
       break;
 
     case task_type_sink_formation:
-      /* Lock the gas, gravity and star particles */
+      /* Lock the gas, sinks and star particles */
       if (ci->hydro.hold || ci->sinks.hold || ci->grav.phold) return 0;
       if (cell_locktree(ci) != 0) return 0;
       if (cell_sink_locktree(ci) != 0) {
@@ -1237,14 +1200,17 @@ void task_get_group_name(int type, int subtype, char *cluster) {
         strcpy(cluster, "RTtransport");
       }
       break;
+    case task_subtype_sink_density:
+      strcpy(cluster, "SinkDensity");
+      break;
     case task_subtype_sink_swallow:
-      strcpy(cluster, "SinkFormation");
+      strcpy(cluster, "SinkSwallow");
       break;
     case task_subtype_sink_do_sink_swallow:
-      strcpy(cluster, "SinkMerger");
+      strcpy(cluster, "DoSinkSwallow");
       break;
     case task_subtype_sink_do_gas_swallow:
-      strcpy(cluster, "SinkAccretion");
+      strcpy(cluster, "DoGasSwallow");
       break;
     default:
       strcpy(cluster, "None");
@@ -1369,7 +1335,6 @@ void task_dump_all(struct engine *e, int step) {
               engine_rank, (long long int)e->tic_step,
               (long long int)e->toc_step, e->updates, e->g_updates,
               e->s_updates, cpufreq);
-      int count = 0;
       for (int l = 0; l < e->sched.nr_tasks; l++) {
         if (!e->sched.tasks[l].implicit &&
             e->sched.tasks[l].tic > e->tic_step) {
@@ -1389,7 +1354,6 @@ void task_dump_all(struct engine *e, int step) {
                                              : 0,
               e->sched.tasks[l].flags, e->sched.tasks[l].sid);
         }
-        count++;
       }
       fclose(file_thread);
     }
@@ -1542,32 +1506,33 @@ void task_dump_stats(const char *dumpfile, struct engine *e,
     /* Get these from all ranks for output from rank 0. Could wrap these into a
      * single operation. */
     size_t size = task_type_count * task_subtype_count;
-    int res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : sum), sum, size,
-                         MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
+    int res =
+        MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : &sum[0][0]), &sum[0][0],
+                   size, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
     if (res != MPI_SUCCESS) mpi_error(res, "Failed to reduce task sums");
 
-    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : tsum), tsum, size,
-                     MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
+    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : &tsum[0][0]),
+                     &tsum[0][0], size, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
     if (res != MPI_SUCCESS) mpi_error(res, "Failed to reduce task tsums");
 
-    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : count), count, size,
-                     MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
+    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : &count[0][0]),
+                     &count[0][0], size, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);
     if (res != MPI_SUCCESS) mpi_error(res, "Failed to reduce task counts");
 
-    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : min), min, size,
-                     MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD);
+    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : &min[0][0]), &min[0][0],
+                     size, MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD);
     if (res != MPI_SUCCESS) mpi_error(res, "Failed to reduce task minima");
 
-    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : tmin), tmin, size,
-                     MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD);
+    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : &tmin[0][0]),
+                     &tmin[0][0], size, MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD);
     if (res != MPI_SUCCESS) mpi_error(res, "Failed to reduce task minima");
 
-    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : max), max, size,
-                     MPI_DOUBLE, MPI_MAX, 0, MPI_COMM_WORLD);
+    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : &max[0][0]), &max[0][0],
+                     size, MPI_DOUBLE, MPI_MAX, 0, MPI_COMM_WORLD);
     if (res != MPI_SUCCESS) mpi_error(res, "Failed to reduce task maxima");
 
-    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : tmax), tmax, size,
-                     MPI_DOUBLE, MPI_MAX, 0, MPI_COMM_WORLD);
+    res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : &tmax[0][0]),
+                     &tmax[0][0], size, MPI_DOUBLE, MPI_MAX, 0, MPI_COMM_WORLD);
     if (res != MPI_SUCCESS) mpi_error(res, "Failed to reduce task maxima");
 
     res = MPI_Reduce((engine_rank == 0 ? MPI_IN_PLACE : total), total, 1,
@@ -1715,6 +1680,7 @@ enum task_categories task_get_category(const struct task *t) {
     case task_type_star_formation_sink:
       return task_category_star_formation;
 
+    case task_type_sink_density_ghost:
     case task_type_sink_formation:
       return task_category_sink;
 
@@ -1776,6 +1742,8 @@ enum task_categories task_get_category(const struct task *t) {
 
     case task_type_fof_self:
     case task_type_fof_pair:
+    case task_type_fof_attach_self:
+    case task_type_fof_attach_pair:
       return task_category_fof;
 
     case task_type_rt_in:
@@ -1822,6 +1790,7 @@ enum task_categories task_get_category(const struct task *t) {
         case task_subtype_bh_feedback:
           return task_category_black_holes;
 
+        case task_subtype_sink_density:
         case task_subtype_sink_swallow:
         case task_subtype_sink_do_sink_swallow:
         case task_subtype_sink_do_gas_swallow:
diff --git a/src/task.h b/src/task.h
index 54f968b7d9c5e8f1e95ed8ef35332e44a7bb5ab9..1e31478890accf1b3975ec692d43deec26fe99db 100644
--- a/src/task.h
+++ b/src/task.h
@@ -103,8 +103,11 @@ enum task_types {
   task_type_bh_swallow_ghost3, /* Implicit */
   task_type_fof_self,
   task_type_fof_pair,
+  task_type_fof_attach_self,
+  task_type_fof_attach_pair,
   task_type_neutrino_weight,
-  task_type_sink_in,     /* Implicit */
+  task_type_sink_in, /* Implicit */
+  task_type_sink_density_ghost,
   task_type_sink_ghost1, /* Implicit */
   task_type_sink_ghost2, /* Implicit */
   task_type_sink_out,    /* Implicit */
@@ -146,6 +149,7 @@ enum task_subtypes {
   task_subtype_stars_prep2,
   task_subtype_stars_feedback,
   task_subtype_sf_counts,
+  task_subtype_grav_counts,
   task_subtype_bpart_rho,
   task_subtype_bpart_feedback,
   task_subtype_bh_density,
@@ -153,6 +157,7 @@ enum task_subtypes {
   task_subtype_do_gas_swallow,
   task_subtype_do_bh_swallow,
   task_subtype_bh_feedback,
+  task_subtype_sink_density,
   task_subtype_sink_do_sink_swallow,
   task_subtype_sink_swallow,
   task_subtype_sink_do_gas_swallow,
diff --git a/src/timeline.h b/src/timeline.h
index 63164d43203501b9e74824aeabd8df2868466c7f..9d06a0bf652b981f93350eff8101535ffca77ea7 100644
--- a/src/timeline.h
+++ b/src/timeline.h
@@ -24,7 +24,7 @@
 
 /* Local headers. */
 #include "inline.h"
-#include "intrinsics.h"
+#include "swift_intrinsics.h"
 
 #include <math.h>
 #include <stdint.h>
diff --git a/src/timers.c b/src/timers.c
index 2f1e04bf83e5363c7786b379f7d446c9c745942e..f23ffbc1b9f3dc1fc3dc98c9f0a2f5522331b5c5 100644
--- a/src/timers.c
+++ b/src/timers.c
@@ -61,6 +61,7 @@ const char* timers_names[timer_count] = {
     "doself_bh_swallow",
     "doself_bh_feedback",
     "doself_grav_pp",
+    "doself_sink_density",
     "doself_sink_swallow",
     "dopair_density",
     "dopair_gradient",
@@ -73,6 +74,7 @@ const char* timers_names[timer_count] = {
     "dopair_bh_feedback",
     "dopair_grav_mm",
     "dopair_grav_pp",
+    "dopair_sink_density",
     "dopair_sink_swallow",
     "dograv_external",
     "dograv_down",
@@ -89,6 +91,7 @@ const char* timers_names[timer_count] = {
     "dosub_self_bh_swallow",
     "dosub_self_bh_feedback",
     "dosub_self_grav",
+    "dosub_self_sink_density",
     "dosub_self_sink_swallow",
     "dosub_pair_density",
     "dosub_pair_gradient",
@@ -100,6 +103,7 @@ const char* timers_names[timer_count] = {
     "dosub_pair_bh_swallow",
     "dosub_pair_bh_feedback",
     "dosub_pair_grav",
+    "dosub_pair_sink_density",
     "dosub_pair_sink_swallow",
     "doself_subset",
     "dopair_subset",
@@ -109,6 +113,7 @@ const char* timers_names[timer_count] = {
     "do_extra_ghost",
     "do_stars_ghost",
     "do_black_holes_ghost",
+    "do_sinks_ghost",
     "dorecv_part",
     "dorecv_gpart",
     "dorecv_spart",
@@ -142,6 +147,8 @@ const char* timers_names[timer_count] = {
     "rt_tchem",
     "rt_advance_cell_time",
     "rt_collect_times",
+    "do_sync",
+    "neutrino_weighting",
 };
 
 /* File to store the timers */
diff --git a/src/timers.h b/src/timers.h
index ad20f6a414e0b36f8a2118ee28cc8768f6e4fc0a..fef6a6ebf8b81bb776d07e89b8ffedd78caefdcf 100644
--- a/src/timers.h
+++ b/src/timers.h
@@ -61,6 +61,7 @@ enum {
   timer_doself_bh_swallow,
   timer_doself_bh_feedback,
   timer_doself_grav_pp,
+  timer_doself_sink_density,
   timer_doself_sink_swallow,
   timer_dopair_density,
   timer_dopair_gradient,
@@ -73,6 +74,7 @@ enum {
   timer_dopair_bh_feedback,
   timer_dopair_grav_mm,
   timer_dopair_grav_pp,
+  timer_dopair_sink_density,
   timer_dopair_sink_swallow,
   timer_dograv_external,
   timer_dograv_down,
@@ -89,6 +91,7 @@ enum {
   timer_dosub_self_bh_swallow,
   timer_dosub_self_bh_feedback,
   timer_dosub_self_grav,
+  timer_dosub_self_sink_density,
   timer_dosub_self_sink_swallow,
   timer_dosub_pair_density,
   timer_dosub_pair_gradient,
@@ -100,6 +103,7 @@ enum {
   timer_dosub_pair_bh_swallow,
   timer_dosub_pair_bh_feedback,
   timer_dosub_pair_grav,
+  timer_dosub_pair_sink_density,
   timer_dosub_pair_sink_swallow,
   timer_doself_subset,
   timer_dopair_subset,
@@ -109,6 +113,7 @@ enum {
   timer_do_extra_ghost,
   timer_do_stars_ghost,
   timer_do_black_holes_ghost,
+  timer_do_sinks_ghost,
   timer_dorecv_part,
   timer_dorecv_gpart,
   timer_dorecv_spart,
@@ -142,6 +147,8 @@ enum {
   timer_do_rt_tchem,
   timer_do_rt_advance_cell_time,
   timer_do_rt_collect_times,
+  timer_do_sync,
+  timer_neutrino_weighting,
   timer_count,
 };
 
diff --git a/src/timestep.h b/src/timestep.h
index 851befc3b5f6c072bbfbb4b326abf09e08e060db..a6002418088e1c194f31bfd2269456f9752f8497 100644
--- a/src/timestep.h
+++ b/src/timestep.h
@@ -25,6 +25,7 @@
 /* Local headers. */
 #include "cooling.h"
 #include "debug.h"
+#include "forcing.h"
 #include "potential.h"
 #include "rt.h"
 #include "timeline.h"
@@ -173,14 +174,18 @@ __attribute__((always_inline)) INLINE static integertime_t get_part_timestep(
     new_dt_grav = min(new_dt_self_grav, new_dt_ext_grav);
   }
 
+  /* Compute the next timestep (forcing terms condition) */
+  const float new_dt_forcing = forcing_terms_timestep(
+      e->time, e->forcing_terms, e->physical_constants, p, xp);
+
   /* Compute the next timestep (chemistry condition, e.g. diffusion) */
   const float new_dt_chemistry =
       chemistry_timestep(e->physical_constants, e->cosmology, e->internal_units,
                          e->hydro_properties, e->chemistry, p);
 
   /* Take the minimum of all */
-  float new_dt = min5(new_dt_hydro, new_dt_cooling, new_dt_grav, new_dt_mhd,
-                      new_dt_chemistry);
+  float new_dt = min3(new_dt_hydro, new_dt_cooling, new_dt_grav);
+  new_dt = min4(new_dt, new_dt_mhd, new_dt_chemistry, new_dt_forcing);
 
   /* Limit change in smoothing length */
   const float dt_h_change =
@@ -213,7 +218,21 @@ __attribute__((always_inline)) INLINE static integertime_t get_part_timestep(
       /* enforce dt_hydro <= nsubcycles * dt_rt. The rare case where
        * new_dti_rt > new_dti will be handled in the parent function
        * that calls this one. */
-      const integertime_t max_subcycles = max(e->max_nr_rt_subcycles, 1);
+      integertime_t max_subcycles = max(e->max_nr_rt_subcycles, 1);
+      if (max_nr_timesteps / max_subcycles < new_dti_rt) {
+        /* multiplication new_dti_rt * max_subcycles would overflow. This can
+         * happen in rare cases, especially if the total physical time the
+         * simulation should cover is small. So limit max_subcycles to a
+         * reasonable value.
+         * First find an integer guess for the maximal permissible number
+         * of sub-cycles. Then find highest power-of-two below that guess.
+         * Divide the guess by a factor of 2 to simplify the subsequent while
+         * loop. The max() is there to prevent bad things happening. */
+        const integertime_t max_subcycles_guess =
+            max(1LL, max_nr_timesteps / (new_dti_rt * 2LL));
+        max_subcycles = 1LL;
+        while (max_subcycles_guess > max_subcycles) max_subcycles *= 2LL;
+      }
       new_dti = min(new_dti, new_dti_rt * max_subcycles);
     }
   }
@@ -240,12 +259,11 @@ __attribute__((always_inline)) INLINE static integertime_t get_part_rt_timestep(
                           e->physical_constants, e->internal_units);
 
   if ((e->policy & engine_policy_cosmology))
-    error("Cosmology factor in get_part_rt_timestep not implemented yet");
-  /* Apply the maximal displacement constraint (FLT_MAX if non-cosmological)*/
-  /* new_dt = min(new_dt, e->dt_max_RMS_displacement); */
+    /* Apply the maximal displacement constraint (FLT_MAX if non-cosmological)*/
+    new_dt = min(new_dt, e->dt_max_RMS_displacement);
 
   /* Apply cosmology correction (This is 1 if non-cosmological) */
-  /* new_dt *= e->cosmology->time_step_factor; */
+  new_dt *= e->cosmology->time_step_factor;
 
   /* Limit timestep within the allowed range */
   new_dt = min(new_dt, e->dt_max);
@@ -379,7 +397,9 @@ __attribute__((always_inline)) INLINE static integertime_t get_sink_timestep(
     const struct sink *restrict sink, const struct engine *restrict e) {
 
   /* Sink time-step */
-  float new_dt_sink = sink_compute_timestep(sink);
+  float new_dt_sink = sink_compute_timestep(
+      sink, e->sink_properties, (e->policy & engine_policy_cosmology),
+      e->cosmology, e->gravity_properties, e->time, e->time_base);
 
   /* Gravity time-step */
   float new_dt_self = FLT_MAX, new_dt_ext = FLT_MAX;
diff --git a/src/tools.c b/src/tools.c
index 022c3fb3b64aa432332e3e137e2ae7a220b91d3b..44c8fbdc39dc51c5dc6361b0a9a5936d46f63054 100644
--- a/src/tools.c
+++ b/src/tools.c
@@ -40,6 +40,7 @@
 
 /* Local includes. */
 #include "active.h"
+#include "adaptive_softening.h"
 #include "cell.h"
 #include "chemistry.h"
 #include "cosmology.h"
@@ -52,7 +53,9 @@
 #include "periodic.h"
 #include "pressure_floor_iact.h"
 #include "runner.h"
-#include "sink.h"
+#include "sink_iact.h"
+#include "sink_properties.h"
+#include "space_getsid.h"
 #include "star_formation_iact.h"
 #include "stars.h"
 
@@ -148,26 +151,32 @@ void pairs_n2(double *dim, struct part *restrict parts, int N, int periodic) {
 
 void pairs_single_density(double *dim, long long int pid,
                           struct part *restrict parts, int N, int periodic) {
-  int i, k;
-  double r2, dx[3];
-  float fdx[3];
-  struct part p;
-  float a = 1.f, H = 0.f;
+  const float a = 1.f;
+  const float H = 0.f;
 
   /* Find "our" part. */
-  for (k = 0; k < N && parts[k].id != pid; k++)
-    ;
+  int k;
+  for (k = 0; k < N && parts[k].id != pid; k++) {
+    /* Nothing to do here */
+  }
+
+  /* Clear accumulators. */
   if (k == N) error("Part not found.");
-  p = parts[k];
+  struct part p = parts[k];
   printf("pairs_single: part[%i].id == %lli.\n", k, pid);
 
   hydro_init_part(&p, NULL);
+  adaptive_softening_init_part(&p);
   mhd_init_part(&p);
 
   /* Loop over all particle pairs. */
   for (k = 0; k < N; k++) {
     if (parts[k].id == p.id) continue;
-    for (i = 0; i < 3; i++) {
+
+    double dx[3];
+    float fdx[3];
+
+    for (int i = 0; i < 3; i++) {
       dx[i] = p.x[i] - parts[k].x[i];
       if (periodic) {
         if (dx[i] < -dim[i] / 2)
@@ -177,7 +186,7 @@ void pairs_single_density(double *dim, long long int pid,
       }
       fdx[i] = dx[i];
     }
-    r2 = fdx[0] * fdx[0] + fdx[1] * fdx[1] + fdx[2] * fdx[2];
+    const float r2 = fdx[0] * fdx[0] + fdx[1] * fdx[1] + fdx[2] * fdx[2];
     if (r2 < p.h * p.h) {
       runner_iact_nonsym_density(r2, fdx, p.h, parts[k].h, &p, &parts[k], a, H);
       /* printf( "pairs_simple: interacting particles %lli [%i,%i,%i] and %lli
@@ -197,13 +206,18 @@ void pairs_single_density(double *dim, long long int pid,
 
 void pairs_all_density(struct runner *r, struct cell *ci, struct cell *cj) {
 
-  float r2, hi, hj, hig2, hjg2, dx[3];
+  float hi, hj, hig2, hjg2;
   struct part *pi, *pj;
   const double dim[3] = {r->e->s->dim[0], r->e->s->dim[1], r->e->s->dim[2]};
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
   const float a = cosmo->a;
   const float H = cosmo->H;
+  double shift[3] = {0.0, 0.0, 0.0};
+  space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
+  const double shift_i[3] = {cj->loc[0] + shift[0], cj->loc[1] + shift[1],
+                             cj->loc[2] + shift[2]};
+  const double shift_j[3] = {cj->loc[0], cj->loc[1], cj->loc[2]};
 
   /* Implements a double-for loop and checks every interaction */
   for (int i = 0; i < ci->hydro.count; ++i) {
@@ -215,17 +229,23 @@ void pairs_all_density(struct runner *r, struct cell *ci, struct cell *cj) {
     /* Skip inactive particles. */
     if (!part_is_active(pi, e)) continue;
 
+    const float pix = pi->x[0] - shift_i[0];
+    const float piy = pi->x[1] - shift_i[1];
+    const float piz = pi->x[2] - shift_i[2];
+
     for (int j = 0; j < cj->hydro.count; ++j) {
 
       pj = &cj->hydro.parts[j];
 
+      const float pjx = pj->x[0] - shift_j[0];
+      const float pjy = pj->x[1] - shift_j[1];
+      const float pjz = pj->x[2] - shift_j[2];
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dx[k] = ci->hydro.parts[i].x[k] - cj->hydro.parts[j].x[k];
-        dx[k] = nearest(dx[k], dim[k]);
-        r2 += dx[k] * dx[k];
-      }
+      const float dx[3] = {nearest(pix - pjx, dim[0]),
+                           nearest(piy - pjy, dim[1]),
+                           nearest(piz - pjz, dim[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
       if (r2 < hig2 && !part_is_inhibited(pj, e)) {
@@ -235,8 +255,7 @@ void pairs_all_density(struct runner *r, struct cell *ci, struct cell *cj) {
         runner_iact_nonsym_chemistry(r2, dx, hi, pj->h, pi, pj, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hi, pj->h, pi, pj, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hi, pj->h, pi, pj, a, H);
-        runner_iact_nonsym_sink(r2, dx, hi, pj->h, pi, pj, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hi, pj->h, pi, pj, a, H);
       }
     }
   }
@@ -251,17 +270,23 @@ void pairs_all_density(struct runner *r, struct cell *ci, struct cell *cj) {
     /* Skip inactive particles. */
     if (!part_is_active(pj, e)) continue;
 
+    const float pjx = pj->x[0] - shift_j[0];
+    const float pjy = pj->x[1] - shift_j[1];
+    const float pjz = pj->x[2] - shift_j[2];
+
     for (int i = 0; i < ci->hydro.count; ++i) {
 
       pi = &ci->hydro.parts[i];
 
+      const float pix = pi->x[0] - shift_i[0];
+      const float piy = pi->x[1] - shift_i[1];
+      const float piz = pi->x[2] - shift_i[2];
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dx[k] = cj->hydro.parts[j].x[k] - ci->hydro.parts[i].x[k];
-        dx[k] = nearest(dx[k], dim[k]);
-        r2 += dx[k] * dx[k];
-      }
+      const float dx[3] = {nearest(pjx - pix, dim[0]),
+                           nearest(pjy - piy, dim[1]),
+                           nearest(pjz - piz, dim[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
       if (r2 < hjg2 && !part_is_inhibited(pi, e)) {
@@ -271,8 +296,7 @@ void pairs_all_density(struct runner *r, struct cell *ci, struct cell *cj) {
         runner_iact_nonsym_chemistry(r2, dx, hj, pi->h, pj, pi, a, H);
         runner_iact_nonsym_pressure_floor(r2, dx, hj, pi->h, pj, pi, a, H);
         runner_iact_nonsym_star_formation(r2, dx, hj, pi->h, pj, pi, a, H);
-        runner_iact_nonsym_sink(r2, dx, hj, pi->h, pj, pi, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_sink(r2, dx, hj, pi->h, pj, pi, a, H);
       }
     }
   }
@@ -281,13 +305,18 @@ void pairs_all_density(struct runner *r, struct cell *ci, struct cell *cj) {
 #ifdef EXTRA_HYDRO_LOOP
 void pairs_all_gradient(struct runner *r, struct cell *ci, struct cell *cj) {
 
-  float r2, hi, hj, hig2, hjg2, dx[3];
+  float hi, hj, hig2, hjg2;
   struct part *pi, *pj;
   const double dim[3] = {r->e->s->dim[0], r->e->s->dim[1], r->e->s->dim[2]};
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
   const float a = cosmo->a;
   const float H = cosmo->H;
+  double shift[3] = {0.0, 0.0, 0.0};
+  space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
+  const double shift_i[3] = {cj->loc[0] + shift[0], cj->loc[1] + shift[1],
+                             cj->loc[2] + shift[2]};
+  const double shift_j[3] = {cj->loc[0], cj->loc[1], cj->loc[2]};
 
   /* Implements a double-for loop and checks every interaction */
   for (int i = 0; i < ci->hydro.count; ++i) {
@@ -299,25 +328,39 @@ void pairs_all_gradient(struct runner *r, struct cell *ci, struct cell *cj) {
     /* Skip inactive particles. */
     if (!part_is_active(pi, e)) continue;
 
+    const float pix = pi->x[0] - shift_i[0];
+    const float piy = pi->x[1] - shift_i[1];
+    const float piz = pi->x[2] - shift_i[2];
+
     for (int j = 0; j < cj->hydro.count; ++j) {
 
       pj = &cj->hydro.parts[j];
       hj = pj->h;
       hjg2 = hj * hj * kernel_gamma2;
 
+      const float pjx = pj->x[0] - shift_j[0];
+      const float pjy = pj->x[1] - shift_j[1];
+      const float pjz = pj->x[2] - shift_j[2];
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dx[k] = ci->hydro.parts[i].x[k] - cj->hydro.parts[j].x[k];
-        dx[k] = nearest(dx[k], dim[k]);
-        r2 += dx[k] * dx[k];
-      }
+      const float dx[3] = {nearest(pix - pjx, dim[0]),
+                           nearest(piy - pjy, dim[1]),
+                           nearest(piz - pjz, dim[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
-      if (r2 < hig2 && !part_is_inhibited(pj, e)) {
-
-        /* Interact */
-        runner_iact_nonsym_gradient(r2, dx, hi, hj, pi, pj, a, H);
+      if (!part_is_inhibited(pj, e)) {
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+        if (r2 < hig2 || r2 < hjg2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hi, hj, pi, pj, a, H);
+        }
+#else
+        if (r2 < hig2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hi, hj, pi, pj, a, H);
+        }
+#endif /* EXTRA_HYDRO_LOOP_TYPE2 */
       }
     }
   }
@@ -332,25 +375,39 @@ void pairs_all_gradient(struct runner *r, struct cell *ci, struct cell *cj) {
     /* Skip inactive particles. */
     if (!part_is_active(pj, e)) continue;
 
+    const float pjx = pj->x[0] - shift_j[0];
+    const float pjy = pj->x[1] - shift_j[1];
+    const float pjz = pj->x[2] - shift_j[2];
+
     for (int i = 0; i < ci->hydro.count; ++i) {
 
       pi = &ci->hydro.parts[i];
       hi = pi->h;
       hig2 = hi * hi * kernel_gamma2;
 
+      const float pix = pi->x[0] - shift_i[0];
+      const float piy = pi->x[1] - shift_i[1];
+      const float piz = pi->x[2] - shift_i[2];
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dx[k] = cj->hydro.parts[j].x[k] - ci->hydro.parts[i].x[k];
-        dx[k] = nearest(dx[k], dim[k]);
-        r2 += dx[k] * dx[k];
-      }
+      const float dx[3] = {nearest(pjx - pix, dim[0]),
+                           nearest(pjy - piy, dim[1]),
+                           nearest(pjz - piz, dim[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
-      if (r2 < hjg2 && !part_is_inhibited(pi, e)) {
-
-        /* Interact */
-        runner_iact_nonsym_gradient(r2, dx, hj, pi->h, pj, pi, a, H);
+      if (!part_is_inhibited(pi, e)) {
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+        if (r2 < hig2 || r2 < hjg2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hj, pi->h, pj, pi, a, H);
+        }
+#else
+        if (r2 < hjg2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hj, pi->h, pj, pi, a, H);
+        }
+#endif /* EXTRA_HYDRO_LOOP_TYPE2 */
       }
     }
   }
@@ -359,13 +416,18 @@ void pairs_all_gradient(struct runner *r, struct cell *ci, struct cell *cj) {
 
 void pairs_all_force(struct runner *r, struct cell *ci, struct cell *cj) {
 
-  float r2, hi, hj, hig2, hjg2, dx[3];
+  float hi, hj, hig2, hjg2;
   struct part *pi, *pj;
   const double dim[3] = {r->e->s->dim[0], r->e->s->dim[1], r->e->s->dim[2]};
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
   const float a = cosmo->a;
   const float H = cosmo->H;
+  double shift[3] = {0.0, 0.0, 0.0};
+  space_getsid_and_swap_cells(e->s, &ci, &cj, shift);
+  const double shift_i[3] = {cj->loc[0] + shift[0], cj->loc[1] + shift[1],
+                             cj->loc[2] + shift[2]};
+  const double shift_j[3] = {cj->loc[0], cj->loc[1], cj->loc[2]};
 
   /* Implements a double-for loop and checks every interaction */
   for (int i = 0; i < ci->hydro.count; ++i) {
@@ -377,19 +439,25 @@ void pairs_all_force(struct runner *r, struct cell *ci, struct cell *cj) {
     /* Skip inactive particles. */
     if (!part_is_active(pi, e)) continue;
 
+    const float pix = pi->x[0] - shift_i[0];
+    const float piy = pi->x[1] - shift_i[1];
+    const float piz = pi->x[2] - shift_i[2];
+
     for (int j = 0; j < cj->hydro.count; ++j) {
 
       pj = &cj->hydro.parts[j];
       hj = pj->h;
       hjg2 = hj * hj * kernel_gamma2;
 
+      const float pjx = pj->x[0] - shift_j[0];
+      const float pjy = pj->x[1] - shift_j[1];
+      const float pjz = pj->x[2] - shift_j[2];
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dx[k] = ci->hydro.parts[i].x[k] - cj->hydro.parts[j].x[k];
-        dx[k] = nearest(dx[k], dim[k]);
-        r2 += dx[k] * dx[k];
-      }
+      const float dx[3] = {nearest(pix - pjx, dim[0]),
+                           nearest(piy - pjy, dim[1]),
+                           nearest(piz - pjz, dim[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
       if (r2 < hig2 || r2 < hjg2) {
@@ -410,19 +478,25 @@ void pairs_all_force(struct runner *r, struct cell *ci, struct cell *cj) {
     /* Skip inactive particles. */
     if (!part_is_active(pj, e)) continue;
 
+    const float pjx = pj->x[0] - shift_j[0];
+    const float pjy = pj->x[1] - shift_j[1];
+    const float pjz = pj->x[2] - shift_j[2];
+
     for (int i = 0; i < ci->hydro.count; ++i) {
 
       pi = &ci->hydro.parts[i];
       hi = pi->h;
       hig2 = hi * hi * kernel_gamma2;
 
+      const float pix = pi->x[0] - shift_i[0];
+      const float piy = pi->x[1] - shift_i[1];
+      const float piz = pi->x[2] - shift_i[2];
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dx[k] = cj->hydro.parts[j].x[k] - ci->hydro.parts[i].x[k];
-        dx[k] = nearest(dx[k], dim[k]);
-        r2 += dx[k] * dx[k];
-      }
+      const float dx[3] = {nearest(pjx - pix, dim[0]),
+                           nearest(pjy - piy, dim[1]),
+                           nearest(pjz - piz, dim[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
       if (r2 < hjg2 || r2 < hig2) {
@@ -514,7 +588,7 @@ void pairs_all_stars_density(struct runner *r, struct cell *ci,
 }
 
 void self_all_density(struct runner *r, struct cell *ci) {
-  float r2, hi, hj, hig2, hjg2, dxi[3];  //, dxj[3];
+  float hi, hj, hig2, hjg2;
   struct part *pi, *pj;
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -528,6 +602,8 @@ void self_all_density(struct runner *r, struct cell *ci) {
     hi = pi->h;
     hig2 = hi * hi * kernel_gamma2;
 
+    const double pix[3] = {pi->x[0], pi->x[1], pi->x[2]};
+
     for (int j = i + 1; j < ci->hydro.count; ++j) {
 
       pj = &ci->hydro.parts[j];
@@ -536,39 +612,37 @@ void self_all_density(struct runner *r, struct cell *ci) {
 
       if (pi == pj) continue;
 
+      const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dxi[k] = ci->hydro.parts[i].x[k] - ci->hydro.parts[j].x[k];
-        r2 += dxi[k] * dxi[k];
-      }
+      float dx[3] = {(float)(pix[0] - pjx[0]), (float)(pix[1] - pjx[1]),
+                     (float)(pix[2] - pjx[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
       if (r2 < hig2 && part_is_active(pi, e) && !part_is_inhibited(pj, e)) {
 
         /* Interact */
-        runner_iact_nonsym_density(r2, dxi, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_chemistry(r2, dxi, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_pressure_floor(r2, dxi, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_star_formation(r2, dxi, hi, hj, pi, pj, a, H);
-        runner_iact_nonsym_sink(r2, dxi, hi, hj, pi, pj, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_density(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_chemistry(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_pressure_floor(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_star_formation(r2, dx, hi, hj, pi, pj, a, H);
+        runner_iact_nonsym_sink(r2, dx, hi, hj, pi, pj, a, H);
       }
 
       /* Hit or miss? */
       if (r2 < hjg2 && part_is_active(pj, e) && !part_is_inhibited(pi, e)) {
 
-        dxi[0] = -dxi[0];
-        dxi[1] = -dxi[1];
-        dxi[2] = -dxi[2];
+        dx[0] = -dx[0];
+        dx[1] = -dx[1];
+        dx[2] = -dx[2];
 
         /* Interact */
-        runner_iact_nonsym_density(r2, dxi, hj, hi, pj, pi, a, H);
-        runner_iact_nonsym_chemistry(r2, dxi, hj, hi, pj, pi, a, H);
-        runner_iact_nonsym_pressure_floor(r2, dxi, hj, hi, pj, pi, a, H);
-        runner_iact_nonsym_star_formation(r2, dxi, hj, hi, pj, pi, a, H);
-        runner_iact_nonsym_sink(r2, dxi, hj, hi, pj, pi, a, H,
-                                e->sink_properties);
+        runner_iact_nonsym_density(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_chemistry(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_pressure_floor(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_star_formation(r2, dx, hj, hi, pj, pi, a, H);
+        runner_iact_nonsym_sink(r2, dx, hj, hi, pj, pi, a, H);
       }
     }
   }
@@ -576,7 +650,7 @@ void self_all_density(struct runner *r, struct cell *ci) {
 
 #ifdef EXTRA_HYDRO_LOOP
 void self_all_gradient(struct runner *r, struct cell *ci) {
-  float r2, hi, hj, hig2, hjg2, dxi[3];  //, dxj[3];
+  float hi, hj, hig2, hjg2;
   struct part *pi, *pj;
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -590,6 +664,8 @@ void self_all_gradient(struct runner *r, struct cell *ci) {
     hi = pi->h;
     hig2 = hi * hi * kernel_gamma2;
 
+    const double pix[3] = {pi->x[0], pi->x[1], pi->x[2]};
+
     for (int j = i + 1; j < ci->hydro.count; ++j) {
 
       pj = &ci->hydro.parts[j];
@@ -598,29 +674,46 @@ void self_all_gradient(struct runner *r, struct cell *ci) {
 
       if (pi == pj) continue;
 
+      const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dxi[k] = ci->hydro.parts[i].x[k] - ci->hydro.parts[j].x[k];
-        r2 += dxi[k] * dxi[k];
-      }
+      float dx[3] = {(float)(pix[0] - pjx[0]), (float)(pix[1] - pjx[1]),
+                     (float)(pix[2] - pjx[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
-      if (r2 < hig2 && part_is_active(pi, e) && !part_is_inhibited(pj, e)) {
-
-        /* Interact */
-        runner_iact_nonsym_gradient(r2, dxi, hi, hj, pi, pj, a, H);
+      if (part_is_active(pi, e) && !part_is_inhibited(pj, e)) {
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+        if (r2 < hig2 || r2 < hjg2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hi, hj, pi, pj, a, H);
+        }
+#else
+        if (r2 < hig2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hi, hj, pi, pj, a, H);
+        }
+#endif /* EXTRA_HYDRO_LOOP_TYPE2 */
       }
 
       /* Hit or miss? */
-      if (r2 < hjg2 && part_is_active(pj, e) && !part_is_inhibited(pi, e)) {
+      if (part_is_active(pj, e) && !part_is_inhibited(pi, e)) {
 
-        dxi[0] = -dxi[0];
-        dxi[1] = -dxi[1];
-        dxi[2] = -dxi[2];
+        dx[0] = -dx[0];
+        dx[1] = -dx[1];
+        dx[2] = -dx[2];
 
-        /* Interact */
-        runner_iact_nonsym_gradient(r2, dxi, hj, hi, pj, pi, a, H);
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+        if (r2 < hig2 || r2 < hjg2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hj, hi, pj, pi, a, H);
+        }
+#else
+        if (r2 < hjg2) {
+          /* Interact */
+          runner_iact_nonsym_gradient(r2, dx, hj, hi, pj, pi, a, H);
+        }
+#endif /* EXTRA_HYDRO_LOOP_TYPE2 */
       }
     }
   }
@@ -628,7 +721,7 @@ void self_all_gradient(struct runner *r, struct cell *ci) {
 #endif /* EXTRA_HYDRO_LOOP */
 
 void self_all_force(struct runner *r, struct cell *ci) {
-  float r2, hi, hj, hig2, hjg2, dxi[3];  //, dxj[3];
+  float hi, hj, hig2, hjg2;
   struct part *pi, *pj;
   const struct engine *e = r->e;
   const struct cosmology *cosmo = e->cosmology;
@@ -642,6 +735,8 @@ void self_all_force(struct runner *r, struct cell *ci) {
     hi = pi->h;
     hig2 = hi * hi * kernel_gamma2;
 
+    const double pix[3] = {pi->x[0], pi->x[1], pi->x[2]};
+
     for (int j = i + 1; j < ci->hydro.count; ++j) {
 
       pj = &ci->hydro.parts[j];
@@ -650,18 +745,18 @@ void self_all_force(struct runner *r, struct cell *ci) {
 
       if (pi == pj) continue;
 
+      const double pjx[3] = {pj->x[0], pj->x[1], pj->x[2]};
+
       /* Pairwise distance */
-      r2 = 0.0f;
-      for (int k = 0; k < 3; k++) {
-        dxi[k] = ci->hydro.parts[i].x[k] - ci->hydro.parts[j].x[k];
-        r2 += dxi[k] * dxi[k];
-      }
+      float dx[3] = {(float)(pix[0] - pjx[0]), (float)(pix[1] - pjx[1]),
+                     (float)(pix[2] - pjx[2])};
+      const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
       /* Hit or miss? */
       if (r2 < hig2 || r2 < hjg2) {
 
         /* Interact */
-        runner_iact_force(r2, dxi, hi, hj, pi, pj, a, H);
+        runner_iact_force(r2, dx, hi, hj, pi, pj, a, H);
       }
     }
   }
@@ -714,28 +809,34 @@ void self_all_stars_density(struct runner *r, struct cell *ci) {
 /**
  * @brief Compute the force on a single particle brute-force.
  */
-void engine_single_density(double *dim, long long int pid,
-                           struct part *restrict parts, int N, int periodic,
-                           const struct cosmology *cosmo) {
-  double r2, dx[3];
-  float fdx[3];
-  struct part p;
-  float a = 1.f, H = 0.f;
+void engine_single_density(const double dim[3], const long long int pid,
+                           struct part *restrict parts, const int N,
+                           const int periodic, const struct cosmology *cosmo,
+                           const struct gravity_props *grav_props) {
+  const float a = 1.f;
+  const float H = 0.f;
 
   /* Find "our" part. */
   int k;
-  for (k = 0; k < N && parts[k].id != pid; k++)
-    ;
+  for (k = 0; k < N && parts[k].id != pid; k++) {
+    /* Nothing to do here */
+  }
+
   if (k == N) error("Part not found.");
-  p = parts[k];
+  struct part p = parts[k];
 
   /* Clear accumulators. */
   hydro_init_part(&p, NULL);
+  adaptive_softening_init_part(&p);
   mhd_init_part(&p);
 
-  /* Loop over all particle pairs (force). */
+  /* Loop over all particle pairs (density). */
   for (k = 0; k < N; k++) {
     if (parts[k].id == p.id) continue;
+
+    double dx[3];
+    float fdx[3];
+
     for (int i = 0; i < 3; i++) {
       dx[i] = p.x[i] - parts[k].x[i];
       if (periodic) {
@@ -746,7 +847,8 @@ void engine_single_density(double *dim, long long int pid,
       }
       fdx[i] = dx[i];
     }
-    r2 = fdx[0] * fdx[0] + fdx[1] * fdx[1] + fdx[2] * fdx[2];
+
+    const float r2 = fdx[0] * fdx[0] + fdx[1] * fdx[1] + fdx[2] * fdx[2];
     if (r2 < p.h * p.h * kernel_gamma2) {
       runner_iact_nonsym_density(r2, fdx, p.h, parts[k].h, &p, &parts[k], a, H);
     }
@@ -754,6 +856,7 @@ void engine_single_density(double *dim, long long int pid,
 
   /* Dump the result. */
   hydro_end_density(&p, cosmo);
+  adaptive_softening_end_density(&p, grav_props);
   mhd_end_density(&p, cosmo);
   message("part %lli (h=%e) has wcount=%e, rho=%e.", p.id, p.h,
           p.density.wcount, hydro_get_comoving_density(&p));
@@ -769,8 +872,10 @@ void engine_single_force(double *dim, long long int pid,
   float a = 1.f, H = 0.f;
 
   /* Find "our" part. */
-  for (k = 0; k < N && parts[k].id != pid; k++)
-    ;
+  for (k = 0; k < N && parts[k].id != pid; k++) {
+    /* Nothing to do here */
+  }
+
   if (k == N) error("Part not found.");
   p = parts[k];
 
diff --git a/src/tracers/EAGLE/tracers.h b/src/tracers/EAGLE/tracers.h
index 6f135882acdcc1077c645228d154e9085ffcec74..686be09777729a54c5611710c8868bc1e9f609e3 100644
--- a/src/tracers/EAGLE/tracers.h
+++ b/src/tracers/EAGLE/tracers.h
@@ -214,6 +214,14 @@ static INLINE void tracers_first_init_xpart(
 
   xp->tracers_data.last_AGN_injection_scale_factor = -1.f;
   xp->tracers_data.density_at_last_AGN_feedback_event = -1.f;
+
+  xp->tracers_data.hit_by_jet_feedback = 0;
+  xp->tracers_data.jet_feedback_energy = 0.f;
+  xp->tracers_data.last_AGN_jet_feedback_scale_factor = 0.f;
+  xp->tracers_data.last_AGN_jet_feedback_time = 0.f;
+  xp->tracers_data.last_jet_kick_velocity = 0.f;
+  xp->tracers_data.last_jet_kick_accretion_mode = BH_thick_disc;
+  xp->tracers_data.last_jet_kick_BH_id = 0;
 }
 
 /**
@@ -317,7 +325,9 @@ static INLINE void tracers_after_black_holes_feedback(
 
 static INLINE void tracers_after_jet_feedback(
     const struct part *p, struct xpart *xp, const int with_cosmology,
-    const float scale_factor, const double time, const double delta_energy) {
+    const float scale_factor, const double time, const double delta_energy,
+    const float vel_kick, const enum BH_accretion_modes accretion_mode,
+    const long long id) {
 
   if (with_cosmology)
     xp->tracers_data.last_AGN_jet_feedback_scale_factor = scale_factor;
@@ -325,6 +335,9 @@ static INLINE void tracers_after_jet_feedback(
     xp->tracers_data.last_AGN_jet_feedback_time = time;
   xp->tracers_data.hit_by_jet_feedback++;
   xp->tracers_data.jet_feedback_energy += delta_energy;
+  xp->tracers_data.last_jet_kick_velocity = vel_kick;
+  xp->tracers_data.last_jet_kick_accretion_mode = accretion_mode;
+  xp->tracers_data.last_jet_kick_BH_id = id;
 }
 
 /**
diff --git a/src/tracers/EAGLE/tracers_io.h b/src/tracers/EAGLE/tracers_io.h
index 9997449151cba56ffd542c2c2d9f28a5e1586e60..cc792109239608c06b1548d48c56815ab1608cab 100644
--- a/src/tracers/EAGLE/tracers_io.h
+++ b/src/tracers/EAGLE/tracers_io.h
@@ -24,6 +24,7 @@
 #include <config.h>
 
 /* Local includes */
+#include "black_holes_properties.h"
 #include "io_properties.h"
 #include "tracers.h"
 
@@ -103,9 +104,10 @@ __attribute__((always_inline)) INLINE static int tracers_write_particles(
       "Maximal temperatures ever reached by the particles");
 
   if (with_cosmology) {
-    list[1] = io_make_output_field(
+    list[1] = io_make_physical_output_field(
         "MaximalTemperatureScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
         xparts, tracers_data.maximum_temperature_scale_factor,
+        /*can convert to comoving=*/0,
         "Scale-factors at which the maximal temperature was reached");
 
   } else {
@@ -136,39 +138,44 @@ __attribute__((always_inline)) INLINE static int tracers_write_particles(
                                  "Total amount of thermal energy from AGN "
                                  "feedback events received by the particles.");
 
-  list[5] = io_make_output_field(
+  list[5] = io_make_physical_output_field(
       "DensitiesBeforeLastAGNEvent", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, xparts,
       tracers_data.density_before_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical density (not subgrid) of the gas fetched before the last AGN "
       "feedback event that hit the particles. -1 if the particles have never "
       "been heated.");
 
-  list[6] = io_make_output_field(
+  list[6] = io_make_physical_output_field(
       "EntropiesBeforeLastAGNEvent", FLOAT, 1, UNIT_CONV_ENTROPY_PER_UNIT_MASS,
       0.f, xparts, tracers_data.entropy_before_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical entropy (not subgrid) per unit mass of the gas fetched before "
       "the last AGN feedback event that hit the particles. -1 if the particles "
       "have never been heated.");
 
-  list[7] = io_make_output_field(
+  list[7] = io_make_physical_output_field(
       "DensitiesAtLastAGNEvent", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, xparts,
       tracers_data.density_at_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical density (not subgrid) of the gas at the last AGN feedback "
       "event that hit the particles. -1 if the particles have never been "
       "heated.");
 
-  list[8] = io_make_output_field(
+  list[8] = io_make_physical_output_field(
       "EntropiesAtLastAGNEvent", FLOAT, 1, UNIT_CONV_ENTROPY_PER_UNIT_MASS, 0.f,
       xparts, tracers_data.entropy_at_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical entropy (not subgrid) per unit mass of the gas at the last AGN "
       "feedback event that hit the particles. -1 if the particles have never "
       "been heated.");
 
   if (with_cosmology) {
 
-    list[9] = io_make_output_field(
+    list[9] = io_make_physical_output_field(
         "LastAGNFeedbackScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
         xparts, tracers_data.last_AGN_injection_scale_factor,
+        /*can convert to comoving=*/0,
         "Scale-factors at which the particles were last hit by AGN feedback. "
         "-1 if a particle has never been hit by feedback");
 
@@ -187,7 +194,65 @@ __attribute__((always_inline)) INLINE static int tracers_write_particles(
       "Star formation rates of the particles averaged over the period set by "
       "the first two snapshot triggers");
 
-  return 11;
+  if (with_jets) {
+    list[11] = io_make_output_field(
+        "KickedByJetFeedback", CHAR, 1, UNIT_CONV_NO_UNITS, 0.f, xparts,
+        tracers_data.hit_by_jet_feedback,
+        "Flags the particles that have been directly kicked by"
+        "an AGN jet feedback event at some point in the past. "
+        "If > 0, contains the number of individual events.");
+
+    list[12] = io_make_physical_output_field(
+        "EnergiesReceivedFromJetFeedback", FLOAT, 1, UNIT_CONV_ENERGY, 0.f,
+        xparts, tracers_data.jet_feedback_energy, /*can convert to comoving=*/0,
+        "Total amount of kinetic energy from AGN "
+        "jet feedback events received by the "
+        "particles while they were still gas "
+        "particles.");
+
+    if (with_cosmology) {
+
+      list[13] = io_make_physical_output_field(
+          "LastAGNJetFeedbackScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
+          xparts, tracers_data.last_AGN_jet_feedback_scale_factor,
+          /*can convert to comoving=*/0,
+          "Scale-factors at which the particles were last hit by jet "
+          "feedback while they were still gas particles. "
+          "-1 if a particle has never been hit by feedback");
+
+    } else {
+
+      list[13] = io_make_output_field(
+          "LastAGNJetFeedbackTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, xparts,
+          tracers_data.last_AGN_jet_feedback_time,
+          "Times at which the particles were last hit by jet"
+          "feedback while they were still gas particles. "
+          "-1 if a particle has never been hit by feedback");
+    }
+
+    list[14] = io_make_output_field("LastAGNJetKickVelocities", FLOAT, 1,
+                                    UNIT_CONV_VELOCITY, 0.f, xparts,
+                                    tracers_data.last_jet_kick_velocity,
+                                    "Kick velocity at last AGN jet event.");
+
+    list[15] = io_make_output_field("LastAGNJetKickMode", CHAR, 1,
+                                    UNIT_CONV_NO_UNITS, 0.f, xparts,
+                                    tracers_data.last_jet_kick_accretion_mode,
+                                    "The accretion/feedback mode the BH was "
+                                    "in when it kicked this particle. 0 "
+                                    "corresponds to the thick disc, 1 to the "
+                                    "thin disc and 2 to the slim disc.");
+
+    list[16] = io_make_output_field("LastAGNJetKickBHId", ULONGLONG, 1,
+                                    UNIT_CONV_NO_UNITS, 0.f, xparts,
+                                    tracers_data.last_jet_kick_BH_id,
+                                    "The id of the BH that last kicked this "
+                                    "particle.");
+
+    return 17;
+  } else {
+    return 11;
+  }
 }
 
 __attribute__((always_inline)) INLINE static int tracers_write_sparticles(
@@ -201,9 +266,10 @@ __attribute__((always_inline)) INLINE static int tracers_write_sparticles(
       "converted to stars");
 
   if (with_cosmology) {
-    list[1] = io_make_output_field(
+    list[1] = io_make_physical_output_field(
         "MaximalTemperatureScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
         sparts, tracers_data.maximum_temperature_scale_factor,
+        /*can convert to comoving=*/0,
         "Scale-factors at which the maximal temperature was reached");
 
   } else {
@@ -228,39 +294,43 @@ __attribute__((always_inline)) INLINE static int tracers_write_sparticles(
                            "an AGN feedback event at some point in the past "
                            "when the particle was still a gas particle.");
 
-  list[4] = io_make_output_field(
+  list[4] = io_make_physical_output_field(
       "EnergiesReceivedFromAGNFeedback", FLOAT, 1, UNIT_CONV_ENERGY, 0.f,
-      sparts, tracers_data.AGN_feedback_energy,
+      sparts, tracers_data.AGN_feedback_energy, /*can convert to comoving=*/0,
       "Total amount of thermal energy from AGN feedback events received by the "
       "particles when the particle was still a gas particle.");
 
-  list[5] = io_make_output_field(
+  list[5] = io_make_physical_output_field(
       "DensitiesBeforeLastAGNEvent", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, sparts,
       tracers_data.density_before_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical density (not subgrid) of the gas fetched before the last AGN "
       "feedback "
       "event that hit the particles when they were still gas particles. -1 if "
       "the particles have never been heated.");
 
-  list[6] = io_make_output_field(
+  list[6] = io_make_physical_output_field(
       "EntropiesBeforeLastAGNEvent", FLOAT, 1, UNIT_CONV_ENTROPY_PER_UNIT_MASS,
       0.f, sparts, tracers_data.entropy_before_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical entropy (not subgrid) per unit mass of the gas fetched before "
       "the last AGN "
       "feedback event that hit the particles when they were still gas "
       "particles."
       " -1 if the particles have never been heated.");
 
-  list[7] = io_make_output_field(
+  list[7] = io_make_physical_output_field(
       "DensitiesAtLastAGNEvent", FLOAT, 1, UNIT_CONV_DENSITY, 0.f, sparts,
       tracers_data.density_at_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical density (not subgrid) of the gas at the last AGN feedback "
       "event that hit the particles when they were still gas particles. -1 if "
       "the particles have never been heated.");
 
-  list[8] = io_make_output_field(
+  list[8] = io_make_physical_output_field(
       "EntropiesAtLastAGNEvent", FLOAT, 1, UNIT_CONV_ENTROPY_PER_UNIT_MASS, 0.f,
       sparts, tracers_data.entropy_at_last_AGN_feedback_event,
+      /*can convert to comoving=*/0,
       "Physical entropy (not subgrid) per unit mass of the gas at the last AGN "
       "feedback event that hit the particles when they were still gas "
       "particles."
@@ -268,9 +338,10 @@ __attribute__((always_inline)) INLINE static int tracers_write_sparticles(
 
   if (with_cosmology) {
 
-    list[9] = io_make_output_field(
+    list[9] = io_make_physical_output_field(
         "LastAGNFeedbackScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
         sparts, tracers_data.last_AGN_injection_scale_factor,
+        /*can convert to comoving=*/0,
         "Scale-factors at which the particles were last hit by AGN feedback "
         "when they were still gas particles. -1 if a particle has never been "
         "hit by feedback");
@@ -292,7 +363,65 @@ __attribute__((always_inline)) INLINE static int tracers_write_sparticles(
       "the first two snapshot triggers when the particle was still a gas "
       "particle.");
 
-  return 11;
+  if (with_jets) {
+    list[11] = io_make_output_field(
+        "KickedByJetFeedback", CHAR, 1, UNIT_CONV_NO_UNITS, 0.f, sparts,
+        tracers_data.hit_by_jet_feedback,
+        "Flags the particles that have been directly kicked by"
+        "an AGN jet feedback event at some point in the past. "
+        "If > 0, contains the number of individual events.");
+
+    list[12] = io_make_output_field("EnergiesReceivedFromJetFeedback", FLOAT, 1,
+                                    UNIT_CONV_ENERGY, 0.f, sparts,
+                                    tracers_data.jet_feedback_energy,
+                                    "Total amount of kinetic energy from AGN "
+                                    "jet feedback events received by the "
+                                    "particles while they were still gas "
+                                    "particles.");
+
+    if (with_cosmology) {
+
+      list[13] = io_make_physical_output_field(
+          "LastAGNJetFeedbackScaleFactors", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f,
+          sparts, tracers_data.last_AGN_jet_feedback_scale_factor,
+          /*can convert to comoving=*/0,
+          "Scale-factors at which the particles were last hit by jet "
+          "feedback while they were still gas particles. "
+          "-1 if a particle has never been hit by feedback");
+
+    } else {
+
+      list[13] = io_make_output_field(
+          "LastAGNJetFeedbackTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, sparts,
+          tracers_data.last_AGN_jet_feedback_time,
+          "Times at which the particles were last hit by jet"
+          "feedback while they were still gas particles. "
+          "-1 if a particle has never been hit by feedback");
+    }
+
+    list[14] = io_make_output_field("LastAGNJetKickVelocities", FLOAT, 1,
+                                    UNIT_CONV_VELOCITY, 0.f, sparts,
+                                    tracers_data.last_jet_kick_velocity,
+                                    "Kick velocity at last AGN jet event.");
+
+    list[15] = io_make_output_field("LastAGNJetKickMode", CHAR, 1,
+                                    UNIT_CONV_NO_UNITS, 0.f, sparts,
+                                    tracers_data.last_jet_kick_accretion_mode,
+                                    "The accretion/feedback mode the BH was "
+                                    "in when it kicked this particle. 0 "
+                                    "corresponds to the thick disc, 1 to the "
+                                    "thin disc and 2 to the slim disc.");
+
+    list[16] = io_make_output_field("LastAGNJetKickBHId", ULONGLONG, 1,
+                                    UNIT_CONV_NO_UNITS, 0.f, sparts,
+                                    tracers_data.last_jet_kick_BH_id,
+                                    "The id of the BH that last kicked this "
+                                    "particle.");
+
+    return 17;
+  } else {
+    return 11;
+  }
 }
 
 __attribute__((always_inline)) INLINE static int tracers_write_bparticles(
diff --git a/src/tracers/EAGLE/tracers_struct.h b/src/tracers/EAGLE/tracers_struct.h
index d1203ddd9fc54dd8d050c966da119873b64ab649..da7324833abdd19f2bc2d693e63f5c2a6db97d55 100644
--- a/src/tracers/EAGLE/tracers_struct.h
+++ b/src/tracers/EAGLE/tracers_struct.h
@@ -23,6 +23,14 @@
 /* Local includes */
 #include "tracers_triggers.h"
 
+/*! The possible accretion modes every black hole can take. */
+enum BH_accretion_modes {
+  BH_thick_disc = 0,       /* At low Eddington ratios */
+  BH_thin_disc,            /* At moderate Eddington ratios */
+  BH_slim_disc,            /* Super-Eddington accretion */
+  BH_accretion_modes_count /* Number of possible accretion modes */
+};
+
 /**
  * @brief Properties of the tracers stored in the extended particle data.
  */
@@ -94,6 +102,16 @@ struct tracers_xpart_data {
 
   /*! Has this particle been hit by AGN feedback? */
   char hit_by_AGN_feedback;
+
+  /*! Kick velocity at last AGN jet event */
+  float last_jet_kick_velocity;
+
+  /*! The accretion/feedback mode of the BH when this particle was last
+   * kicked */
+  enum BH_accretion_modes last_jet_kick_accretion_mode;
+
+  /*! The ID of the BH that did the last kick */
+  long long last_jet_kick_BH_id;
 };
 
 /**
diff --git a/src/tracers/none/tracers_struct.h b/src/tracers/none/tracers_struct.h
index 16c5d273a8062c3f31f6c35d1dba8f69fa3d1bcd..4143fa952b8b7af13667a559014f956147aa275c 100644
--- a/src/tracers/none/tracers_struct.h
+++ b/src/tracers/none/tracers_struct.h
@@ -26,8 +26,10 @@ struct tracers_xpart_data {};
 
 /**
  * @brief Properties of the tracers stored in the star particle data.
+ *
+ * Note: In this model, they are identical to the xpart data.
  */
-struct tracers_spart_data {};
+#define tracers_spart_data tracers_xpart_data
 
 /**
  * @brief Properties of the tracers stored in the black hole particle data.
diff --git a/src/units.c b/src/units.c
index c1d60814e3bd5f2e130f867d37c2bb98f3bec40a..52f897c86e594920d1b7ac162cae3180b635abc2 100644
--- a/src/units.c
+++ b/src/units.c
@@ -400,6 +400,32 @@ void units_get_base_unit_exponents_array(float baseUnitsExp[5],
       baseUnitsExp[UNIT_CURRENT] = -1.f;
       break;
 
+    case UNIT_CONV_MAGNETIC_FIELD_VECTOR_POTENTIAL:
+      baseUnitsExp[UNIT_MASS] = 1.f;
+      baseUnitsExp[UNIT_LENGTH] = 1.f;
+      baseUnitsExp[UNIT_TIME] = -2.f;
+      baseUnitsExp[UNIT_CURRENT] = -1.f;
+      break;
+
+    case UNIT_CONV_MAGNETIC_FIELD_PER_TIME:
+      baseUnitsExp[UNIT_MASS] = 1.f;
+      baseUnitsExp[UNIT_TIME] = -3.f;
+      baseUnitsExp[UNIT_CURRENT] = -1.f;
+      break;
+
+    case UNIT_CONV_MAGNETIC_FIELD_SQUARED:
+      baseUnitsExp[UNIT_MASS] = 2.f;
+      baseUnitsExp[UNIT_TIME] = -4.f;
+      baseUnitsExp[UNIT_CURRENT] = -2.f;
+      break;
+
+    case UNIT_CONV_MAGNETIC_CURL:
+      baseUnitsExp[UNIT_MASS] = 1.f;
+      baseUnitsExp[UNIT_LENGTH] = -1.f;
+      baseUnitsExp[UNIT_TIME] = -2.f;
+      baseUnitsExp[UNIT_CURRENT] = -1.f;
+      break;
+
     case UNIT_CONV_MAGNETIC_DIVERGENCE:
       baseUnitsExp[UNIT_MASS] = 1.f;
       baseUnitsExp[UNIT_LENGTH] = -1.f;
@@ -427,6 +453,20 @@ void units_get_base_unit_exponents_array(float baseUnitsExp[5],
       baseUnitsExp[UNIT_CURRENT] = -1.f;
       break;
 
+    case UNIT_CONV_ELECTRIC_CHARGE_FIELD_STRENGTH:
+      baseUnitsExp[UNIT_MASS] = 1.f;
+      baseUnitsExp[UNIT_LENGTH] = 1.f;
+      baseUnitsExp[UNIT_TIME] = -3.f;
+      baseUnitsExp[UNIT_CURRENT] = -1.f;
+      break;
+
+    case UNIT_CONV_ELECTRIC_CHARGE_FIELD_STRENGTH_RATE:
+      baseUnitsExp[UNIT_MASS] = 1.f;
+      baseUnitsExp[UNIT_LENGTH] = 1.f;
+      baseUnitsExp[UNIT_TIME] = -4.f;
+      baseUnitsExp[UNIT_CURRENT] = -1.f;
+      break;
+
     case UNIT_CONV_TEMPERATURE:
       baseUnitsExp[UNIT_TEMPERATURE] = 1.f;
       break;
diff --git a/src/units.h b/src/units.h
index 98582e1324d5c71586ec95c2266990b58986ffde..3ca1de439cf7719cd6e4fa364504f7cc2dbef599 100644
--- a/src/units.h
+++ b/src/units.h
@@ -97,10 +97,16 @@ enum unit_conversion_factor {
   UNIT_CONV_ELECTRIC_CONDUCTANCE,
   UNIT_CONV_MAGNETIC_FLUX,
   UNIT_CONV_MAGNETIC_FIELD,
+  UNIT_CONV_MAGNETIC_FIELD_PER_TIME,
+  UNIT_CONV_MAGNETIC_FIELD_SQUARED,
   UNIT_CONV_MAGNETIC_DIVERGENCE,
+  UNIT_CONV_MAGNETIC_CURL,
   UNIT_CONV_MAGNETIC_INDUCTANCE,
   UNIT_CONV_MAGNETIC_HELICITY,
   UNIT_CONV_MAGNETIC_CROSS_HELICITY,
+  UNIT_CONV_MAGNETIC_FIELD_VECTOR_POTENTIAL,
+  UNIT_CONV_ELECTRIC_CHARGE_FIELD_STRENGTH,
+  UNIT_CONV_ELECTRIC_CHARGE_FIELD_STRENGTH_RATE,
   UNIT_CONV_TEMPERATURE,
   UNIT_CONV_AREA,
   UNIT_CONV_VOLUME,
diff --git a/src/utilities.h b/src/utilities.h
index 28faf6b1e59159af4ee4a7b87054ab6a36478c9c..747ef2154ed2d0b09c22208d3c88aa8a15fcbd3c 100644
--- a/src/utilities.h
+++ b/src/utilities.h
@@ -56,4 +56,47 @@ INLINE static int find_value_in_monot_incr_array(const float x,
     return index_low;
 }
 
+/**
+ * @brief Search for a value in a monotonically increasing array to find the
+ *      index such that table[i,j] = array[i*n_col + j] < value <
+ *      table[i + 1, j] = array[(i + 1)*n_col + j]
+ *
+ * @param x The value to find
+ * @param array The array to search
+ * @param n_row The number of rows of the table
+ * @param n_col The number of columns of the table
+ * @param j The column index to perform the search
+ *
+ * Return -1 and n for x below and above the array edge values respectively.
+ */
+INLINE static int vertical_find_value_in_monot_incr_array(const float x,
+                                                          const float *array,
+                                                          const int n_row,
+                                                          const int n_col,
+                                                          const int j) {
+
+  int i_low = 0;
+  int i_high = n_row - 1;
+  int i_mid;
+
+  // Until table[i_low,j] < x < table[i_high=i_low + 1, j]
+  while (i_high - i_low > 1) {
+    i_mid = (i_high + i_low) / 2;  // Middle index
+
+    // Replace the low or high i with the middle
+    if (array[i_mid * n_col + j] <= x)
+      i_low = i_mid;
+    else
+      i_high = i_mid;
+  }
+
+  // Set index with the found i_low or an error value if outside the array
+  if (x < array[j])
+    return -1;
+  else if (array[(n_row - 1) * n_col + j] <= x)
+    return n_row;
+  else
+    return i_low;
+}
+
 #endif /* SWIFT_UTILITIES_H */
diff --git a/src/vector.h b/src/vector.h
index 0c8c2c7c798270342b55fb23b209d3f6bd079e75..cc73f84c8852ea09bba279402c98a3f914c897c8 100644
--- a/src/vector.h
+++ b/src/vector.h
@@ -115,12 +115,23 @@
 #define vec_gather(base, offsets) _mm512_i32gather_ps(offsets.m, base, 1)
 
 /* Initialises a vector struct with a default value. */
-#define FILL_VEC(a)                                                     \
-  {                                                                     \
-    .f[0] = a, .f[1] = a, .f[2] = a, .f[3] = a, .f[4] = a, .f[5] = a,   \
-    .f[6] = a, .f[7] = a, .f[8] = a, .f[9] = a, .f[10] = a, .f[11] = a, \
-    .f[12] = a, .f[13] = a, .f[14] = a, .f[15] = a                      \
-  }
+#define FILL_VEC(a) \
+  {.f[0] = a,       \
+   .f[1] = a,       \
+   .f[2] = a,       \
+   .f[3] = a,       \
+   .f[4] = a,       \
+   .f[5] = a,       \
+   .f[6] = a,       \
+   .f[7] = a,       \
+   .f[8] = a,       \
+   .f[9] = a,       \
+   .f[10] = a,      \
+   .f[11] = a,      \
+   .f[12] = a,      \
+   .f[13] = a,      \
+   .f[14] = a,      \
+   .f[15] = a}
 
 /* Performs a horizontal add on the vector and adds the result to a float. */
 #ifdef __ICC
@@ -204,11 +215,15 @@
 #define vec_dbl_fmax(a, b) _mm256_max_pd(a, b)
 
 /* Initialises a vector struct with a default value. */
-#define FILL_VEC(a)                                                   \
-  {                                                                   \
-    .f[0] = a, .f[1] = a, .f[2] = a, .f[3] = a, .f[4] = a, .f[5] = a, \
-    .f[6] = a, .f[7] = a                                              \
-  }
+#define FILL_VEC(a) \
+  {.f[0] = a,       \
+   .f[1] = a,       \
+   .f[2] = a,       \
+   .f[3] = a,       \
+   .f[4] = a,       \
+   .f[5] = a,       \
+   .f[6] = a,       \
+   .f[7] = a}
 
 /* Performs a horizontal add on the vector and adds the result to a float. */
 #define VEC_HADD(a, b)            \
@@ -376,8 +391,7 @@
 #define vec_dbl_fmax(a, b) _mm_max_pd(a, b)
 
 /* Initialises a vector struct with a default value. */
-#define FILL_VEC(a) \
-  { .f[0] = a, .f[1] = a, .f[2] = a, .f[3] = a }
+#define FILL_VEC(a) {.f[0] = a, .f[1] = a, .f[2] = a, .f[3] = a}
 
 /* Performs a horizontal add on the vector and adds the result to a float. */
 #define VEC_HADD(a, b)         \
diff --git a/src/velociraptor_dummy.c b/src/velociraptor_dummy.c
deleted file mode 100644
index 03f3469accc816684485134aedc5870e8c6439ab..0000000000000000000000000000000000000000
--- a/src/velociraptor_dummy.c
+++ /dev/null
@@ -1,73 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (c) 2018 James Willis (james.s.willis@durham.ac.uk)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-
-/* Config parameters. */
-#include <config.h>
-
-/* Local includes. */
-#include "error.h"
-#include "swift_velociraptor_part.h"
-#include "velociraptor_interface.h"
-
-/* Dummy VELOCIraptor interface for testing compilation without linking the
- * actual VELOCIraptor library. */
-#ifdef HAVE_DUMMY_VELOCIRAPTOR
-struct cosmoinfo {};
-struct unitinfo {};
-struct cell_loc {};
-struct siminfo {};
-
-/*
-int InitVelociraptor(char *config_name, char *output_name,
-                     struct cosmoinfo cosmo_info, struct unitinfo unit_info,
-                     struct siminfo sim_info, const int numthreads) {
-
-  error("This is only a dummy. Call the real one!");
-  return 0;
-}
-
-int InvokeVelociraptor(const size_t num_gravity_parts,
-                       const size_t num_hydro_parts, const int snapnum,
-                       struct swift_vel_part *swift_parts,
-                       const int *cell_node_ids, char *output_name,
-                       const int numthreads) {
-
-  error("This is only a dummy. Call the real one!");
-  return 0;
-}
-*/
-int InitVelociraptor(char *config_name, struct unitinfo unit_info,
-                     struct siminfo sim_info, const int numthreads) {
-
-  error("This is only a dummy. Call the real one!");
-  return 0;
-}
-
-struct groupinfo *InvokeVelociraptor(
-    const int snapnum, char *output_name, struct cosmoinfo cosmo_info,
-    struct siminfo sim_info, const size_t num_gravity_parts,
-    const size_t num_hydro_parts, const size_t num_star_parts,
-    struct swift_vel_part *swift_parts, const int *cell_node_ids,
-    const int numthreads, const int return_group_flags,
-    int *const num_in_groups) {
-  error("This is only a dummy. Call the real one!");
-  return 0;
-}
-
-#endif /* HAVE_DUMMY_VELOCIRAPTOR */
diff --git a/src/velociraptor_interface.c b/src/velociraptor_interface.c
index fab15248c16f0c775fb751fc6fbba99f8c98a1cb..4248bb002b07192cb6c72417107058ccfb45928f 100644
--- a/src/velociraptor_interface.c
+++ b/src/velociraptor_interface.c
@@ -504,6 +504,9 @@ void write_orphan_particle_array(hid_t h_file, const char *name,
   io_write_attribute_f(dset_id, "a-scale exponent",
                        props.scale_factor_exponent);
   io_write_attribute_s(dset_id, "Expression for physical CGS units", buffer);
+  io_write_attribute_b(h_data, "Value stored as physical", props.is_physical);
+  io_write_attribute_b(h_data, "Property can be converted to comoving",
+                       props.is_convertible_to_comoving);
 
   /* Write data, if there is any */
   if (nr_flagged_all > 0) {
@@ -1106,3 +1109,28 @@ void velociraptor_invoke(struct engine *e, const int linked_with_snap) {
   error("SWIFT not configured to run with VELOCIraptor.");
 #endif /* HAVE_VELOCIRAPTOR */
 }
+
+/* Dummy VELOCIraptor interface for testing compilation without linking the
+ * actual VELOCIraptor library. Uses --enable-dummy-velociraptor configure
+ * option. */
+#ifdef HAVE_DUMMY_VELOCIRAPTOR
+
+int InitVelociraptor(char *config_name, struct unitinfo unit_info,
+                     struct siminfo sim_info, const int numthreads) {
+  error("This is only a dummy. Call the real one!");
+  return 0;
+}
+
+struct vr_return_data InvokeVelociraptor(
+    const int snapnum, char *output_name, struct cosmoinfo cosmo_info,
+    struct siminfo sim_info, const size_t num_gravity_parts,
+    const size_t num_hydro_parts, const size_t num_star_parts,
+    struct swift_vel_part *swift_parts, const int *cell_node_ids,
+    const int numthreads, const int return_group_flags,
+    const int return_most_bound) {
+  error("This is only a dummy. Call the real one!");
+  struct vr_return_data data = {0};
+  return data;
+}
+
+#endif /* HAVE_DUMMY_VELOCIRAPTOR */
diff --git a/swift.c b/swift.c
index 319680294b57a3712b2cbeaa4b363041f1234679..151769512a5d2974c9378d5a407bf049e0a5cc11 100644
--- a/swift.c
+++ b/swift.c
@@ -90,6 +90,7 @@ int main(int argc, char *argv[]) {
   struct cooling_function_data cooling_func;
   struct cosmology cosmo;
   struct external_potential potential;
+  struct forcing_terms forcing_terms;
   struct extra_io_properties extra_io_props;
   struct star_formation starform;
   struct pm_mesh mesh;
@@ -176,6 +177,10 @@ int main(int argc, char *argv[]) {
   int with_cooling = 0;
   int with_self_gravity = 0;
   int with_hydro = 0;
+#ifdef MOVING_MESH
+  int with_grid_hydro = 0;
+  int with_grid = 0;
+#endif
   int with_stars = 0;
   int with_fof = 0;
   int with_lightcone = 0;
@@ -408,8 +413,15 @@ int main(int argc, char *argv[]) {
     with_cooling = 1;
     with_feedback = 1;
   }
+#ifdef MOVING_MESH
+  if (with_hydro) {
+    with_grid = 1;
+  }
+#endif
 
   /* Deal with thread numbers */
+  if (nr_threads <= 0)
+    error("Invalid number of threads provided (%d), must be > 0.", nr_threads);
   if (nr_pool_threads == -1) nr_pool_threads = nr_threads;
 
   /* Write output parameter file */
@@ -516,11 +528,17 @@ int main(int argc, char *argv[]) {
 #endif
 
 #ifdef WITH_MPI
+#ifdef SWIFT_DEBUG_CHECKS
+  if (with_sinks) {
+    pretime_message("Warning: sink particles are are WIP yet with MPI.");
+  }
+#else
   if (with_sinks) {
     pretime_message("Error: sink particles are not available yet with MPI.");
     return 1;
   }
-#endif
+#endif /* SWIFT_DEBUG_CHECKS */
+#endif /* WITH_MPI */
 
   if (with_sinks && with_star_formation) {
     pretime_message(
@@ -667,9 +685,6 @@ int main(int argc, char *argv[]) {
   if (with_rt && with_cooling) {
     error("Error: Cannot use radiative transfer and cooling simultaneously.");
   }
-  if (with_rt && with_cosmology) {
-    error("Error: Cannot use run radiative transfer with cosmology (yet).");
-  }
 #endif /* idfef RT_NONE */
 
 #ifdef SINK_NONE
@@ -1074,6 +1089,11 @@ int main(int argc, char *argv[]) {
     if (with_hydro) {
 #ifdef NONE_SPH
       error("Can't run with hydro when compiled without a hydro model!");
+#endif
+#ifdef MOVING_MESH
+      warning(
+          "Moving mesh hydrodynamics is in the process of being merged and "
+          "will not perform as expected right now!");
 #endif
     }
     if (with_stars) {
@@ -1153,7 +1173,8 @@ int main(int argc, char *argv[]) {
 
     /* Initialise the sink properties */
     if (with_sinks) {
-      sink_props_init(&sink_properties, &prog_const, &us, params, &cosmo);
+      sink_props_init(&sink_properties, &feedback_properties, &prog_const, &us,
+                      params, &hydro_properties, &cosmo, with_feedback);
     } else
       bzero(&sink_properties, sizeof(struct sink_props));
 
@@ -1282,7 +1303,7 @@ int main(int argc, char *argv[]) {
       if (!dry_run && gparts[k].id_or_neg_offset == 0 &&
           (gparts[k].type == swift_type_dark_matter ||
            gparts[k].type == swift_type_dark_matter_background))
-        error("SWIFT does not allow the ID 0.");
+        error("SWIFT does not allow the ID 0 for dark matter.");
     if (!with_stars && !dry_run) {
       for (size_t k = 0; k < Ngpart; ++k)
         if (gparts[k].type == swift_type_stars) error("Linking problem");
@@ -1321,7 +1342,7 @@ int main(int argc, char *argv[]) {
     N_long[swift_type_dark_matter] =
         with_gravity ? Ngpart - Ngpart_background - Nbaryons - Nnupart : 0;
 
-    MPI_Allreduce(&N_long, &N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
+    MPI_Allreduce(N_long, N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
                   MPI_SUM, MPI_COMM_WORLD);
 #else
     N_total[swift_type_gas] = Ngas;
@@ -1416,6 +1437,11 @@ int main(int argc, char *argv[]) {
       potential_init(params, &prog_const, &us, &s, &potential);
     if (myrank == 0) potential_print(&potential);
 
+    /* Initialise the forcing terms */
+    bzero(&forcing_terms, sizeof(struct forcing_terms));
+    forcing_terms_init(params, &prog_const, &us, &s, &forcing_terms);
+    if (myrank == 0) forcing_terms_print(&forcing_terms);
+
     /* Initialise the long-range gravity mesh */
     if (with_self_gravity && periodic) {
 #ifdef HAVE_FFTW
@@ -1441,7 +1467,7 @@ int main(int argc, char *argv[]) {
     N_long[swift_type_sink] = s.nr_sinks;
     N_long[swift_type_black_hole] = s.nr_bparts;
     N_long[swift_type_neutrino] = s.nr_nuparts;
-    MPI_Allreduce(&N_long, &N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
+    MPI_Allreduce(N_long, N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
                   MPI_SUM, MPI_COMM_WORLD);
 #else
     N_total[swift_type_gas] = s.nr_parts;
@@ -1501,7 +1527,12 @@ int main(int argc, char *argv[]) {
     if (with_drift_all) engine_policies |= engine_policy_drift_all;
     if (with_mpole_reconstruction)
       engine_policies |= engine_policy_reconstruct_mpoles;
+#ifndef MOVING_MESH
     if (with_hydro) engine_policies |= engine_policy_hydro;
+#else
+    if (with_hydro) engine_policies |= engine_policy_grid_hydro;
+    if (with_grid) engine_policies |= engine_policy_grid;
+#endif
     if (with_self_gravity) engine_policies |= engine_policy_self_gravity;
     if (with_external_gravity)
       engine_policies |= engine_policy_external_gravity;
@@ -1534,9 +1565,9 @@ int main(int argc, char *argv[]) {
                 &gravity_properties, &stars_properties, &black_holes_properties,
                 &sink_properties, &neutrino_properties, &neutrino_response,
                 &feedback_properties, &pressure_floor_props, &rt_properties,
-                &mesh, &pow_data, &potential, &cooling_func, &starform,
-                &chemistry, &extra_io_props, &fof_properties, &los_properties,
-                &lightcone_array_properties, &ics_metadata);
+                &mesh, &pow_data, &potential, &forcing_terms, &cooling_func,
+                &starform, &chemistry, &extra_io_props, &fof_properties,
+                &los_properties, &lightcone_array_properties, &ics_metadata);
     engine_config(/*restart=*/0, /*fof=*/0, &e, params, nr_nodes, myrank,
                   nr_threads, nr_pool_threads, with_aff, talking, restart_dir,
                   restart_file, &reparttype);
@@ -1625,7 +1656,7 @@ int main(int argc, char *argv[]) {
       if (with_power)
         calc_all_power_spectra(e.power_data, e.s, &e.threadpool, e.verbose);
 
-      engine_dump_snapshot(&e);
+      engine_dump_snapshot(&e, /*fof=*/0);
     }
 
     /* Dump initial state statistics, if not working with an output list */
@@ -1868,7 +1899,7 @@ int main(int argc, char *argv[]) {
           !e.stf_this_timestep)
         velociraptor_invoke(&e, /*linked_with_snap=*/1);
 #endif
-      engine_dump_snapshot(&e);
+      engine_dump_snapshot(&e, /*fof=*/0);
 #ifdef HAVE_VELOCIRAPTOR
       if (with_structure_finding && e.snapshot_invoke_stf &&
           e.s->gpart_group_data)
@@ -1927,6 +1958,7 @@ int main(int argc, char *argv[]) {
   free(output_options);
 
 #ifdef WITH_MPI
+  partition_clean(&initial_partition, &reparttype);
   if ((res = MPI_Finalize()) != MPI_SUCCESS)
     error("call to MPI_Finalize failed with error %i.", res);
 #endif
diff --git a/swift_fof.c b/swift_fof.c
index 996015bae300f574c96e3e7ce3d8e9c907ec9000..521eb5b8418ebee88a7c1f826fbf5133ad4b7452 100644
--- a/swift_fof.c
+++ b/swift_fof.c
@@ -525,7 +525,7 @@ int main(int argc, char *argv[]) {
   N_long[swift_type_black_hole] = Nbpart;
   N_long[swift_type_neutrino] = Nnupart;
   N_long[swift_type_count] = Ngpart;
-  MPI_Allreduce(&N_long, &N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
+  MPI_Allreduce(N_long, N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
                 MPI_SUM, MPI_COMM_WORLD);
 #else
   N_total[swift_type_gas] = Ngas;
@@ -613,7 +613,7 @@ int main(int argc, char *argv[]) {
   N_long[swift_type_stars] = s.nr_sparts;
   N_long[swift_type_black_hole] = s.nr_bparts;
   N_long[swift_type_neutrino] = s.nr_nuparts;
-  MPI_Allreduce(&N_long, &N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
+  MPI_Allreduce(N_long, N_total, swift_type_count + 1, MPI_LONG_LONG_INT,
                 MPI_SUM, MPI_COMM_WORLD);
 #else
   N_total[swift_type_gas] = s.nr_parts;
@@ -660,6 +660,7 @@ int main(int argc, char *argv[]) {
       /*neutrino_response=*/NULL, /*feedback_properties=*/NULL,
       /*pressure_floor_properties=*/NULL,
       /*rt_properties=*/NULL, &mesh, /*pow_data=*/NULL, /*potential=*/NULL,
+      /*forcing_terms=*/NULL,
       /*cooling_func=*/NULL, /*starform=*/NULL, /*chemistry=*/NULL,
       /*extra_io_props=*/NULL, &fof_properties, /*los_properties=*/NULL,
       /*lightcone_properties=*/NULL, &ics_metadata);
@@ -715,7 +716,7 @@ int main(int argc, char *argv[]) {
   if (with_sinks) e.policy |= engine_policy_sinks;
 
   /* Write output. */
-  engine_dump_snapshot(&e);
+  engine_dump_snapshot(&e, /*fof=*/1);
 
 #ifdef WITH_MPI
   MPI_Barrier(MPI_COMM_WORLD);
@@ -768,11 +769,6 @@ int main(int argc, char *argv[]) {
   }
 #endif
 
-#ifdef WITH_MPI
-  if ((res = MPI_Finalize()) != MPI_SUCCESS)
-    error("call to MPI_Finalize failed with error %i.", res);
-#endif
-
   /* Clean everything */
   cosmology_clean(&cosmo);
   pm_mesh_clean(&mesh);
@@ -780,6 +776,12 @@ int main(int argc, char *argv[]) {
   free(params);
   free(output_options);
 
+#ifdef WITH_MPI
+  partition_clean(&initial_partition, &reparttype);
+  if ((res = MPI_Finalize()) != MPI_SUCCESS)
+    error("call to MPI_Finalize failed with error %i.", res);
+#endif
+
   /* Say goodbye. */
   if (myrank == 0) message("done. Bye.");
 
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 53fce13c93487429a517e960e958b08ff6921c77..7b10d5204197e2748c80c5523f202acc43c0343f 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -15,7 +15,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # Add the source directory and the non-standard paths to the included library headers to CFLAGS
-AM_CFLAGS = -I$(top_srcdir)/src $(HDF5_CPPFLAGS) $(GSL_INCS) $(FFTW_INCS) $(NUMA_INCS) $(OPENMP_CFLAGS) $(CHEALPIX_CFLAGS)
+AM_CFLAGS = -I$(top_srcdir)/src $(HDF5_CPPFLAGS) $(GSL_INCS) $(FFTW_INCS) $(NUMA_INCS) $(CHEALPIX_CFLAGS)
 
 AM_LDFLAGS = ../src/.libs/libswiftsim.a $(HDF5_LDFLAGS) $(HDF5_LIBS) $(FFTW_LIBS) $(NUMA_LIBS) $(TCMALLOC_LIBS) $(JEMALLOC_LIBS) $(TBBMALLOC_LIBS) $(GRACKLE_LIBS) $(GSL_LIBS) $(PROFILER_LIBS) $(CHEALPIX_LIBS)
 
@@ -29,26 +29,24 @@ TESTS = testGreetings testMaths testReading.sh testKernel testKernelLongGrav \
         testParser.sh test125cells.sh test125cellsPerturbed.sh testFFT \
         testAdiabaticIndex testRandom testRandomSpacing testRandomPoisson testErfc \
         testMatrixInversion testThreadpool testDump testCSDS testInteractions.sh \
-        testVoronoi1D testVoronoi2D testVoronoi3D testGravityDerivatives \
-	testPeriodicBC.sh testPeriodicBCPerturbed.sh testPotentialSelf \
-	testPotentialPair testEOS testUtilities testSelectOutput.sh \
-	testCbrt testCosmology testRandomCone testOutputList testFormat.sh \
-	test27cellsStars.sh test27cellsStarsPerturbed.sh testHydroMPIrules \
+        testGravityDerivatives testPeriodicBC.sh testPeriodicBCPerturbed.sh \
+        testPotentialSelf testPotentialPair testEOS testUtilities testSelectOutput.sh \
+        testCbrt testCosmology testRandomCone testOutputList testFormat.sh \
+        test27cellsStars.sh test27cellsStarsPerturbed.sh testHydroMPIrules \
         testAtomic testGravitySpeed testNeutrinoCosmology.sh testNeutrinoFermiDirac \
-	testLog testDistance testTimeline
+	    testLog testDistance testTimeline
 
 # List of test programs to compile
 check_PROGRAMS = testGreetings testReading testTimeIntegration testKernelLongGrav \
 		 testActivePair test27cells test27cells_subset test125cells testParser \
-                 testKernel testFFT testInteractions testMaths testRandom testExp \
-                 testSymmetry testDistance testThreadpool testRandomSpacing testErfc \
-                 testAdiabaticIndex testRiemannExact testRiemannTRRS testRandomPoisson testRandomCone \
-                 testRiemannHLLC testMatrixInversion testDump testCSDS \
-		 testVoronoi1D testVoronoi2D testVoronoi3D testPeriodicBC \
-		 testGravityDerivatives testPotentialSelf testPotentialPair testEOS testUtilities \
-		 testSelectOutput testCbrt testCosmology testOutputList test27cellsStars \
-		 test27cellsStars_subset testCooling testComovingCooling testFeedback testHashmap \
-                 testAtomic testHydroMPIrules testGravitySpeed testNeutrinoCosmology \
+         testKernel testFFT testInteractions testMaths testRandom testExp \
+         testSymmetry testDistance testThreadpool testRandomSpacing testErfc \
+         testAdiabaticIndex testRiemannExact testRiemannTRRS testRandomPoisson testRandomCone \
+         testRiemannHLLC testMatrixInversion testDump testCSDS \
+		 testPeriodicBC testGravityDerivatives testPotentialSelf testPotentialPair testEOS \
+		 testUtilities testSelectOutput testCbrt testCosmology testOutputList \
+		 test27cellsStars test27cellsStars_subset testCooling testComovingCooling testFeedback \
+		 testHashmap testAtomic testHydroMPIrules testGravitySpeed testNeutrinoCosmology \
 		 testNeutrinoFermiDirac testLog testTimeline
 
 # Rebuild tests when SWIFT is updated.
@@ -128,12 +126,6 @@ testRiemannHLLC_SOURCES = testRiemannHLLC.c
 
 testMatrixInversion_SOURCES = testMatrixInversion.c
 
-testVoronoi1D_SOURCES = testVoronoi1D.c
-
-testVoronoi2D_SOURCES = testVoronoi2D.c
-
-testVoronoi3D_SOURCES = testVoronoi3D.c
-
 testThreadpool_SOURCES = testThreadpool.c
 
 testDump_SOURCES = testDump.c
@@ -172,15 +164,15 @@ testHydroMPIrules = testHydroMPIrules.c
 
 # Files necessary for distribution
 EXTRA_DIST = testReading.sh makeInput.py testActivePair.sh \
-	     test27cells.sh test27cellsPerturbed.sh testParser.sh testPeriodicBC.sh \
-	     testPeriodicBCPerturbed.sh test125cells.sh test125cellsPerturbed.sh testParserInput.yaml \
-	     difffloat.py tolerance_125_normal.dat tolerance_125_perturbed.dat \
+             test27cells.sh test27cellsPerturbed.sh testParser.sh testPeriodicBC.sh \
+             testPeriodicBCPerturbed.sh test125cells.sh test125cellsPerturbed.sh testParserInput.yaml \
+             difffloat.py tolerance_125_normal.dat tolerance_125_perturbed.dat \
              tolerance_27_normal.dat tolerance_27_perturbed.dat tolerance_27_perturbed_h.dat tolerance_27_perturbed_h2.dat \
-	     tolerance_testInteractions.dat tolerance_pair_active.dat tolerance_pair_force_active.dat \
-	     fft_params.yml tolerance_periodic_BC_normal.dat tolerance_periodic_BC_perturbed.dat \
-	     testEOS.sh testEOS_plot.sh testSelectOutput.sh selectOutput.yml \
+             tolerance_testInteractions.dat tolerance_pair_active.dat tolerance_pair_force_active.dat \
+             fft_params.yml tolerance_periodic_BC_normal.dat tolerance_periodic_BC_perturbed.dat \
+             testEOS_plot.sh testSelectOutput.sh selectOutput.yml \
              output_list_params.yml output_list_time.txt output_list_redshift.txt \
-             output_list_scale_factor.txt testEOS.sh testEOS_plot.sh \
-	     test27cellsStars.sh test27cellsStarsPerturbed.sh star_tolerance_27_normal.dat \
-	     star_tolerance_27_perturbed.dat star_tolerance_27_perturbed_h.dat star_tolerance_27_perturbed_h2.dat \
-	     testNeutrinoCosmology.dat testNeutrinoCosmology.sh
+             output_list_scale_factor.txt testEOS_plot.sh \
+             test27cellsStars.sh test27cellsStarsPerturbed.sh star_tolerance_27_normal.dat \
+             star_tolerance_27_perturbed.dat star_tolerance_27_perturbed_h.dat star_tolerance_27_perturbed_h2.dat \
+             testNeutrinoCosmology.dat testNeutrinoCosmology.sh
diff --git a/tests/difffloat.py b/tests/difffloat.py
index da662113a86fda130fe7417ee5d634d48053cb4a..c91e815d97b0d1fa9c38fdb44ed25855c370658c 100644
--- a/tests/difffloat.py
+++ b/tests/difffloat.py
@@ -17,7 +17,7 @@
 #
 ##############################################################################
 
-from numpy import *
+import numpy as np
 import sys
 
 abs_tol = 1e-7
@@ -48,20 +48,20 @@ with open(file1, "r") as f:
     if "ID" in line:
         part_props = line.split()[1:]
 
-data1 = loadtxt(file1)
-data2 = loadtxt(file2)
+data1 = np.loadtxt(file1)
+data2 = np.loadtxt(file2)
 if fileTol != "":
-    dataTol = loadtxt(fileTol)
-    n_linesTol = shape(dataTol)[0]
-    n_columnsTol = shape(dataTol)[1]
+    dataTol = np.loadtxt(fileTol)
+    n_linesTol = np.shape(dataTol)[0]
+    n_columnsTol = np.shape(dataTol)[1]
 
 
-if shape(data1) != shape(data2):
+if np.shape(data1) != np.shape(data2):
     print("Non-matching array sizes in the files", file1, "and", file2, ".")
     sys.exit(1)
 
-n_lines = shape(data1)[0]
-n_columns = shape(data1)[1]
+n_lines = np.shape(data1)[0]
+n_columns = np.shape(data1)[1]
 
 if fileTol != "":
     if n_linesTol != 3:
@@ -72,9 +72,9 @@ if fileTol != "":
 if fileTol == "":
     print("Absolute difference tolerance:", abs_tol)
     print("Relative difference tolerance:", rel_tol)
-    absTol = ones(n_columns) * abs_tol
-    relTol = ones(n_columns) * rel_tol
-    limTol = zeros(n_columns)
+    absTol = np.ones(n_columns) * abs_tol
+    relTol = np.ones(n_columns) * rel_tol
+    limTol = np.zeros(n_columns)
 else:
     print("Tolerances read from file")
     absTol = dataTol[0, :]
diff --git a/tests/makeInput.py b/tests/makeInput.py
index c3544d7a25f93ec5c914754db2a93d8a6b30b7c4..43fe3da976e28e0cdb00c6ade0dcf27c21cb642f 100644
--- a/tests/makeInput.py
+++ b/tests/makeInput.py
@@ -27,18 +27,19 @@ from numpy import *
 periodic = 1  # 1 For periodic box
 boxSize = 1.0
 L = 4  # Number of particles along one axis
-rho = 2.0  # Density
+density = 2.0  # Density
 P = 1.0  # Pressure
 gamma = 5.0 / 3.0  # Gas adiabatic index
+material = 0 # Ideal gas
 fileName = "input.hdf5"
 
 # ---------------------------------------------------
 numPart = L ** 3
-mass = boxSize ** 3 * rho / numPart
-internalEnergy = P / ((gamma - 1.0) * rho)
+mass = boxSize ** 3 * density / numPart
+internalEnergy = P / ((gamma - 1.0) * density)
 
 # chemistry data
-he_density = rho * 0.24
+he_density = density * 0.24
 
 # Generate particles
 coords = zeros((numPart, 3))
@@ -46,7 +47,9 @@ v = zeros((numPart, 3))
 m = zeros((numPart, 1))
 h = zeros((numPart, 1))
 u = zeros((numPart, 1))
+rho = zeros((numPart, 1))
 ids = zeros((numPart, 1), dtype="L")
+mat = zeros((numPart, 1), dtype="i")
 
 # chemistry data
 he = zeros((numPart, 1))
@@ -67,7 +70,9 @@ for i in range(L):
             m[index] = mass
             h[index] = 2.251 * boxSize / L
             u[index] = internalEnergy
+            rho[index] = density
             ids[index] = index
+            mat[index] = material
             # chemistry data
             he[index] = he_density
 
@@ -114,8 +119,12 @@ ds = grp.create_dataset("SmoothingLength", (numPart, 1), "f")
 ds[()] = h
 ds = grp.create_dataset("InternalEnergy", (numPart, 1), "f")
 ds[()] = u
+ds = grp.create_dataset("Density", (numPart, 1), "f")
+ds[()] = rho
 ds = grp.create_dataset("ParticleIDs", (numPart, 1), "L")
 ds[()] = ids
+ds = grp.create_dataset("MaterialIDs", (numPart, 1), "i")
+ds[()] = mat
 # chemistry
 ds = grp.create_dataset("HeDensity", (numPart, 1), "f")
 ds[()] = he
diff --git a/tests/test125cells.c b/tests/test125cells.c
index 9d2c3638668d5714478fa3a36d95ed24793a235c..128c36480fb36723b713a0b3e98e8bdd83fc9559 100644
--- a/tests/test125cells.c
+++ b/tests/test125cells.c
@@ -115,12 +115,12 @@ void set_energy_state(struct part *part, enum pressure_field press, float size,
     defined(HOPKINS_PU_SPH_MONAGHAN) || defined(ANARCHY_PU_SPH) || \
     defined(SPHENIX_SPH) || defined(PHANTOM_SPH) || defined(GASOLINE_SPH)
   part->u = pressure / (hydro_gamma_minus_one * density);
-#elif defined(PLANETARY_SPH)
+#elif defined(PLANETARY_SPH) || defined(REMIX_SPH)
+  set_idg_def(&eos.idg_def, 0);
+  part->mat_id = 0;
   part->u = pressure / (hydro_gamma_minus_one * density);
 #elif defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
   part->conserved.energy = pressure / (hydro_gamma_minus_one * density);
-#elif defined(SHADOWFAX_SPH)
-  part->primitives.P = pressure;
 #else
   error("Need to define pressure here !");
 #endif
@@ -221,27 +221,15 @@ void reset_particles(struct cell *c, struct hydro_space *hs,
     hydro_first_init_part(p, &c->hydro.xparts[i]);
     p->time_bin = 1;
 #endif
+#if defined(REMIX_SPH)
+    p->rho = density;
+    hydro_first_init_part(p, &c->hydro.xparts[i]);
+    p->time_bin = 1;
+#endif
 
     hydro_init_part(p, hs);
-
-#if defined(SHADOWFAX_SPH)
-    float volume = p->conserved.mass / density;
-    p->cell.volume = volume;
-    p->primitives.rho = density;
-    p->primitives.v[0] = p->v[0];
-    p->primitives.v[1] = p->v[1];
-    p->primitives.v[2] = p->v[2];
-    p->conserved.momentum[0] = p->conserved.mass * p->v[0];
-    p->conserved.momentum[1] = p->conserved.mass * p->v[1];
-    p->conserved.momentum[2] = p->conserved.mass * p->v[2];
-    p->conserved.energy =
-        p->primitives.P / hydro_gamma_minus_one * volume +
-        0.5f *
-            (p->conserved.momentum[0] * p->conserved.momentum[0] +
-             p->conserved.momentum[1] * p->conserved.momentum[1] +
-             p->conserved.momentum[2] * p->conserved.momentum[2]) /
-            p->conserved.mass;
-#endif
+    adaptive_softening_init_part(p);
+    mhd_init_part(p);
   }
 }
 
@@ -301,12 +289,16 @@ struct cell *make_cell(size_t n, const double offset[3], double size, double h,
             size * (z + 0.5 + random_uniform(-0.5, 0.5) * pert) / (float)n;
         part->h = size * h / (float)n;
         h_max = fmax(h_max, part->h);
+        part->depth_h = 0;
 
-#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(SHADOWFAX_SPH)
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
         part->conserved.mass = density * volume / count;
 #else
         part->mass = density * volume / count;
 #endif
+#if defined(REMIX_SPH)
+        part->rho = density;
+#endif
 
         set_velocity(part, vel, size);
         set_energy_state(part, press, size, density);
@@ -329,6 +321,7 @@ struct cell *make_cell(size_t n, const double offset[3], double size, double h,
 
   /* Cell properties */
   cell->split = 0;
+  cell->depth = 0;
   cell->hydro.h_max = h_max;
   cell->hydro.h_max_active = h_max;
   cell->hydro.count = count;
@@ -338,9 +331,12 @@ struct cell *make_cell(size_t n, const double offset[3], double size, double h,
   cell->width[0] = size;
   cell->width[1] = size;
   cell->width[2] = size;
+  cell->dmin = size;
   cell->loc[0] = offset[0];
   cell->loc[1] = offset[1];
   cell->loc[2] = offset[2];
+  cell->h_min_allowed = cell->dmin * 0.5 * (1. / kernel_gamma);
+  cell->h_max_allowed = cell->dmin * (1. / kernel_gamma);
 
   cell->hydro.super = cell;
   cell->hydro.ti_old_part = 8;
@@ -390,10 +386,10 @@ void dump_particle_fields(char *fileName, struct cell *main_cell,
             main_cell->hydro.parts[pid].v[0], main_cell->hydro.parts[pid].v[1],
             main_cell->hydro.parts[pid].v[2], main_cell->hydro.parts[pid].h,
             hydro_get_comoving_density(&main_cell->hydro.parts[pid]),
-#if defined(MINIMAL_SPH) || defined(PLANETARY_SPH) ||    \
-    defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) ||  \
-    defined(SHADOWFAX_SPH) || defined(HOPKINS_PU_SPH) || \
-    defined(HOPKINS_PU_SPH_MONAGHAN) || defined(GASOLINE_SPH)
+#if defined(MINIMAL_SPH) || defined(PLANETARY_SPH) ||              \
+    defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) ||            \
+    defined(HOPKINS_PU_SPH) || defined(HOPKINS_PU_SPH_MONAGHAN) || \
+    defined(GASOLINE_SPH) || defined(REMIX_SPH)
             0.f,
 #elif defined(ANARCHY_PU_SPH) || defined(SPHENIX_SPH) || defined(PHANTOM_SPH)
             main_cell->hydro.parts[pid].viscosity.div_v,
@@ -451,17 +447,31 @@ void dump_particle_fields(char *fileName, struct cell *main_cell,
 
 /* Just a forward declaration... */
 void runner_dopair1_branch_density(struct runner *r, struct cell *ci,
-                                   struct cell *cj);
-void runner_doself1_branch_density(struct runner *r, struct cell *ci);
+                                   struct cell *cj, int limit_h_min,
+                                   int limit_h_max);
+void runner_doself1_branch_density(struct runner *r, struct cell *ci,
+                                   int limit_h_min, int limit_h_max);
 #ifdef EXTRA_HYDRO_LOOP
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+void runner_dopair2_branch_gradient(struct runner *r, struct cell *ci,
+                                    struct cell *cj, int limit_h_min,
+                                    int limit_h_max);
+void runner_doself2_branch_gradient(struct runner *r, struct cell *ci,
+                                    int limit_h_min, int limit_h_max);
+#else
 void runner_dopair1_branch_gradient(struct runner *r, struct cell *ci,
-                                    struct cell *cj);
-void runner_doself1_branch_gradient(struct runner *r, struct cell *ci);
+                                    struct cell *cj, int limit_h_min,
+                                    int limit_h_max);
+void runner_doself1_branch_gradient(struct runner *r, struct cell *ci,
+                                    int limit_h_min, int limit_h_max);
+#endif /* EXTRA_HYDRO_LOOP_TYPE2 */
 #endif /* EXTRA_HYDRO LOOP */
 void runner_dopair2_branch_force(struct runner *r, struct cell *ci,
-                                 struct cell *cj);
-void runner_doself2_branch_force(struct runner *r, struct cell *ci);
-void runner_doself2_force(struct runner *r, struct cell *ci);
+                                 struct cell *cj, int limit_h_min,
+                                 int limit_h_max);
+void runner_doself2_branch_force(struct runner *r, struct cell *ci,
+                                 int limit_h_min, int limit_h_max);
+
 void runner_doself2_force_vec(struct runner *r, struct cell *ci);
 
 /* And go... */
@@ -608,6 +618,10 @@ int main(int argc, char *argv[]) {
   cosmology_init_no_cosmo(&cosmo);
   engine.cosmology = &cosmo;
 
+  struct sink_props sink_props;
+  bzero(&sink_props, sizeof(struct sink_props));
+  engine.sink_properties = &sink_props;
+
   struct runner runner;
   runner.e = &engine;
 
@@ -666,13 +680,16 @@ int main(int argc, char *argv[]) {
 
     /* Reset particles. */
     for (int i = 0; i < 125; ++i) {
-      for (int pid = 0; pid < cells[i]->hydro.count; ++pid)
+      for (int pid = 0; pid < cells[i]->hydro.count; ++pid) {
         hydro_init_part(&cells[i]->hydro.parts[pid], &space.hs);
+        adaptive_softening_init_part(&cells[i]->hydro.parts[pid]);
+        mhd_init_part(&cells[i]->hydro.parts[pid]);
+      }
     }
 
     /* First, sort stuff */
     for (int j = 0; j < 125; ++j)
-      runner_do_hydro_sort(&runner, cells[j], 0x1FFF, 0, 0, 0);
+      runner_do_hydro_sort(&runner, cells[j], 0x1FFF, 0, 0, 0, 0);
 
       /* Do the density calculation */
 
@@ -706,7 +723,9 @@ int main(int argc, char *argv[]) {
 
                 struct cell *cj = cells[iii * 25 + jjj * 5 + kkk];
 
-                if (cj > ci) runner_dopair1_branch_density(&runner, ci, cj);
+                if (cj > ci)
+                  runner_dopair1_branch_density(
+                      &runner, ci, cj, /*limit_h_min=*/0, /*limit_h_max=*/0);
               }
             }
           }
@@ -716,7 +735,8 @@ int main(int argc, char *argv[]) {
 
     /* And now the self-interaction for the central cells*/
     for (int j = 0; j < 27; ++j)
-      runner_doself1_branch_density(&runner, inner_cells[j]);
+      runner_doself1_branch_density(&runner, inner_cells[j], /*limit_h_min=*/0,
+                                    /*limit_h_max=*/0);
 
     /* Ghost to finish everything on the central cells */
     for (int j = 0; j < 27; ++j) runner_do_ghost(&runner, inner_cells[j], 0);
@@ -748,7 +768,15 @@ int main(int argc, char *argv[]) {
 
                 struct cell *cj = cells[iii * 25 + jjj * 5 + kkk];
 
-                if (cj > ci) runner_dopair1_branch_gradient(&runner, ci, cj);
+                if (cj > ci) {
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+                  runner_dopair2_branch_gradient(
+                      &runner, ci, cj, /*limit_h_min=*/0, /*limit_h_max=*/0);
+#else
+                  runner_dopair1_branch_gradient(
+                      &runner, ci, cj, /*limit_h_min=*/0, /*limit_h_max=*/0);
+#endif
+                }
               }
             }
           }
@@ -757,8 +785,15 @@ int main(int argc, char *argv[]) {
     }
 
     /* And now the self-interaction for the central cells */
-    for (int j = 0; j < 27; ++j)
-      runner_doself1_branch_gradient(&runner, inner_cells[j]);
+    for (int j = 0; j < 27; ++j) {
+#ifdef EXTRA_HYDRO_LOOP_TYPE2
+      runner_doself2_branch_gradient(&runner, inner_cells[j], /*limit_h_min=*/0,
+                                     /*limit_h_max=*/0);
+#else
+      runner_doself1_branch_gradient(&runner, inner_cells[j], /*limit_h_min=*/0,
+                                     /*limit_h_max=*/0);
+#endif
+    }
 
     /* Extra ghost to finish everything on the central cells */
     for (int j = 0; j < 27; ++j)
@@ -788,7 +823,8 @@ int main(int argc, char *argv[]) {
 
             const ticks sub_tic = getticks();
 
-            runner_dopair2_branch_force(&runner, main_cell, cj);
+            runner_dopair2_branch_force(&runner, main_cell, cj,
+                                        /*limit_h_min=*/0, /*limit_h_max=*/0);
 
             timings[ctr++] += getticks() - sub_tic;
           }
@@ -799,7 +835,8 @@ int main(int argc, char *argv[]) {
     ticks self_tic = getticks();
 
     /* And now the self-interaction for the main cell */
-    runner_doself2_branch_force(&runner, main_cell);
+    runner_doself2_branch_force(&runner, main_cell, /*limit_h_min=*/0,
+                                /*limit_h_max=*/0);
 
     timings[26] += getticks() - self_tic;
 
@@ -816,8 +853,11 @@ int main(int argc, char *argv[]) {
     }
 
     for (int i = 0; i < 125; ++i) {
-      for (int pid = 0; pid < cells[i]->hydro.count; ++pid)
+      for (int pid = 0; pid < cells[i]->hydro.count; ++pid) {
         hydro_init_part(&cells[i]->hydro.parts[pid], &space.hs);
+        adaptive_softening_init_part(&cells[i]->hydro.parts[pid]);
+        mhd_init_part(&cells[i]->hydro.parts[pid]);
+      }
     }
   }
 
diff --git a/tests/test27cells.c b/tests/test27cells.c
index b463a3b328b5733847ccafaab88a58ebbc4673e5..e90d4e1781058c66c71390b357486749ec39c176 100644
--- a/tests/test27cells.c
+++ b/tests/test27cells.c
@@ -152,19 +152,17 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
           part->h = size * h / (float)n;
         h_max = fmaxf(h_max, part->h);
         part->id = ++(*partId);
+        part->depth_h = 0;
 
-#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(SHADOWFAX_SPH)
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
         part->conserved.mass = density * volume / count;
 
-#ifdef SHADOWFAX_SPH
-        double anchor[3] = {0., 0., 0.};
-        double side[3] = {1., 1., 1.};
-        voronoi_cell_init(&part->cell, part->x, anchor, side);
-#endif
-
 #else
         part->mass = density * volume / count;
 #endif
+#if defined(REMIX_SPH)
+        part->rho_evol = density;
+#endif
 
 #if defined(HOPKINS_PE_SPH)
         part->entropy = 1.f;
@@ -185,6 +183,7 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
 
   /* Cell properties */
   cell->split = 0;
+  cell->depth = 0;
   cell->hydro.h_max = h_max;
   cell->hydro.h_max_active = h_max;
   cell->hydro.count = count;
@@ -193,9 +192,12 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
   cell->width[0] = size;
   cell->width[1] = size;
   cell->width[2] = size;
+  cell->dmin = size;
   cell->loc[0] = offset[0];
   cell->loc[1] = offset[1];
   cell->loc[2] = offset[2];
+  cell->h_min_allowed = cell->dmin * 0.5 * (1. / kernel_gamma);
+  cell->h_max_allowed = cell->dmin * (1. / kernel_gamma);
 
   cell->hydro.super = cell;
   cell->hydro.ti_old_part = 8;
@@ -220,29 +222,25 @@ void clean_up(struct cell *ci) {
  * @brief Initializes all particles field to be ready for a density calculation
  */
 void zero_particle_fields(struct cell *c) {
-#ifdef SHADOWFAX_SPH
-  struct hydro_space hs;
-  hs.anchor[0] = 0.;
-  hs.anchor[1] = 0.;
-  hs.anchor[2] = 0.;
-  hs.side[0] = 1.;
-  hs.side[1] = 1.;
-  hs.side[2] = 1.;
-  struct hydro_space *hspointer = &hs;
-#else
   struct hydro_space *hspointer = NULL;
-#endif
+
   for (int pid = 0; pid < c->hydro.count; pid++) {
     hydro_init_part(&c->hydro.parts[pid], hspointer);
+    adaptive_softening_init_part(&c->hydro.parts[pid]);
+    mhd_init_part(&c->hydro.parts[pid]);
   }
 }
 
 /**
  * @brief Ends the loop by adding the appropriate coefficients
  */
-void end_calculation(struct cell *c, const struct cosmology *cosmo) {
+void end_calculation(struct cell *c, const struct cosmology *cosmo,
+                     const struct gravity_props *gravity_props) {
+
   for (int pid = 0; pid < c->hydro.count; pid++) {
     hydro_end_density(&c->hydro.parts[pid], cosmo);
+    adaptive_softening_end_density(&c->hydro.parts[pid], gravity_props);
+    mhd_end_density(&c->hydro.parts[pid], cosmo);
 
     /* Recover the common "Neighbour number" definition */
     c->hydro.parts[pid].density.wcount *= pow_dimension(c->hydro.parts[pid].h);
@@ -276,7 +274,7 @@ void dump_particle_fields(char *fileName, struct cell *main_cell,
             main_cell->hydro.parts[pid].v[0], main_cell->hydro.parts[pid].v[1],
             main_cell->hydro.parts[pid].v[2],
             hydro_get_comoving_density(&main_cell->hydro.parts[pid]),
-#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(SHADOWFAX_SPH)
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
             0.f,
 #elif defined(HOPKINS_PU_SPH) || defined(HOPKINS_PU_SPH_MONAGHAN) || \
     defined(ANARCHY_PU_SPH)
@@ -325,7 +323,7 @@ void dump_particle_fields(char *fileName, struct cell *main_cell,
               cj->hydro.parts[pjd].v[0], cj->hydro.parts[pjd].v[1],
               cj->hydro.parts[pjd].v[2],
               hydro_get_comoving_density(&cj->hydro.parts[pjd]),
-#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(SHADOWFAX_SPH)
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
               0.f,
 #else
               main_cell->hydro.parts[pjd].density.rho_dh,
@@ -356,8 +354,10 @@ void dump_particle_fields(char *fileName, struct cell *main_cell,
 
 /* Just a forward declaration... */
 void runner_dopair1_branch_density(struct runner *r, struct cell *ci,
-                                   struct cell *cj);
-void runner_doself1_branch_density(struct runner *r, struct cell *c);
+                                   struct cell *cj, int limit_h_min,
+                                   int limit_h_max);
+void runner_doself1_branch_density(struct runner *r, struct cell *c,
+                                   int limit_h_min, int limit_h_max);
 void runner_dopair_subset_branch_density(struct runner *r,
                                          struct cell *restrict ci,
                                          struct part *restrict parts_i,
@@ -495,6 +495,14 @@ int main(int argc, char *argv[]) {
   cosmology_init_no_cosmo(&cosmo);
   engine.cosmology = &cosmo;
 
+  struct gravity_props gravity_props;
+  bzero(&gravity_props, sizeof(struct gravity_props));
+  gravity_props.G_Newton = 1.;
+
+  struct sink_props sink_props;
+  bzero(&sink_props, sizeof(struct sink_props));
+  engine.sink_properties = &sink_props;
+
   struct runner runner;
   runner.e = &engine;
 
@@ -519,7 +527,7 @@ int main(int argc, char *argv[]) {
 
         runner_do_drift_part(&runner, cells[i * 9 + j * 3 + k], 0);
 
-        runner_do_hydro_sort(&runner, cells[i * 9 + j * 3 + k], 0x1FFF, 0, 0,
+        runner_do_hydro_sort(&runner, cells[i * 9 + j * 3 + k], 0x1FFF, 0, 0, 0,
                              0);
       }
     }
@@ -566,7 +574,8 @@ int main(int argc, char *argv[]) {
         DOPAIR1_SUBSET(&runner, main_cell, main_cell->hydro.parts, pid, count,
                        cells[j]);
 #else
-        DOPAIR1(&runner, main_cell, cells[j]);
+        DOPAIR1(&runner, main_cell, cells[j], /*limit_h_min=*/0,
+                /*limit_h_max=*/0);
 #endif
 
         timings[j] += getticks() - sub_tic;
@@ -579,7 +588,7 @@ int main(int argc, char *argv[]) {
 #ifdef TEST_DOSELF_SUBSET
     DOSELF1_SUBSET(&runner, main_cell, main_cell->hydro.parts, pid, count);
 #else
-    DOSELF1(&runner, main_cell);
+    DOSELF1(&runner, main_cell, /*limit_h_min=*/0, /*limit_h_max=*/0);
 #endif
 
     timings[13] += getticks() - self_tic;
@@ -588,7 +597,7 @@ int main(int argc, char *argv[]) {
     time += toc - tic;
 
     /* Let's get physical ! */
-    end_calculation(main_cell, &cosmo);
+    end_calculation(main_cell, &cosmo, &gravity_props);
 
     /* Dump if necessary */
     if (i % 50 == 0) {
@@ -639,7 +648,7 @@ int main(int argc, char *argv[]) {
   const ticks toc = getticks();
 
   /* Let's get physical ! */
-  end_calculation(main_cell, &cosmo);
+  end_calculation(main_cell, &cosmo, &gravity_props);
 
   /* Dump */
   sprintf(outputFileName, "brute_force_27_%.150s.dat", outputFileNameExtension);
diff --git a/tests/test27cellsStars.c b/tests/test27cellsStars.c
index 7785cdaed65e1415626a8f997c15bed1500ad80e..2bfdf9ce3590e182508df110002de9b4838d052f 100644
--- a/tests/test27cellsStars.c
+++ b/tests/test27cellsStars.c
@@ -174,9 +174,12 @@ struct cell *make_cell(size_t n, size_t n_stars, double *offset, double size,
   cell->width[0] = size;
   cell->width[1] = size;
   cell->width[2] = size;
+  cell->dmin = size;
   cell->loc[0] = offset[0];
   cell->loc[1] = offset[1];
   cell->loc[2] = offset[2];
+  cell->h_min_allowed = cell->dmin * 0.5 * (1. / kernel_gamma);
+  cell->h_max_allowed = cell->dmin * (1. / kernel_gamma);
 
   cell->hydro.super = cell;
   cell->stars.ti_old_part = 8;
@@ -277,8 +280,10 @@ void dump_particle_fields(char *fileName, struct cell *main_cell,
 
 /* Just a forward declaration... */
 void runner_dopair_branch_stars_density(struct runner *r, struct cell *ci,
-                                        struct cell *cj);
-void runner_doself_branch_stars_density(struct runner *r, struct cell *c);
+                                        struct cell *cj, int limit_h_min,
+                                        int limit_h_max);
+void runner_doself_branch_stars_density(struct runner *r, struct cell *c,
+                                        int limit_h_min, int limit_h_max);
 void runner_dopair_subset_branch_stars_density(struct runner *r,
                                                struct cell *restrict ci,
                                                struct spart *restrict sparts_i,
@@ -430,7 +435,7 @@ int main(int argc, char *argv[]) {
         runner_do_drift_part(&runner, cells[i * 9 + j * 3 + k], 0);
         runner_do_drift_spart(&runner, cells[i * 9 + j * 3 + k], 0);
 
-        runner_do_hydro_sort(&runner, cells[i * 9 + j * 3 + k], 0x1FFF, 0, 0,
+        runner_do_hydro_sort(&runner, cells[i * 9 + j * 3 + k], 0x1FFF, 0, 0, 0,
                              0);
         runner_do_stars_sort(&runner, cells[i * 9 + j * 3 + k], 0x1FFF, 0, 0);
       }
@@ -471,7 +476,8 @@ int main(int argc, char *argv[]) {
         DOPAIR1_SUBSET(&runner, main_cell, main_cell->stars.parts, pid, scount,
                        cells[j]);
 #else
-        DOPAIR1(&runner, main_cell, cells[j]);
+        DOPAIR1(&runner, main_cell, cells[j], /*limit_h_min=*/0,
+                /*limit_h_max=*/0);
 #endif
 
         timings[j] += getticks() - sub_tic;
@@ -484,7 +490,8 @@ int main(int argc, char *argv[]) {
 #ifdef TEST_DOSELF_SUBSET
     DOSELF1_SUBSET(&runner, main_cell, main_cell->stars.parts, pid, scount);
 #else
-    DOSELF1(&runner, main_cell);
+    DOSELF1(&runner, main_cell, /*limit_h_min=*/0,
+            /*limit_h_max=*/0);
 #endif
 
     timings[13] += getticks() - self_tic;
diff --git a/tests/testActivePair.c b/tests/testActivePair.c
index ed02a6890d07321f81b2a8f6454b42177ad77f11..1a9f5e849c89731941138957fb51c590a7240219 100644
--- a/tests/testActivePair.c
+++ b/tests/testActivePair.c
@@ -29,14 +29,18 @@
 /* Local headers. */
 #include "swift.h"
 
-#define NODE_ID 1
+#define NODE_ID 0
 
 /* Typdef function pointer for interaction function. */
-typedef void (*interaction_func)(struct runner *, struct cell *, struct cell *);
+typedef void (*serial_interaction_func)(struct runner *, struct cell *,
+                                        struct cell *);
+typedef void (*interaction_func)(struct runner *, struct cell *, struct cell *,
+                                 int, int);
 typedef void (*init_func)(struct cell *, const struct cosmology *,
                           const struct hydro_props *,
                           const struct pressure_floor_props *);
-typedef void (*finalise_func)(struct cell *, const struct cosmology *);
+typedef void (*finalise_func)(struct cell *, const struct cosmology *,
+                              const struct gravity_props *);
 
 /**
  * @brief Constructs a cell and all of its particle in a valid state prior to
@@ -61,6 +65,7 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
   const size_t count = n * n * n;
   const double volume = size * size * size;
   float h_max = 0.f;
+  float h_max_active = 0.f;
   struct cell *cell = NULL;
   if (posix_memalign((void **)&cell, cell_align, sizeof(struct cell)) != 0) {
     error("Couldn't allocate the cell");
@@ -72,6 +77,11 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
     error("couldn't allocate particles, no. of particles: %d", (int)count);
   }
   bzero(cell->hydro.parts, count * sizeof(struct part));
+  if (posix_memalign((void **)&cell->hydro.xparts, part_align,
+                     count * sizeof(struct xpart)) != 0) {
+    error("couldn't allocate x particles, no. of particles: %d", (int)count);
+  }
+  bzero(cell->hydro.xparts, count * sizeof(struct xpart));
 
   /* Construct the parts */
   struct part *part = cell->hydro.parts;
@@ -97,17 +107,11 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
           part->h = size * h / (float)n;
         h_max = fmaxf(h_max, part->h);
         part->id = ++(*partId);
+        part->depth_h = 0;
 
 /* Set the mass */
-#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(SHADOWFAX_SPH)
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
         part->conserved.mass = density * volume / count;
-
-#ifdef SHADOWFAX_SPH
-        double anchor[3] = {0., 0., 0.};
-        double side[3] = {1., 1., 1.};
-        voronoi_cell_init(&part->cell, part->x, anchor, side);
-#endif /* SHADOWFAX_SPH */
-
 #else
         part->mass = density * volume / count;
 #endif
@@ -124,6 +128,15 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
         part->entropy_one_over_gamma = 1.f;
 #elif defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
         part->conserved.energy = 1.f;
+#elif defined(PLANETARY_SPH)
+        set_idg_def(&eos.idg_def, 0);
+        part->mat_id = 0;
+        part->u = 1.f;
+#elif defined(REMIX_SPH)
+        set_idg_def(&eos.idg_def, 0);
+        part->mat_id = 0;
+        part->u = 1.f;
+        part->rho_evol = 1.f;
 #endif
 
 #if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
@@ -132,10 +145,12 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
 #endif
 
         /* Set the time-bin */
-        if (random_uniform(0, 1.f) < fraction_active)
+        if (random_uniform(0, 1.f) < fraction_active) {
           part->time_bin = 1;
-        else
+          h_max_active = fmaxf(h_max_active, part->h);
+        } else {
           part->time_bin = num_time_bins + 1;
+        }
 
 #ifdef SWIFT_DEBUG_CHECKS
         part->ti_drift = 8;
@@ -149,16 +164,21 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
 
   /* Cell properties */
   cell->split = 0;
+  cell->depth = 0;
   cell->hydro.h_max = h_max;
+  cell->hydro.h_max_active = h_max_active;
   cell->hydro.count = count;
   cell->hydro.dx_max_part = 0.;
   cell->hydro.dx_max_sort = 0.;
   cell->width[0] = size;
   cell->width[1] = size;
   cell->width[2] = size;
+  cell->dmin = size;
   cell->loc[0] = offset[0];
   cell->loc[1] = offset[1];
   cell->loc[2] = offset[2];
+  cell->h_min_allowed = cell->dmin * 0.5 * (1. / kernel_gamma);
+  cell->h_max_allowed = cell->dmin * (1. / kernel_gamma);
 
   cell->hydro.super = cell;
   cell->hydro.ti_old_part = 8;
@@ -175,6 +195,8 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
 
 void clean_up(struct cell *ci) {
   cell_free_hydro_sorts(ci);
+  free(ci->hydro.parts);
+  free(ci->hydro.xparts);
   free(ci);
 }
 
@@ -192,6 +214,8 @@ void zero_particle_fields_density(
 #endif
 
     hydro_init_part(&c->hydro.parts[pid], NULL);
+    adaptive_softening_init_part(&c->hydro.parts[pid]);
+    mhd_init_part(&c->hydro.parts[pid]);
   }
 }
 
@@ -267,6 +291,44 @@ void zero_particle_fields_force(
     }
     p->geometry.volume = 1.0f;
 #endif
+#ifdef PLANETARY_SPH
+    p->rho = 1.f;
+    p->density.rho_dh = 0.f;
+    p->density.wcount = 48.f / (kernel_norm * pow_dimension(p->h));
+    p->density.wcount_dh = 0.f;
+    p->density.rot_v[0] = 0.f;
+    p->density.rot_v[1] = 0.f;
+    p->density.rot_v[2] = 0.f;
+    p->density.div_v = 0.f;
+#endif /* PLANETARY_SPH */
+#if defined(REMIX_SPH)
+    p->rho = 1.f;
+    p->m0 = 1.f;
+    p->density.rho_dh = 0.f;
+    p->density.wcount = 48.f / (kernel_norm * pow_dimension(p->h));
+    p->density.wcount_dh = 0.f;
+    p->gradient.m0_bar = 1.f;
+    p->gradient.grad_m0_bar_gradhterm = 0.f;
+    memset(p->grad_m0, 0.f, 3 * sizeof(float));
+    memset(p->dv_norm_kernel, 0.f, 3 * 3 * sizeof(float));
+    memset(p->du_norm_kernel, 0.f, 3 * sizeof(float));
+    memset(p->drho_norm_kernel, 0.f, 3 * sizeof(float));
+    memset(p->dh_norm_kernel, 0.f, 3 * sizeof(float));
+    memset(p->gradient.grad_m0_bar, 0.f, 3 * sizeof(float));
+    memset(p->gradient.m1_bar, 0.f, 3 * sizeof(float));
+    memset(p->gradient.grad_m1_bar, 0.f, 3 * 3 * sizeof(float));
+    memset(p->gradient.grad_m1_bar_gradhterm, 0.f, 3 * sizeof(float));
+    p->gradient.m2_bar.xx = 1.f;
+    p->gradient.m2_bar.yy = 1.f;
+    p->gradient.m2_bar.zz = 1.f;
+    p->gradient.m2_bar.xy = 0.f;
+    p->gradient.m2_bar.xz = 0.f;
+    p->gradient.m2_bar.yz = 0.f;
+    zero_sym_matrix(&p->gradient.grad_m2_bar[0]);
+    zero_sym_matrix(&p->gradient.grad_m2_bar[1]);
+    zero_sym_matrix(&p->gradient.grad_m2_bar[2]);
+    zero_sym_matrix(&p->gradient.grad_m2_bar_gradhterm);
+#endif /* REMIX_SPH */
 
     /* And prepare for a round of force tasks. */
     hydro_prepare_force(p, xp, cosmo, hydro_props, pressure_floor, 0., 0.);
@@ -277,9 +339,13 @@ void zero_particle_fields_force(
 /**
  * @brief Ends the density loop by adding the appropriate coefficients
  */
-void end_calculation_density(struct cell *c, const struct cosmology *cosmo) {
+void end_calculation_density(struct cell *c, const struct cosmology *cosmo,
+                             const struct gravity_props *gravity_props) {
+
   for (int pid = 0; pid < c->hydro.count; pid++) {
     hydro_end_density(&c->hydro.parts[pid], cosmo);
+    adaptive_softening_end_density(&c->hydro.parts[pid], gravity_props);
+    mhd_end_density(&c->hydro.parts[pid], cosmo);
 
 #if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
     /* undo the artificial correction that was applied to wcount */
@@ -296,7 +362,8 @@ void end_calculation_density(struct cell *c, const struct cosmology *cosmo) {
 /**
  * @brief Ends the force loop by adding the appropriate coefficients
  */
-void end_calculation_force(struct cell *c, const struct cosmology *cosmo) {
+void end_calculation_force(struct cell *c, const struct cosmology *cosmo,
+                           const struct gravity_props *gravity_props) {
   for (int pid = 0; pid < c->hydro.count; pid++) {
     struct part *volatile part = &c->hydro.parts[pid];
     hydro_end_force(part, cosmo);
@@ -332,14 +399,15 @@ void dump_particle_fields(char *fileName, struct cell *ci, struct cell *cj) {
 }
 
 /* Just a forward declaration... */
-void runner_dopair1_density(struct runner *r, struct cell *ci, struct cell *cj);
 void runner_dopair2_force_vec(struct runner *r, struct cell *ci,
                               struct cell *cj);
 void runner_doself1_density_vec(struct runner *r, struct cell *ci);
 void runner_dopair1_branch_density(struct runner *r, struct cell *ci,
-                                   struct cell *cj);
+                                   struct cell *cj, int limit_h_min,
+                                   int limit_h_max);
 void runner_dopair2_branch_force(struct runner *r, struct cell *ci,
-                                 struct cell *cj);
+                                 struct cell *cj, int limit_h_min,
+                                 int limit_h_max);
 
 /**
  * @brief Computes the pair interactions of two cells using SWIFT and a brute
@@ -348,25 +416,25 @@ void runner_dopair2_branch_force(struct runner *r, struct cell *ci,
 void test_pair_interactions(struct runner *runner, struct cell **ci,
                             struct cell **cj, char *swiftOutputFileName,
                             char *bruteForceOutputFileName,
-                            interaction_func serial_interaction,
+                            serial_interaction_func serial_interaction,
                             interaction_func vec_interaction, init_func init,
                             finalise_func finalise) {
 
   const struct engine *e = runner->e;
 
-  runner_do_hydro_sort(runner, *ci, 0x1FFF, 0, 0, 0);
-  runner_do_hydro_sort(runner, *cj, 0x1FFF, 0, 0, 0);
+  runner_do_hydro_sort(runner, *ci, 0x1FFF, 0, 0, 0, 0);
+  runner_do_hydro_sort(runner, *cj, 0x1FFF, 0, 0, 0, 0);
 
   /* Zero the fields */
   init(*ci, e->cosmology, e->hydro_properties, e->pressure_floor_props);
   init(*cj, e->cosmology, e->hydro_properties, e->pressure_floor_props);
 
   /* Run the test */
-  vec_interaction(runner, *ci, *cj);
+  vec_interaction(runner, *ci, *cj, 0, 0);
 
   /* Let's get physical ! */
-  finalise(*ci, e->cosmology);
-  finalise(*cj, e->cosmology);
+  finalise(*ci, e->cosmology, e->gravity_properties);
+  finalise(*cj, e->cosmology, e->gravity_properties);
 
   /* Dump if necessary */
   dump_particle_fields(swiftOutputFileName, *ci, *cj);
@@ -381,8 +449,8 @@ void test_pair_interactions(struct runner *runner, struct cell **ci,
   serial_interaction(runner, *ci, *cj);
 
   /* Let's get physical ! */
-  finalise(*ci, e->cosmology);
-  finalise(*cj, e->cosmology);
+  finalise(*ci, e->cosmology, e->gravity_properties);
+  finalise(*cj, e->cosmology, e->gravity_properties);
 
   dump_particle_fields(bruteForceOutputFileName, *ci, *cj);
 }
@@ -394,8 +462,8 @@ void test_all_pair_interactions(
     struct runner *runner, double *offset2, size_t particles, double size,
     double h, double rho, long long *partId, double perturbation, double h_pert,
     char *swiftOutputFileName, char *bruteForceOutputFileName,
-    interaction_func serial_interaction, interaction_func vec_interaction,
-    init_func init, finalise_func finalise) {
+    serial_interaction_func serial_interaction,
+    interaction_func vec_interaction, init_func init, finalise_func finalise) {
 
   double offset1[3] = {0, 0, 0};
   struct cell *ci, *cj;
@@ -546,8 +614,10 @@ int main(int argc, char *argv[]) {
   struct space space;
   struct engine engine;
   struct cosmology cosmo;
+  struct gravity_props gravity_props;
   struct hydro_props hydro_props;
   struct pressure_floor_props pressure_floor;
+  struct sink_props sink_props;
   struct phys_const prog_const;
   struct runner *runner;
   static long long partId = 0;
@@ -633,12 +703,16 @@ int main(int argc, char *argv[]) {
 
   prog_const.const_vacuum_permeability = 1.0;
   engine.physical_constants = &prog_const;
-
   cosmology_init_no_cosmo(&cosmo);
   engine.cosmology = &cosmo;
   hydro_props_init_no_hydro(&hydro_props);
   engine.hydro_properties = &hydro_props;
   engine.pressure_floor_props = &pressure_floor;
+  bzero(&gravity_props, sizeof(struct gravity_props));
+  gravity_props.G_Newton = 1.;
+  engine.gravity_properties = &gravity_props;
+  bzero(&sink_props, sizeof(struct sink_props));
+  engine.sink_properties = &sink_props;
 
   if (posix_memalign((void **)&runner, SWIFT_STRUCT_ALIGNMENT,
                      sizeof(struct runner)) != 0) {
@@ -667,7 +741,7 @@ int main(int argc, char *argv[]) {
   double offset[3] = {1., 0., 0.};
 
   /* Define which interactions to call */
-  interaction_func serial_inter_func = &pairs_all_density;
+  serial_interaction_func serial_inter_func = &pairs_all_density;
   interaction_func vec_inter_func = &runner_dopair1_branch_density;
   init_func init = &zero_particle_fields_density;
   finalise_func finalise = &end_calculation_density;
@@ -738,5 +812,10 @@ int main(int argc, char *argv[]) {
                              perturbation, h_pert, swiftOutputFileName,
                              bruteForceOutputFileName, serial_inter_func,
                              vec_inter_func, init, finalise);
+#ifdef WITH_VECTORIZATION
+  cache_clean(&runner->ci_cache);
+  cache_clean(&runner->cj_cache);
+#endif
+  free(runner);
   return 0;
 }
diff --git a/tests/testCbrt.c b/tests/testCbrt.c
index 431753f2b6384d9b45aa7b501374ba24d8a245a2..0a03fff3c8ff34055fbc2750f2bf28fbf6591955 100644
--- a/tests/testCbrt.c
+++ b/tests/testCbrt.c
@@ -54,7 +54,7 @@ int main(int argc, char *argv[]) {
   if (posix_memalign((void **)&data, 64, num_vals * sizeof(float)) != 0)
     error("Failed to allocted memory for the test");
   for (int k = 0; k < num_vals; k++) {
-    data[k] = (float)rand() / RAND_MAX;
+    data[k] = (float)rand() / (float)RAND_MAX;
     data[k] = (1.0f - data[k]) * range_min + data[k] * range_max;
     if (data[k] == 0.f) k--; /* Skip 0 to avoid spurious mistakes */
   }
diff --git a/tests/testEOS.c b/tests/testEOS.c
index db6fa4fbef8252245c0c38677dfe47a2ea130cca..514a8dc73ecccd4732b7e340440174f8d49e5a7a 100644
--- a/tests/testEOS.c
+++ b/tests/testEOS.c
@@ -48,300 +48,15 @@
 #endif
 
 /**
- * @brief Write a list of densities, energies, and resulting pressures to file
- *  from an equation of state.
+ * @brief Test planetary equations of state.
  *
  *                      WORK IN PROGRESS
  *
- * So far only does the Hubbard & MacFarlane (1980) equations of state.
- *
- * Usage:
- *      $  ./testEOS  (mat_id)  (do_output)
- *
- * Sys args (optional):
- *      mat_id | int | Material ID, see equation_of_state.h for the options.
- *          Default: 201 (= id_HM80_ice).
- *
- *      do_output | int | Set 1 to write the output file of rho, u, P values,
- *          set 0 for no output. Default: 0.
- *
- * Output text file contains:
- *  header
- *  num_rho num_u   mat_id                      # Header values
- *  rho_0   rho_1   rho_2   ...   rho_num_rho   # Array of densities, rho
- *  u_0     u_1     u_2     ...   u_num_u       # Array of energies, u
- *  P_0_0   P_0_1   ...     P_0_num_u           # Array of pressures, P(rho, u)
- *  P_1_0   ...     ...     P_1_num_u
- *  ...     ...     ...     ...
- *  P_num_rho_0     ...     P_num_rho_num_u
- *  c_0_0   c_0_1   ...     c_0_num_u           # Array of sound speeds, c(rho,
- * u)
- *  c_1_0   ...     ...     c_1_num_u
- *  ...     ...     ...     ...
- *  c_num_rho_0     ...     c_num_rho_num_u
- *
- * Note that the values tested extend beyond the range that most EOS are
- * designed for (e.g. outside table limits), to help test the EOS in case of
- * unexpected particle behaviour.
- *
+ *  *
  */
 
 #ifdef EOS_PLANETARY
-int main(int argc, char *argv[]) {
-  float rho, u, log_rho, log_u, P, c;
-  struct unit_system us;
-  struct swift_params *params =
-      (struct swift_params *)malloc(sizeof(struct swift_params));
-  if (params == NULL) error("Error allocating memory for the parameter file.");
-  const struct phys_const *phys_const = 0;  // Unused placeholder
-  const float J_kg_to_erg_g = 1e4;          // Convert J/kg to erg/g
-  char filename[64];
-  // Output table params
-  const int num_rho = 100, num_u = 100;
-  float log_rho_min = logf(1e-4f), log_rho_max = logf(1e3f),  // Densities (cgs)
-      log_u_min = logf(1e4f),
-        log_u_max = logf(1e13f),  // Sp. int. energies (SI)
-      log_rho_step = (log_rho_max - log_rho_min) / (num_rho - 1.f),
-        log_u_step = (log_u_max - log_u_min) / (num_u - 1.f);
-  float A1_rho[num_rho], A1_u[num_u];
-  // Sys args
-  int mat_id_in, do_output;
-  // Default sys args
-  const int mat_id_def = eos_planetary_id_HM80_ice;
-  const int do_output_def = 0;
-
-  // Check the number of system arguments and use defaults if not provided
-  switch (argc) {
-    case 1:
-      // Default both
-      mat_id_in = mat_id_def;
-      do_output = do_output_def;
-      break;
-
-    case 2:
-      // Read mat_id, default do_output
-      mat_id_in = atoi(argv[1]);
-      do_output = do_output_def;
-      break;
-
-    case 3:
-      // Read both
-      mat_id_in = atoi(argv[1]);
-      do_output = atoi(argv[2]);
-      break;
-
-    default:
-      error("Invalid number of system arguments!\n");
-      mat_id_in = mat_id_def;  // Ignored, just here to keep the compiler happy
-      do_output = do_output_def;
-  };
-
-  enum eos_planetary_material_id mat_id =
-      (enum eos_planetary_material_id)mat_id_in;
-
-  /* Greeting message */
-  printf("This is %s\n", package_description());
-
-  // Check material ID
-  const enum eos_planetary_type_id type =
-      (enum eos_planetary_type_id)(mat_id / eos_planetary_type_factor);
-
-  // Select the material base type
-  switch (type) {
-    // Tillotson
-    case eos_planetary_type_Til:
-      switch (mat_id) {
-        case eos_planetary_id_Til_iron:
-          printf("  Tillotson iron \n");
-          break;
-
-        case eos_planetary_id_Til_granite:
-          printf("  Tillotson granite \n");
-          break;
-
-        case eos_planetary_id_Til_water:
-          printf("  Tillotson water \n");
-          break;
-
-        default:
-          error("Unknown material ID! mat_id = %d \n", mat_id);
-      };
-      break;
-
-    // Hubbard & MacFarlane (1980)
-    case eos_planetary_type_HM80:
-      switch (mat_id) {
-        case eos_planetary_id_HM80_HHe:
-          printf("  Hubbard & MacFarlane (1980) hydrogen-helium atmosphere \n");
-          break;
-
-        case eos_planetary_id_HM80_ice:
-          printf("  Hubbard & MacFarlane (1980) ice mix \n");
-          break;
-
-        case eos_planetary_id_HM80_rock:
-          printf("  Hubbard & MacFarlane (1980) rock mix \n");
-          break;
-
-        default:
-          error("Unknown material ID! mat_id = %d \n", mat_id);
-      };
-      break;
-
-    // SESAME
-    case eos_planetary_type_SESAME:
-      switch (mat_id) {
-        case eos_planetary_id_SESAME_iron:
-          printf("  SESAME basalt 7530 \n");
-          break;
-
-        case eos_planetary_id_SESAME_basalt:
-          printf("  SESAME basalt 7530 \n");
-          break;
-
-        case eos_planetary_id_SESAME_water:
-          printf("  SESAME water 7154 \n");
-          break;
-
-        case eos_planetary_id_SS08_water:
-          printf("  Senft & Stewart (2008) SESAME-like water \n");
-          break;
-
-        default:
-          error("Unknown material ID! mat_id = %d \n", mat_id);
-      };
-      break;
-
-    default:
-      error("Unknown material type! mat_id = %d \n", mat_id);
-  }
-
-  // Convert to internal units
-  // Earth masses and radii
-  //  units_init(&us, 5.9724e27, 6.3710e8, 1.f, 1.f, 1.f);
-  // SI
-  units_init(&us, 1000.f, 100.f, 1.f, 1.f, 1.f);
-  log_rho_min -= logf(units_cgs_conversion_factor(&us, UNIT_CONV_DENSITY));
-  log_rho_max -= logf(units_cgs_conversion_factor(&us, UNIT_CONV_DENSITY));
-  log_u_min += logf(J_kg_to_erg_g / units_cgs_conversion_factor(
-                                        &us, UNIT_CONV_ENERGY_PER_UNIT_MASS));
-  log_u_max += logf(J_kg_to_erg_g / units_cgs_conversion_factor(
-                                        &us, UNIT_CONV_ENERGY_PER_UNIT_MASS));
-
-  // Set the input parameters
-  // Which EOS to initialise
-  parser_set_param(params, "EoS:planetary_use_Til:1");
-  parser_set_param(params, "EoS:planetary_use_HM80:1");
-  parser_set_param(params, "EoS:planetary_use_SESAME:1");
-  // Table file names
-  parser_set_param(params,
-                   "EoS:planetary_HM80_HHe_table_file:"
-                   "../examples/planetary_HM80_HHe.txt");
-  parser_set_param(params,
-                   "EoS:planetary_HM80_ice_table_file:"
-                   "../examples/planetary_HM80_ice.txt");
-  parser_set_param(params,
-                   "EoS:planetary_HM80_rock_table_file:"
-                   "../examples/planetary_HM80_rock.txt");
-  parser_set_param(params,
-                   "EoS:planetary_SESAME_iron_table_file:"
-                   "../examples/planetary_SESAME_iron_2140.txt");
-  parser_set_param(params,
-                   "EoS:planetary_SESAME_basalt_table_file:"
-                   "../examples/planetary_SESAME_basalt_7530.txt");
-  parser_set_param(params,
-                   "EoS:planetary_SESAME_water_table_file:"
-                   "../examples/planetary_SESAME_water_7154.txt");
-  parser_set_param(params,
-                   "EoS:planetary_SS08_water_table_file:"
-                   "../examples/planetary_SS08_water.txt");
-
-  // Initialise the EOS materials
-  eos_init(&eos, phys_const, &us, params);
-
-  // Manual debug testing
-  if (1) {
-    printf("\n ### MANUAL DEBUG TESTING ### \n");
-
-    rho = 5960;
-    u = 1.7e8;
-    P = gas_pressure_from_internal_energy(rho, u, eos_planetary_id_HM80_ice);
-    printf("u = %.2e,    rho = %.2e,    P = %.2e \n", u, rho, P);
-
-    return 0;
-  }
-
-  // Output file
-  sprintf(filename, "testEOS_rho_u_P_c_%d.txt", mat_id);
-  FILE *f = fopen(filename, "w");
-  if (f == NULL) {
-    printf("Could not open output file!\n");
-    exit(EXIT_FAILURE);
-  }
-
-  if (do_output == 1) {
-    fprintf(f, "Density  Sp.Int.Energy  mat_id \n");
-    fprintf(f, "%d      %d            %d \n", num_rho, num_u, mat_id);
-  }
-
-  // Densities
-  log_rho = log_rho_min;
-  for (int i = 0; i < num_rho; i++) {
-    A1_rho[i] = exp(log_rho);
-    log_rho += log_rho_step;
-
-    if (do_output == 1)
-      fprintf(f, "%.6e ",
-              A1_rho[i] * units_cgs_conversion_factor(&us, UNIT_CONV_DENSITY));
-  }
-  if (do_output == 1) fprintf(f, "\n");
-
-  // Sp. int. energies
-  log_u = log_u_min;
-  for (int i = 0; i < num_u; i++) {
-    A1_u[i] = exp(log_u);
-    log_u += log_u_step;
-
-    if (do_output == 1)
-      fprintf(f, "%.6e ",
-              A1_u[i] * units_cgs_conversion_factor(
-                            &us, UNIT_CONV_ENERGY_PER_UNIT_MASS));
-  }
-  if (do_output == 1) fprintf(f, "\n");
-
-  // Pressures
-  for (int i = 0; i < num_rho; i++) {
-    rho = A1_rho[i];
-
-    for (int j = 0; j < num_u; j++) {
-      P = gas_pressure_from_internal_energy(rho, A1_u[j], mat_id);
-
-      if (do_output == 1)
-        fprintf(f, "%.6e ",
-                P * units_cgs_conversion_factor(&us, UNIT_CONV_PRESSURE));
-    }
-
-    if (do_output == 1) fprintf(f, "\n");
-  }
-
-  // Sound speeds
-  for (int i = 0; i < num_rho; i++) {
-    rho = A1_rho[i];
-
-    for (int j = 0; j < num_u; j++) {
-      c = gas_soundspeed_from_internal_energy(rho, A1_u[j], mat_id);
-
-      if (do_output == 1)
-        fprintf(f, "%.6e ",
-                c * units_cgs_conversion_factor(&us, UNIT_CONV_SPEED));
-    }
-
-    if (do_output == 1) fprintf(f, "\n");
-  }
-  fclose(f);
-
-  return 0;
-}
+int main(int argc, char *argv[]) { return 0; }
 #else
 int main(int argc, char *argv[]) { return 0; }
 #endif
diff --git a/tests/testEOS.sh b/tests/testEOS.sh
deleted file mode 100755
index bcd87eabbf15a962808843dda76d1829f2917c97..0000000000000000000000000000000000000000
--- a/tests/testEOS.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/bash
-
-echo ""
-
-rm -f testEOS_rho_u_P_*.txt
-
-echo "Running testEOS for each planetary material"
-
-A1_mat_id=(
-    100
-    101
-    102
-    200
-    201
-    202
-    300
-    301
-    302
-    303
-)
-
-for mat_id in "${A1_mat_id[@]}"
-do
-    ./testEOS "$mat_id" 1
-done
-
-exit $?
diff --git a/tests/testInteractions.c b/tests/testInteractions.c
index a4d7ead66ad0ef94accb722d61a48795617182ea..960c2f92543ee278415842d9682e108da5051d70 100644
--- a/tests/testInteractions.c
+++ b/tests/testInteractions.c
@@ -78,7 +78,7 @@ struct part *make_particles(size_t count, double *offset, double spacing,
   p->h = h;
   p->id = ++(*partId);
 
-#if !defined(GIZMO_MFV_SPH) && !defined(SHADOWFAX_SPH)
+#if !defined(GIZMO_MFV_SPH)
   p->mass = 1.0f;
 #endif
 
@@ -99,7 +99,7 @@ struct part *make_particles(size_t count, double *offset, double spacing,
 
     p->h = h;
     p->id = ++(*partId);
-#if !defined(GIZMO_SPH) && !defined(SHADOWFAX_SPH)
+#if !defined(GIZMO_SPH)
     p->mass = 1.0f;
 #endif
   }
@@ -111,11 +111,11 @@ struct part *make_particles(size_t count, double *offset, double spacing,
  */
 void prepare_force(struct part *parts, size_t count) {
 
-#if !defined(GIZMO_MFV_SPH) && !defined(SHADOWFAX_SPH) &&            \
-    !defined(MINIMAL_SPH) && !defined(PLANETARY_SPH) &&              \
-    !defined(HOPKINS_PU_SPH) && !defined(HOPKINS_PU_SPH_MONAGHAN) && \
-    !defined(ANARCHY_PU_SPH) && !defined(SPHENIX_SPH) &&             \
-    !defined(PHANTOM_SPH) && !defined(GASOLINE_SPH)
+#if !defined(GIZMO_MFV_SPH) && !defined(MINIMAL_SPH) &&              \
+    !defined(PLANETARY_SPH) && !defined(HOPKINS_PU_SPH) &&           \
+    !defined(HOPKINS_PU_SPH_MONAGHAN) && !defined(ANARCHY_PU_SPH) && \
+    !defined(SPHENIX_SPH) && !defined(PHANTOM_SPH) &&                \
+    !defined(GASOLINE_SPH) && !defined(REMIX_SPH)
   struct part *p;
   for (size_t i = 0; i < count; ++i) {
     p = &parts[i];
@@ -142,8 +142,8 @@ void dump_indv_particle_fields(char *fileName, struct part *p) {
           "%8.5f %8.5f %13e %13e %13e %13e %13e %8.5f %8.5f\n",
           p->id, p->x[0], p->x[1], p->x[2], p->v[0], p->v[1], p->v[2], p->h,
           hydro_get_comoving_density(p),
-#if defined(MINIMAL_SPH) || defined(PLANETARY_SPH) || \
-    defined(SHADOWFAX_SPH) || defined(PHANTOM_SPH) || defined(GASOLINE_SPH)
+#if defined(MINIMAL_SPH) || defined(PLANETARY_SPH) || defined(PHANTOM_SPH) || \
+    defined(GASOLINE_SPH)
           0.f,
 #else
           p->density.div_v,
diff --git a/tests/testNeutrinoFermiDirac.c b/tests/testNeutrinoFermiDirac.c
index 5cf1ad6070e87e7625e621e57d698f6b56eaf14e..5bcc42939116daf7c317a83baf7a97d603823c24 100644
--- a/tests/testNeutrinoFermiDirac.c
+++ b/tests/testNeutrinoFermiDirac.c
@@ -70,7 +70,7 @@ int main(int argc, char *argv[]) {
 
   /* We also construct a histogram */
   int bins = 1000;
-  int *histogram1 = (int *)calloc(bins, sizeof(int));
+  int *histogram1 = (int *)calloc(bins + 1, sizeof(int));
 
   /* Generate the same numbers again and compute statistics and histogram */
   for (int i = 0; i < N; i++) {
diff --git a/tests/testOutputList.c b/tests/testOutputList.c
index 3a7300e7bc35c64730e7a406df8fe1938d01e40f..d4bfa81a5793d107520d031331187eceb96b0d37 100644
--- a/tests/testOutputList.c
+++ b/tests/testOutputList.c
@@ -24,6 +24,7 @@
 
 #define Ntest 3
 #define tol 1e-12
+#define itol 1000
 #define filename "output_list_params.yml"
 
 /* Expected values from file */
@@ -68,10 +69,10 @@ void test_no_cosmo(struct engine *e, const char *name, const int with_assert) {
 
     /* Set current time */
     e->ti_current = (output_time - e->time_begin) / e->time_base;
-    e->ti_current += 1;
+    e->ti_current += itol;
 
     /* Read next value */
-    integertime_t ti_next;
+    integertime_t ti_next = 0;
     output_list_read_next_time(list, e, name, &ti_next);
 
     output_time = (double)(ti_next * e->time_base) + e->time_begin;
@@ -109,7 +110,7 @@ void test_cosmo(struct engine *e, const char *name, const int with_assert) {
     e->ti_current += 16;
 
     /* Read next value */
-    integertime_t ti_next;
+    integertime_t ti_next = 0;
     output_list_read_next_time(list, e, name, &ti_next);
 
     output_time = (double)exp(ti_next * e->time_base) * e->cosmology->a_begin;
@@ -144,10 +145,12 @@ int main(int argc, char *argv[]) {
 
   /* Pseudo initialization of engine */
   struct engine e;
+  bzero(&e, sizeof(struct engine));
   e.cosmology = &cosmo;
   e.parameter_file = &params;
   e.physical_constants = &phys_const;
   e.internal_units = &us;
+  e.verbose = 1;
 
   int with_assert = 1;
   int without_assert = 0;
diff --git a/tests/testPeriodicBC.c b/tests/testPeriodicBC.c
index 13c123f01f07f757a621b6fe1ac262313a2847bd..be31b66051e6f5a17902a26b3c069f621731f0f7 100644
--- a/tests/testPeriodicBC.c
+++ b/tests/testPeriodicBC.c
@@ -131,19 +131,17 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
         part->h = size * h / (float)n;
         h_max = fmax(h_max, part->h);
         part->id = ++(*partId);
+        part->depth_h = 0;
 
-#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(SHADOWFAX_SPH)
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
         part->conserved.mass = density * volume / count;
 
-#ifdef SHADOWFAX_SPH
-        double anchor[3] = {0., 0., 0.};
-        double side[3] = {1., 1., 1.};
-        voronoi_cell_init(&part->cell, part->x, anchor, side);
-#endif
-
 #else
         part->mass = density * volume / count;
 #endif
+#if defined(REMIX_SPH)
+        part->rho_evol = density;
+#endif
 
 #if defined(HOPKINS_PE_SPH)
         part->entropy = 1.f;
@@ -164,16 +162,21 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
 
   /* Cell properties */
   cell->split = 0;
+  cell->depth = 0;
   cell->hydro.h_max = h_max;
+  cell->hydro.h_max_active = h_max;
   cell->hydro.count = count;
   cell->hydro.dx_max_part = 0.;
   cell->hydro.dx_max_sort = 0.;
   cell->width[0] = size;
   cell->width[1] = size;
   cell->width[2] = size;
+  cell->dmin = size;
   cell->loc[0] = offset[0];
   cell->loc[1] = offset[1];
   cell->loc[2] = offset[2];
+  cell->h_min_allowed = cell->dmin * 0.5 * (1. / kernel_gamma);
+  cell->h_max_allowed = cell->dmin * (1. / kernel_gamma);
 
   cell->hydro.super = cell;
   cell->hydro.ti_old_part = 8;
@@ -200,15 +203,21 @@ void clean_up(struct cell *ci) {
 void zero_particle_fields(struct cell *c) {
   for (int pid = 0; pid < c->hydro.count; pid++) {
     hydro_init_part(&c->hydro.parts[pid], NULL);
+    adaptive_softening_init_part(&c->hydro.parts[pid]);
+    mhd_init_part(&c->hydro.parts[pid]);
   }
 }
 
 /**
  * @brief Ends the loop by adding the appropriate coefficients
  */
-void end_calculation(struct cell *c, const struct cosmology *cosmo) {
+void end_calculation(struct cell *c, const struct cosmology *cosmo,
+                     const struct gravity_props *gravity_props) {
+
   for (int pid = 0; pid < c->hydro.count; pid++) {
     hydro_end_density(&c->hydro.parts[pid], cosmo);
+    adaptive_softening_end_density(&c->hydro.parts[pid], gravity_props);
+    mhd_end_density(&c->hydro.parts[pid], cosmo);
   }
 }
 
@@ -239,7 +248,7 @@ void dump_particle_fields(char *fileName, struct cell *main_cell, int i, int j,
             main_cell->hydro.parts[pid].v[0], main_cell->hydro.parts[pid].v[1],
             main_cell->hydro.parts[pid].v[2],
             hydro_get_comoving_density(&main_cell->hydro.parts[pid]),
-#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH) || defined(SHADOWFAX_SPH)
+#if defined(GIZMO_MFV_SPH) || defined(GIZMO_MFM_SPH)
             0.f,
 #else
             main_cell->hydro.parts[pid].density.rho_dh,
@@ -286,11 +295,12 @@ int check_results(struct part *serial_parts, struct part *vec_parts, int count,
 }
 
 /* Just a forward declaration... */
-void runner_doself1_density(struct runner *r, struct cell *ci);
 void runner_doself1_density_vec(struct runner *r, struct cell *ci);
 void runner_dopair1_branch_density(struct runner *r, struct cell *ci,
-                                   struct cell *cj);
-void runner_doself1_branch_density(struct runner *r, struct cell *c);
+                                   struct cell *cj, int limit_h_min,
+                                   int limit_h_max);
+void runner_doself1_branch_density(struct runner *r, struct cell *c,
+                                   int limit_h_min, int limit_h_max);
 
 void test_boundary_conditions(struct cell **cells, struct runner *runner,
                               const int loc_i, const int loc_j, const int loc_k,
@@ -326,17 +336,21 @@ void test_boundary_conditions(struct cell **cells, struct runner *runner,
         /* Get the neighbouring cell */
         struct cell *cj = cells[iii * (dim * dim) + jjj * dim + kkk];
 
-        if (cj != main_cell) DOPAIR1(runner, main_cell, cj);
+        if (cj != main_cell)
+          DOPAIR1(runner, main_cell, cj, /*limit_h_min=*/0,
+                  /*limit_h_max=*/0);
       }
     }
   }
 
   /* And now the self-interaction */
 
-  DOSELF1(runner, main_cell);
+  DOSELF1(runner, main_cell, /*limit_h_min=*/0,
+          /*limit_h_max=*/0);
 
   /* Let's get physical ! */
-  end_calculation(main_cell, runner->e->cosmology);
+  end_calculation(main_cell, runner->e->cosmology,
+                  runner->e->gravity_properties);
 
   /* Dump particles from the main cell. */
   dump_particle_fields(swiftOutputFileName, main_cell, loc_i, loc_j, loc_k);
@@ -370,7 +384,8 @@ void test_boundary_conditions(struct cell **cells, struct runner *runner,
   self_all_density(runner, main_cell);
 
   /* Let's get physical ! */
-  end_calculation(main_cell, runner->e->cosmology);
+  end_calculation(main_cell, runner->e->cosmology,
+                  runner->e->gravity_properties);
 
   /* Dump */
   dump_particle_fields(bruteForceOutputFileName, main_cell, loc_i, loc_j,
@@ -512,6 +527,15 @@ int main(int argc, char *argv[]) {
   struct pressure_floor_props pressure_floor;
   engine.pressure_floor_props = &pressure_floor;
 
+  struct sink_props sink_props;
+  bzero(&sink_props, sizeof(struct sink_props));
+  engine.sink_properties = &sink_props;
+
+  struct gravity_props gravity_props;
+  bzero(&gravity_props, sizeof(struct gravity_props));
+  gravity_props.G_Newton = 1.f;
+  engine.gravity_properties = &gravity_props;
+
   /* Construct some cells */
   struct cell *cells[dim * dim * dim];
   static long long partId = 0;
@@ -525,7 +549,7 @@ int main(int argc, char *argv[]) {
         runner_do_drift_part(runner, cells[i * (dim * dim) + j * dim + k], 0);
 
         runner_do_hydro_sort(runner, cells[i * (dim * dim) + j * dim + k],
-                             0x1FFF, 0, 0, 0);
+                             0x1FFF, 0, 0, 0, 0);
       }
     }
   }
diff --git a/tests/testRandomCone.c b/tests/testRandomCone.c
index 2393362bad991a2fa93aecda15a9d326b9ed82de..a7e4568de213770cbd6ce4a7afd4c41cb6244100 100644
--- a/tests/testRandomCone.c
+++ b/tests/testRandomCone.c
@@ -18,6 +18,9 @@
  ******************************************************************************/
 #include <config.h>
 
+/* System includes. */
+#include <fenv.h>
+
 /* Local headers. */
 #include "swift.h"
 
@@ -47,10 +50,9 @@ const int N_cube = 5;
  * the uniformity of the distribution.
  * @param tolerance The tolerance of each bin relative to the expected value.
  */
-void test_cone(int64_t id_bh, const integertime_t ti_current,
-               const enum random_number_type type, double opening_angle,
-               float unit_vector[3], const int N_test, const int N_bins,
-               const double tolerance) {
+float test_cone(int64_t id_bh, const integertime_t ti_current,
+                const enum random_number_type type, double opening_angle,
+                float unit_vector[3]) {
 
   /* Compute cosine that corresponds to the maximum opening angle */
   const double cos_theta_max = cos(opening_angle);
@@ -58,59 +60,41 @@ void test_cone(int64_t id_bh, const integertime_t ti_current,
   /* Initialize an array that will hold a random vector every step */
   float rand_vector[3];
 
-  /* Initialize an array that will hold the binned number of drawn cosines,
-     i.e. this is the probability density function that we wish to test. */
-  double binned_cosines[N_bins];
-  for (int j = 0; j < N_bins; ++j) {
-    binned_cosines[j] = 0.;
+  /* Generate a random unit vector within a cone around unit_vector  */
+  random_direction_in_cone(id_bh, ti_current, type, opening_angle, unit_vector,
+                           rand_vector);
+
+  /* Check that this vector is actually within the cone we want  */
+  const double cos_rand_unit = rand_vector[0] * unit_vector[0] +
+                               rand_vector[1] * unit_vector[1] +
+                               rand_vector[2] * unit_vector[2];
+  if (cos_rand_unit < 0.99999 * cos_theta_max) {
+    printf("Cos_opening_angle is: %f, Random cos is: %f\n", cos_theta_max,
+           cos_rand_unit);
+    error("Generated random unit vector is outside cone.");
   }
 
-  for (int k = 0; k < N_test; ++k) {
+  return cos_rand_unit;
+}
 
-    /* Generate random ids. */
-    const long long id_p = rand() * (1LL << 31) + rand();
-
-    /* Generate a random unit vector within a cone around unit_vector  */
-    random_direction_in_cone(id_p, id_bh, ti_current, type, opening_angle,
-                             unit_vector, rand_vector);
-
-    /* Check that this vector is actually within the cone we want  */
-    const double cos_rand_unit = rand_vector[0] * unit_vector[0] +
-                                 rand_vector[1] * unit_vector[1] +
-                                 rand_vector[2] * unit_vector[2];
-    if (cos_rand_unit < 0.99999 * cos_theta_max) {
-      printf("Cos_opening_angle is: %f, Random cos is: %f\n", cos_theta_max,
-             cos_rand_unit);
-      error("Generated random unit vector is outside cone.");
-    }
+int main(int argc, char *argv[]) {
 
-    /* Add the unit vector to the probability density function array. The solid
-     * angle subtended by some angle theta grows as (1-cos(theta)). Furthermore,
-     * we are limited to the spherical cap defined by the angles [0, theta_max].
-     * Therefore the variable which we expect to be uniformly distributed is (1
-     * - cos(theta)) / (1 - cos(theta_max)). */
-    double uniform_variable = (1. - cos_rand_unit) / (1 - cos_theta_max);
-    for (int j = 0; j < N_bins; ++j) {
-      if ((uniform_variable > (double)j / (double)N_bins) &&
-          (uniform_variable < (double)(j + 1) / (double)N_bins)) {
-        binned_cosines[j] = binned_cosines[j] + 1. / (double)N_test;
-      }
-    }
-  }
+  /* Initialize CPU frequency, this also starts time. */
+  unsigned long long cpufreq = 0;
+  clocks_set_cpufreq(cpufreq);
 
-  /* Check whether the binned quantity is really uniformly distributed. If it
-   * is, the density (value) of each bin should be 1/N_bin. */
-  for (int j = 0; j < N_bins; ++j) {
-    if ((binned_cosines[j] < (1. - tolerance) / (double)N_bins) ||
-        (binned_cosines[j] > (1. + tolerance) / (double)N_bins)) {
-      error(
-          "Generated distribution of random unit vectors within a cone exceeds "
-          "the limit imposed by the tolerance.");
-    }
-  }
-}
+/* Choke on FPEs */
+#ifdef HAVE_FE_ENABLE_EXCEPT
+  feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW);
+#endif
 
-int main(int argc, char *argv[]) {
+  /* Get some randomness going */
+  const int seed = time(NULL);
+  message("Seed = %d", seed);
+  srand(seed);
+
+  /* Log the swift random seed */
+  message("SWIFT random seed = %d", SWIFT_RANDOM_SEED_XOR);
 
   /* Test the random-vector-in-cone function, for different values of opening
    * angle from 0 to pi/2 (in radians). For each of these opening angles we draw
@@ -135,15 +119,16 @@ int main(int argc, char *argv[]) {
       const double cos_unit =
           random_unit_interval(id_bh, ti_current, random_number_BH_kick);
       const double sin_unit = sqrtf(max(0., (1. - cos_unit) * (1. + cos_unit)));
-      const double phi_unit = random_unit_interval(id_bh * id_bh, ti_current,
-                                                   random_number_BH_kick);
+      const double phi_unit =
+          (2. * M_PI) * random_unit_interval(id_bh * id_bh, ti_current,
+                                             random_number_BH_kick);
       unit_vector[0] = sin_unit * cos(phi_unit);
       unit_vector[1] = sin_unit * sin(phi_unit);
       unit_vector[2] = cos_unit;
 
       /* Do the test. */
       test_cone(id_bh, ti_current, random_number_BH_kick, opening_angle,
-                unit_vector, 100000, 10, 0.1);
+                unit_vector);
     }
   }
 
@@ -151,26 +136,72 @@ int main(int argc, char *argv[]) {
    * bins, but for just one opening angle and one randomly generated cone */
   const double opening_angle_0 = 0.2;
 
-  /* Generate an id for the bh and a time. We do this for every opening
-   * angle and every cone. */
-  const long long id_bh_0 = rand() * (1LL << 31) + rand();
-  const integertime_t ti_current_0 = rand() * (1LL << 31) + rand();
+  /* Compute cosine that corresponds to the maximum opening angle */
+  const double cos_theta_max = cos(opening_angle_0);
 
   /* Generate a random unit vector that defines a cone, along with the
    * opening angle. */
+  const long long id_bh_0 = rand() * (1LL << 31) + rand();
+  const integertime_t ti_current_0 = rand() * (1LL << 31) + rand();
+
   float unit_vector_0[3];
   const double cos_unit =
       random_unit_interval(id_bh_0, ti_current_0, random_number_BH_kick);
   const double sin_unit = sqrtf(max(0., (1. - cos_unit) * (1. + cos_unit)));
-  const double phi_unit = random_unit_interval(id_bh_0 * id_bh_0, ti_current_0,
-                                               random_number_BH_kick);
+  const double phi_unit =
+      (2. * M_PI) * random_unit_interval(id_bh_0 * id_bh_0, ti_current_0,
+                                         random_number_BH_kick);
   unit_vector_0[0] = sin_unit * cos(phi_unit);
   unit_vector_0[1] = sin_unit * sin(phi_unit);
   unit_vector_0[2] = cos_unit;
 
-  /* Do the test. */
-  test_cone(id_bh_0, ti_current_0, random_number_BH_kick, opening_angle_0,
-            unit_vector_0, 100000000, 100, 0.01);
+  /* Some parameters to test the uniformity of drawn vectors */
+  int N_test = 10000000;
+  int N_bins = 100;
+  float tolerance = 0.05;
+
+  /* Initialize an array that will hold the binned number of drawn cosines,
+     i.e. this is the probability density function that we wish to test. */
+  double binned_cosines[N_bins];
+  for (int j = 0; j < N_bins; ++j) {
+    binned_cosines[j] = 0.;
+  }
+
+  /* Draw N_test vectors and bin them to test uniformity */
+  for (int k = 0; k < N_test; ++k) {
+
+    const long long id_bh = rand() * (1LL << 31) + rand();
+    const integertime_t ti_current = rand() * (1LL << 31) + rand();
+
+    /* Do the test, with a newly generated BH id and time */
+    const float cos_rand_unit =
+        test_cone(id_bh, ti_current, random_number_BH_kick, opening_angle_0,
+                  unit_vector_0);
+
+    /* Add the unit vector to the probability density function array. The solid
+     * angle subtended by some angle theta grows as (1-cos(theta)). Furthermore,
+     * we are limited to the spherical cap defined by the angles [0, theta_max].
+     * Therefore the variable which we expect to be uniformly distributed is (1
+     * - cos(theta)) / (1 - cos(theta_max)). */
+    double uniform_variable = (1. - cos_rand_unit) / (1 - cos_theta_max);
+    for (int j = 0; j < N_bins; ++j) {
+      if ((uniform_variable > (double)j / (double)N_bins) &&
+          (uniform_variable < (double)(j + 1) / (double)N_bins)) {
+        binned_cosines[j] = binned_cosines[j] + 1. / (double)N_test;
+      }
+    }
+  }
+
+  /* Check whether the binned quantity is really uniformly distributed. If it
+   * is, the density (value) of each bin should be 1/N_bin. */
+  for (int j = 0; j < N_bins; ++j) {
+    if ((binned_cosines[j] < (1. - tolerance) / (double)N_bins) ||
+        (binned_cosines[j] > (1. + tolerance) / (double)N_bins)) {
+      error(
+          "Generated distribution of random unit vectors within a cone exceeds "
+          "the limit imposed by the tolerance.");
+    }
+  }
 
   /* We now repeat the same process, but we do not generate random unit vectors
    * to define the cones. Instead, we sample unit vectors along the grid
@@ -207,7 +238,7 @@ int main(int argc, char *argv[]) {
 
           /* Do the test. */
           test_cone(id_bh, ti_current, random_number_BH_kick, opening_angle,
-                    unit_vector, 100000, 10, 0.1);
+                    unit_vector);
         }
       }
     }
diff --git a/tests/testRiemannTRRS.c b/tests/testRiemannTRRS.c
index 16956bec0fac0b645a577c8e4869450a21eff6d8..db988c3021bf808b6239fe4cb9a5cafc1ebc0eb0 100644
--- a/tests/testRiemannTRRS.c
+++ b/tests/testRiemannTRRS.c
@@ -18,6 +18,8 @@
  ******************************************************************************/
 #include <config.h>
 
+#if !defined(RIEMANN_SOLVER_NONE)
+
 /* Local headers. */
 #include <string.h>
 
@@ -326,3 +328,7 @@ int main(int argc, char* argv[]) {
 
   return 0;
 }
+
+#else
+int main(int argc, char* argv[]) { return 0; }
+#endif /* !RIEMANN_SOLVER_NONE */
diff --git a/tests/testSelectOutput.c b/tests/testSelectOutput.c
index b6a73c54828be921a2907ab9ebd21488f4f245da..527af39edfdb43cce064c36499e6d5bb6f210c28 100644
--- a/tests/testSelectOutput.c
+++ b/tests/testSelectOutput.c
@@ -172,7 +172,7 @@ int main(int argc, char *argv[]) {
 
   /* write output file */
   message("Writing output.");
-  write_output_single(&e, &us, &us);
+  write_output_single(&e, &us, &us, /*fof=*/0);
 
   /* Clean-up */
   message("Cleaning memory.");
diff --git a/tests/testSymmetry.c b/tests/testSymmetry.c
index a1fa8ed7bca97bb91d027fb52fd90920fcd78536..e3bb175d2cf198f757f178c27195fc0ac24f9120 100644
--- a/tests/testSymmetry.c
+++ b/tests/testSymmetry.c
@@ -39,13 +39,6 @@ void print_bytes(void *p, size_t len) {
 
 void test(void) {
 
-#if defined(SHADOWFAX_SPH)
-  /* Initialize the Voronoi simulation box */
-  double box_anchor[3] = {-2.0f, -2.0f, -2.0f};
-  double box_side[3] = {6.0f, 6.0f, 6.0f};
-/*  voronoi_set_box(box_anchor, box_side);*/
-#endif
-
   /* Start with some values for the cosmological parameters */
   const float a = (float)random_uniform(0.8, 1.);
   const float H = 1.f;
@@ -70,59 +63,6 @@ void test(void) {
   pi.time_bin = 1;
   pj.time_bin = 1;
 
-#if defined(SHADOWFAX_SPH)
-  /* Give the primitive variables sensible values, since the Riemann solver does
-     not like negative densities and pressures */
-  pi.primitives.rho = random_uniform(0.1f, 1.0f);
-  pi.primitives.v[0] = random_uniform(-10.0f, 10.0f);
-  pi.primitives.v[1] = random_uniform(-10.0f, 10.0f);
-  pi.primitives.v[2] = random_uniform(-10.0f, 10.0f);
-  pi.primitives.P = random_uniform(0.1f, 1.0f);
-  pj.primitives.rho = random_uniform(0.1f, 1.0f);
-  pj.primitives.v[0] = random_uniform(-10.0f, 10.0f);
-  pj.primitives.v[1] = random_uniform(-10.0f, 10.0f);
-  pj.primitives.v[2] = random_uniform(-10.0f, 10.0f);
-  pj.primitives.P = random_uniform(0.1f, 1.0f);
-  /* make gradients zero */
-  pi.primitives.gradients.rho[0] = 0.0f;
-  pi.primitives.gradients.rho[1] = 0.0f;
-  pi.primitives.gradients.rho[2] = 0.0f;
-  pi.primitives.gradients.v[0][0] = 0.0f;
-  pi.primitives.gradients.v[0][1] = 0.0f;
-  pi.primitives.gradients.v[0][2] = 0.0f;
-  pi.primitives.gradients.v[1][0] = 0.0f;
-  pi.primitives.gradients.v[1][1] = 0.0f;
-  pi.primitives.gradients.v[1][2] = 0.0f;
-  pi.primitives.gradients.v[2][0] = 0.0f;
-  pi.primitives.gradients.v[2][1] = 0.0f;
-  pi.primitives.gradients.v[2][2] = 0.0f;
-  pi.primitives.gradients.P[0] = 0.0f;
-  pi.primitives.gradients.P[1] = 0.0f;
-  pi.primitives.gradients.P[2] = 0.0f;
-  pj.primitives.gradients.rho[0] = 0.0f;
-  pj.primitives.gradients.rho[1] = 0.0f;
-  pj.primitives.gradients.rho[2] = 0.0f;
-  pj.primitives.gradients.v[0][0] = 0.0f;
-  pj.primitives.gradients.v[0][1] = 0.0f;
-  pj.primitives.gradients.v[0][2] = 0.0f;
-  pj.primitives.gradients.v[1][0] = 0.0f;
-  pj.primitives.gradients.v[1][1] = 0.0f;
-  pj.primitives.gradients.v[1][2] = 0.0f;
-  pj.primitives.gradients.v[2][0] = 0.0f;
-  pj.primitives.gradients.v[2][1] = 0.0f;
-  pj.primitives.gradients.v[2][2] = 0.0f;
-  pj.primitives.gradients.P[0] = 0.0f;
-  pj.primitives.gradients.P[1] = 0.0f;
-  pj.primitives.gradients.P[2] = 0.0f;
-
-  /* set time step to reasonable value */
-  pi.force.dt = 0.001;
-  pj.force.dt = 0.001;
-
-  voronoi_cell_init(&pi.cell, pi.x, box_anchor, box_side);
-  voronoi_cell_init(&pj.cell, pj.x, box_anchor, box_side);
-#endif
-
 #if defined(GIZMO_MFV_SPH)
   /* Give the primitive variables sensible values, since the Riemann solver does
      not like negative densities and pressures */
@@ -200,6 +140,7 @@ void test(void) {
   runner_iact_chemistry(r2, dx, pi.h, pj.h, &pi, &pj, a, H);
   runner_iact_pressure_floor(r2, dx, pi.h, pj.h, &pi, &pj, a, H);
   runner_iact_star_formation(r2, dx, pi.h, pj.h, &pi, &pj, a, H);
+  runner_iact_sink(r2, dx, pi.h, pj.h, &pi, &pj, a, H);
 
   /* Call the non-symmetric version */
   runner_iact_nonsym_density(r2, dx, pi2.h, pj2.h, &pi2, &pj2, a, H);
@@ -207,6 +148,7 @@ void test(void) {
   runner_iact_nonsym_chemistry(r2, dx, pi2.h, pj2.h, &pi2, &pj2, a, H);
   runner_iact_nonsym_pressure_floor(r2, dx, pi2.h, pj2.h, &pi2, &pj2, a, H);
   runner_iact_nonsym_star_formation(r2, dx, pi2.h, pj2.h, &pi2, &pj2, a, H);
+  runner_iact_nonsym_sink(r2, dx, pi2.h, pj2.h, &pi2, &pj2, a, H);
   dx[0] = -dx[0];
   dx[1] = -dx[1];
   dx[2] = -dx[2];
@@ -215,6 +157,7 @@ void test(void) {
   runner_iact_nonsym_chemistry(r2, dx, pj2.h, pi2.h, &pj2, &pi2, a, H);
   runner_iact_nonsym_pressure_floor(r2, dx, pj2.h, pi2.h, &pj2, &pi2, a, H);
   runner_iact_nonsym_star_formation(r2, dx, pj2.h, pi2.h, &pj2, &pi2, a, H);
+  runner_iact_nonsym_sink(r2, dx, pj2.h, pi2.h, &pj2, &pi2, a, H);
 
   /* Check that the particles are the same */
   i_not_ok = memcmp(&pi, &pi2, sizeof(struct part));
diff --git a/tests/testVoronoi1D.c b/tests/testVoronoi1D.c
deleted file mode 100644
index 083d9aaa279f241ae1ac4d0bfaeb2780a39574a4..0000000000000000000000000000000000000000
--- a/tests/testVoronoi1D.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (C) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-#include "hydro/Shadowswift/voronoi1d_algorithm.h"
-
-int main(int argc, char *argv[]) {
-
-  double box_anchor[1] = {-0.5};
-  double box_side[1] = {2.};
-
-  /* Create a Voronoi cell */
-  double x[1] = {0.5f};
-  struct voronoi_cell cell;
-  voronoi_cell_init(&cell, x, box_anchor, box_side);
-
-  /* Interact with a left and right neighbour */
-  float xL[1] = {0.5f};
-  float xR[1] = {-0.5f};
-  voronoi_cell_interact(&cell, xL, 1);
-  voronoi_cell_interact(&cell, xR, 2);
-
-  /* Interact with some more neighbours to check if they are properly ignored */
-  float x0[1] = {0.6f};
-  float x1[1] = {-0.7f};
-  voronoi_cell_interact(&cell, x0, 3);
-  voronoi_cell_interact(&cell, x1, 4);
-
-  /* Finalize cell and check results */
-  voronoi_cell_finalize(&cell);
-
-  if (cell.volume != 0.5f) {
-    error("Wrong volume: %g!", cell.volume);
-  }
-  if (cell.centroid != 0.5f) {
-    error("Wrong centroid: %g!", cell.centroid);
-  }
-  if (cell.idL != 1) {
-    error("Wrong left neighbour: %llu!", cell.idL);
-  }
-  if (cell.idR != 2) {
-    error("Wrong right neighbour: %llu!", cell.idR);
-  }
-
-  /* Check face method */
-  float A;
-  float midpoint[3];
-  A = voronoi_get_face(&cell, 1, midpoint);
-  if (A != 1.0f) {
-    error("Wrong surface area returned for left neighbour!");
-  }
-  if (midpoint[0] != -0.25f) {
-    error("Wrong midpoint returned for left neighbour!");
-  }
-  A = voronoi_get_face(&cell, 2, midpoint);
-  if (A != 1.0f) {
-    error("Wrong surface area returned for right neighbour!");
-  }
-  if (midpoint[0] != 0.25f) {
-    error("Wrong midpoint returned for right neighbour!");
-  }
-
-  return 0;
-}
diff --git a/tests/testVoronoi2D.c b/tests/testVoronoi2D.c
deleted file mode 100644
index 4708f7bdd44aea6b4ce9d022a5f5a0fc2e65f8c2..0000000000000000000000000000000000000000
--- a/tests/testVoronoi2D.c
+++ /dev/null
@@ -1,225 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (C) 2017 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-#include <config.h>
-
-/* Local headers. */
-#include "hydro/Shadowswift/voronoi2d_algorithm.h"
-#include "tools.h"
-
-/* Number of cells used to test the 2D interaction algorithm */
-#define TESTVORONOI2D_NUMCELL 100
-
-int main(int argc, char *argv[]) {
-
-  /* initialize simulation box */
-  double anchor[3] = {-0.5f, -0.5f, -0.5f};
-  double side[3] = {2.0f, 2.0f, 2.0f};
-
-  /* test initialization and finalization algorithms */
-  {
-    message("Testing initialization and finalization algorithm...");
-
-    struct voronoi_cell cell;
-    double x[3] = {0.5, 0.5, 0.5};
-
-    voronoi_cell_init(&cell, x, anchor, side);
-
-    float maxradius = voronoi_cell_finalize(&cell);
-
-    assert(maxradius == 2.0f * sqrtf(2.0f));
-
-    assert(cell.volume == 4.0f);
-
-    assert(cell.centroid[0] == 0.5f);
-    assert(cell.centroid[1] == 0.5f);
-
-    message("Done.");
-  }
-
-  /* test interaction algorithm: normal case */
-  {
-    message("Testing %i cell grid with random positions...",
-            TESTVORONOI2D_NUMCELL);
-
-    /* create 100 cells with random positions in [0,1]x[0,1] */
-    struct voronoi_cell cells[TESTVORONOI2D_NUMCELL];
-    double x[2];
-    float dx[2];
-    int i, j;
-    float Atot;
-    struct voronoi_cell *cell_i, *cell_j;
-
-    for (i = 0; i < TESTVORONOI2D_NUMCELL; ++i) {
-      x[0] = random_uniform(0., 1.);
-      x[1] = random_uniform(0., 1.);
-      voronoi_cell_init(&cells[i], x, anchor, side);
-#ifdef VORONOI_VERBOSE
-      message("cell[%i]: %g %g", i, x[0], x[1]);
-#endif
-    }
-
-    /* interact the cells (with periodic boundaries) */
-    for (i = 0; i < TESTVORONOI2D_NUMCELL; ++i) {
-      cell_i = &cells[i];
-      for (j = 0; j < TESTVORONOI2D_NUMCELL; ++j) {
-        if (i != j) {
-          cell_j = &cells[j];
-          dx[0] = cell_i->x[0] - cell_j->x[0];
-          dx[1] = cell_i->x[1] - cell_j->x[1];
-          /* periodic boundaries */
-          if (dx[0] >= 0.5f) {
-            dx[0] -= 1.0f;
-          }
-          if (dx[0] < -0.5f) {
-            dx[0] += 1.0f;
-          }
-          if (dx[1] >= 0.5f) {
-            dx[1] -= 1.0f;
-          }
-          if (dx[1] < -0.5f) {
-            dx[1] += 1.0f;
-          }
-#ifdef VORONOI_VERBOSE
-          message("Cell %i before:", i);
-          voronoi_print_cell(&cells[i]);
-          message("Interacting cell %i with cell %i (%g %g, %g %g", i, j,
-                  cells[i].x[0], cells[i].x[1], cells[j].x[0], cells[j].x[1]);
-#endif
-          voronoi_cell_interact(cell_i, dx, j);
-        }
-      }
-    }
-
-    Atot = 0.0f;
-    /* print the cells to the stdout */
-    for (i = 0; i < TESTVORONOI2D_NUMCELL; ++i) {
-#ifdef VORONOI_VERBOSE
-      printf("Cell %i:\n", i);
-      voronoi_print_cell(&cells[i]);
-#endif
-      voronoi_cell_finalize(&cells[i]);
-      Atot += cells[i].volume;
-    }
-
-    /* Check the total surface area */
-    assert(fabs(Atot - 1.0f) < 1.e-6);
-
-    /* Check the neighbour relations for an arbitrary cell: cell 44
-       We plotted the grid and manually found the correct neighbours and their
-       order. */
-    assert(cells[44].nvert == 7);
-    assert(cells[44].ngbs[0] == 26);
-    assert(cells[44].ngbs[1] == 38);
-    assert(cells[44].ngbs[2] == 3);
-    assert(cells[44].ngbs[3] == 33);
-    assert(cells[44].ngbs[4] == 5);
-    assert(cells[44].ngbs[5] == 90);
-    assert(cells[44].ngbs[6] == 4);
-
-    message("Done.");
-  }
-
-  /* test interaction algorithm */
-  {
-    message("Testing 100 cell grid with Cartesian mesh positions...");
-
-    struct voronoi_cell cells[100];
-    double x[2];
-    float dx[2];
-    int i, j;
-    float Atot;
-    struct voronoi_cell *cell_i, *cell_j;
-
-    for (i = 0; i < 10; ++i) {
-      for (j = 0; j < 10; ++j) {
-        x[0] = (i + 0.5f) * 0.1;
-        x[1] = (j + 0.5f) * 0.1;
-        voronoi_cell_init(&cells[10 * i + j], x, anchor, side);
-      }
-    }
-
-    /* interact the cells (with periodic boundaries) */
-    for (i = 0; i < 100; ++i) {
-      cell_i = &cells[i];
-      for (j = 0; j < 100; ++j) {
-        if (i != j) {
-          cell_j = &cells[j];
-          dx[0] = cell_i->x[0] - cell_j->x[0];
-          dx[1] = cell_i->x[1] - cell_j->x[1];
-          /* periodic boundaries */
-          if (dx[0] >= 0.5f) {
-            dx[0] -= 1.0f;
-          }
-          if (dx[0] < -0.5f) {
-            dx[0] += 1.0f;
-          }
-          if (dx[1] >= 0.5f) {
-            dx[1] -= 1.0f;
-          }
-          if (dx[1] < -0.5f) {
-            dx[1] += 1.0f;
-          }
-#ifdef VORONOI_VERBOSE
-          message("Cell %i before:", i);
-          voronoi_print_cell(&cells[i]);
-          message("Interacting cell %i with cell %i (%g %g, %g %g", i, j,
-                  cells[i].x[0], cells[i].x[1], cells[j].x[0], cells[j].x[1]);
-#endif
-          voronoi_cell_interact(cell_i, dx, j);
-        }
-      }
-    }
-
-    Atot = 0.0f;
-    /* print the cells to the stdout */
-    for (i = 0; i < 100; ++i) {
-#ifdef VORONOI_VERBOSE
-      printf("Cell %i:\n", i);
-      voronoi_print_cell(&cells[i]);
-#endif
-      voronoi_cell_finalize(&cells[i]);
-      Atot += cells[i].volume;
-    }
-
-    /* Check the total surface area */
-    assert(fabs(Atot - 1.0f) < 1.e-6);
-
-    /* Check the neighbour relations for an arbitrary cell: cell 44 We plotted
-       the grid and manually found the correct neighbours and their
-       order. Variation is found when optimizing, so we have two possible
-       outcomes... */
-    if (cells[44].nvert == 5) {
-      assert(cells[44].nvert == 5);
-      assert(cells[44].ngbs[0] == 43);
-      assert(cells[44].ngbs[1] == 34);
-      assert(cells[44].ngbs[2] == 45);
-      assert(cells[44].ngbs[3] == 55);
-
-    } else {
-      assert(cells[44].nvert == 4);
-      assert(cells[44].ngbs[0] == 34);
-      assert(cells[44].ngbs[1] == 45);
-      assert(cells[44].ngbs[2] == 54);
-      assert(cells[44].ngbs[3] == 43);
-    }
-    message("Done.");
-  }
-
-  return 0;
-}
diff --git a/tests/testVoronoi3D.c b/tests/testVoronoi3D.c
deleted file mode 100644
index de3eaf2d50782a20f526229484e2b255ec24dd52..0000000000000000000000000000000000000000
--- a/tests/testVoronoi3D.c
+++ /dev/null
@@ -1,1522 +0,0 @@
-/*******************************************************************************
- * This file is part of SWIFT.
- * Copyright (C) 2016 Bert Vandenbroucke (bert.vandenbroucke@gmail.com).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- ******************************************************************************/
-#include <config.h>
-
-/* Some standard headers. */
-#include <stdlib.h>
-
-/* Local headers. */
-#include "error.h"
-#include "hydro/Shadowswift/voronoi3d_algorithm.h"
-#include "part.h"
-#include "tools.h"
-
-/* Number of random generators to use in the first grid build test */
-#define TESTVORONOI3D_NUMCELL_RANDOM 100
-
-/* Number of cartesian generators to use (in one coordinate direction) for the
-   second grid build test. The total number of generators is the third power of
-   this number (so be careful with large numbers) */
-#define TESTVORONOI3D_NUMCELL_CARTESIAN_1D 5
-
-/* Total number of generators in the second grid build test. Do not change this
-   value, but change the 1D value above instead. */
-#define TESTVORONOI3D_NUMCELL_CARTESIAN_3D                                   \
-  (TESTVORONOI3D_NUMCELL_CARTESIAN_1D * TESTVORONOI3D_NUMCELL_CARTESIAN_1D * \
-   TESTVORONOI3D_NUMCELL_CARTESIAN_1D)
-
-/* Bottom front left corner and side lengths of the large box that contains all
-   particles and is used as initial cell at the start of the construction */
-#define VORONOI3D_BOX_ANCHOR_X 0.0f
-#define VORONOI3D_BOX_ANCHOR_Y 0.0f
-#define VORONOI3D_BOX_ANCHOR_Z 0.0f
-#define VORONOI3D_BOX_SIDE_X 1.0f
-#define VORONOI3D_BOX_SIDE_Y 1.0f
-#define VORONOI3D_BOX_SIDE_Z 1.0f
-
-/**
- * @brief Get the volume of the simulation box stored in the global variables.
- *
- * This method is only used for debugging purposes.
- *
- * @return Volume of the simulation box as it is stored in the global variables.
- */
-float voronoi_get_box_volume(void) {
-  return VORONOI3D_BOX_SIDE_X * VORONOI3D_BOX_SIDE_Y * VORONOI3D_BOX_SIDE_Z;
-}
-
-/**
- * @brief Get the centroid of the simulation box stored in the global variables.
- *
- * This method is only used for debugging purposes.
- *
- * @param box_centroid Array to store the resulting coordinates in.
- */
-void voronoi_get_box_centroid(float *box_centroid) {
-  box_centroid[0] = 0.5f * VORONOI3D_BOX_SIDE_X + VORONOI3D_BOX_ANCHOR_X;
-  box_centroid[1] = 0.5f * VORONOI3D_BOX_SIDE_Y + VORONOI3D_BOX_ANCHOR_Y;
-  box_centroid[2] = 0.5f * VORONOI3D_BOX_SIDE_Z + VORONOI3D_BOX_ANCHOR_Z;
-}
-
-/**
- * @brief Get the surface area and coordinates of the face midpoint for the
- * face of the simulation box with the given unique ID.
- *
- * This method is only used for debugging purposes.
- *
- * @param id Unique ID of one of the 6 faces of the simulation box.
- * @param face_midpoint Array to store the coordinates of the requested
- * midpoint in.
- * @return Surface area of the face, or 0 if the given ID does not correspond to
- * a known face of the simulation box.
- */
-float voronoi_get_box_face(unsigned long long id, float *face_midpoint) {
-
-  if (id == VORONOI3D_BOX_FRONT) {
-    face_midpoint[0] = 0.5f * VORONOI3D_BOX_SIDE_X + VORONOI3D_BOX_ANCHOR_X;
-    face_midpoint[1] = VORONOI3D_BOX_ANCHOR_Y;
-    face_midpoint[2] = 0.5f * VORONOI3D_BOX_SIDE_Z + VORONOI3D_BOX_ANCHOR_Z;
-    return VORONOI3D_BOX_SIDE_X * VORONOI3D_BOX_SIDE_Z;
-  }
-  if (id == VORONOI3D_BOX_BACK) {
-    face_midpoint[0] = 0.5f * VORONOI3D_BOX_SIDE_X + VORONOI3D_BOX_ANCHOR_X;
-    face_midpoint[1] = VORONOI3D_BOX_ANCHOR_Y + VORONOI3D_BOX_SIDE_Y;
-    face_midpoint[2] = 0.5f * VORONOI3D_BOX_SIDE_Z + VORONOI3D_BOX_ANCHOR_Z;
-    return VORONOI3D_BOX_SIDE_X * VORONOI3D_BOX_SIDE_Z;
-  }
-
-  if (id == VORONOI3D_BOX_BOTTOM) {
-    face_midpoint[0] = 0.5f * VORONOI3D_BOX_SIDE_X + VORONOI3D_BOX_ANCHOR_X;
-    face_midpoint[1] = 0.5f * VORONOI3D_BOX_SIDE_Y + VORONOI3D_BOX_ANCHOR_Y;
-    face_midpoint[2] = VORONOI3D_BOX_ANCHOR_Z;
-    return VORONOI3D_BOX_SIDE_X * VORONOI3D_BOX_SIDE_Y;
-  }
-  if (id == VORONOI3D_BOX_TOP) {
-    face_midpoint[0] = 0.5f * VORONOI3D_BOX_SIDE_X + VORONOI3D_BOX_ANCHOR_X;
-    face_midpoint[1] = 0.5f * VORONOI3D_BOX_SIDE_Y + VORONOI3D_BOX_ANCHOR_Y;
-    face_midpoint[2] = VORONOI3D_BOX_ANCHOR_Z + VORONOI3D_BOX_SIDE_Z;
-    return VORONOI3D_BOX_SIDE_X * VORONOI3D_BOX_SIDE_Y;
-  }
-
-  if (id == VORONOI3D_BOX_LEFT) {
-    face_midpoint[0] = VORONOI3D_BOX_ANCHOR_X;
-    face_midpoint[1] = 0.5f * VORONOI3D_BOX_SIDE_Y + VORONOI3D_BOX_ANCHOR_Y;
-    face_midpoint[2] = 0.5f * VORONOI3D_BOX_SIDE_Z + VORONOI3D_BOX_ANCHOR_Z;
-    return VORONOI3D_BOX_SIDE_X * VORONOI3D_BOX_SIDE_Y;
-  }
-  if (id == VORONOI3D_BOX_RIGHT) {
-    face_midpoint[0] = VORONOI3D_BOX_ANCHOR_X + VORONOI3D_BOX_SIDE_X;
-    face_midpoint[1] = 0.5f * VORONOI3D_BOX_SIDE_Y + VORONOI3D_BOX_ANCHOR_Y;
-    face_midpoint[2] = 0.5f * VORONOI3D_BOX_SIDE_Z + VORONOI3D_BOX_ANCHOR_Z;
-    return VORONOI3D_BOX_SIDE_X * VORONOI3D_BOX_SIDE_Y;
-  }
-
-  return 0.0f;
-}
-
-/**
- * @brief Check if voronoi_volume_tetrahedron() works
- */
-void test_voronoi_volume_tetrahedron(void) {
-  float v1[3] = {0., 0., 0.};
-  float v2[3] = {0., 0., 1.};
-  float v3[3] = {0., 1., 0.};
-  float v4[3] = {1., 0., 0.};
-
-  float V = voronoi_volume_tetrahedron(v1, v2, v3, v4);
-  assert(V == 1.0f / 6.0f);
-}
-
-/**
- * @brief Check if voronoi_centroid_tetrahedron() works
- */
-void test_voronoi_centroid_tetrahedron(void) {
-  float v1[3] = {0., 0., 0.};
-  float v2[3] = {0., 0., 1.};
-  float v3[3] = {0., 1., 0.};
-  float v4[3] = {1., 0., 0.};
-
-  float centroid[3];
-  voronoi_centroid_tetrahedron(centroid, v1, v2, v3, v4);
-  assert(centroid[0] == 0.25f);
-  assert(centroid[1] == 0.25f);
-  assert(centroid[2] == 0.25f);
-}
-
-/**
- * @brief Check if voronoi_calculate_cell() works
- */
-void test_calculate_cell(void) {
-
-  double box_anchor[3] = {VORONOI3D_BOX_ANCHOR_X, VORONOI3D_BOX_ANCHOR_Y,
-                          VORONOI3D_BOX_ANCHOR_Z};
-  double box_side[3] = {VORONOI3D_BOX_SIDE_X, VORONOI3D_BOX_SIDE_Y,
-                        VORONOI3D_BOX_SIDE_Z};
-
-  struct voronoi_cell cell;
-
-  cell.x[0] = 0.5f;
-  cell.x[1] = 0.5f;
-  cell.x[2] = 0.5f;
-
-  /* Initialize the cell to a large cube. */
-  voronoi_initialize(&cell, box_anchor, box_side);
-  /* Calculate the volume and centroid of the large cube. */
-  voronoi_calculate_cell(&cell);
-  /* Calculate the faces. */
-  voronoi_calculate_faces(&cell);
-
-  /* Update these values if you ever change to another large cube! */
-  assert(cell.volume == voronoi_get_box_volume());
-  float box_centroid[3];
-  voronoi_get_box_centroid(box_centroid);
-  assert(cell.centroid[0] = box_centroid[0]);
-  assert(cell.centroid[1] = box_centroid[1]);
-  assert(cell.centroid[2] = box_centroid[2]);
-
-  /* Check cell neighbours. */
-  assert(cell.nface == 6);
-  assert(cell.ngbs[0] == VORONOI3D_BOX_FRONT);
-  assert(cell.ngbs[1] == VORONOI3D_BOX_LEFT);
-  assert(cell.ngbs[2] == VORONOI3D_BOX_BOTTOM);
-  assert(cell.ngbs[3] == VORONOI3D_BOX_TOP);
-  assert(cell.ngbs[4] == VORONOI3D_BOX_BACK);
-  assert(cell.ngbs[5] == VORONOI3D_BOX_RIGHT);
-
-  /* Check cell faces */
-  float face_midpoint[3], face_area;
-  face_area = voronoi_get_box_face(VORONOI3D_BOX_FRONT, face_midpoint);
-  assert(cell.face_areas[0] == face_area);
-  assert(cell.face_midpoints[0][0] == face_midpoint[0] - cell.x[0]);
-  assert(cell.face_midpoints[0][1] == face_midpoint[1] - cell.x[1]);
-  assert(cell.face_midpoints[0][2] == face_midpoint[2] - cell.x[2]);
-
-  face_area = voronoi_get_box_face(VORONOI3D_BOX_LEFT, face_midpoint);
-  assert(cell.face_areas[1] == face_area);
-  assert(cell.face_midpoints[1][0] == face_midpoint[0] - cell.x[0]);
-  assert(cell.face_midpoints[1][1] == face_midpoint[1] - cell.x[1]);
-  assert(cell.face_midpoints[1][2] == face_midpoint[2] - cell.x[2]);
-
-  face_area = voronoi_get_box_face(VORONOI3D_BOX_BOTTOM, face_midpoint);
-  assert(cell.face_areas[2] == face_area);
-  assert(cell.face_midpoints[2][0] == face_midpoint[0] - cell.x[0]);
-  assert(cell.face_midpoints[2][1] == face_midpoint[1] - cell.x[1]);
-  assert(cell.face_midpoints[2][2] == face_midpoint[2] - cell.x[2]);
-
-  face_area = voronoi_get_box_face(VORONOI3D_BOX_TOP, face_midpoint);
-  assert(cell.face_areas[3] == face_area);
-  assert(cell.face_midpoints[3][0] == face_midpoint[0] - cell.x[0]);
-  assert(cell.face_midpoints[3][1] == face_midpoint[1] - cell.x[1]);
-  assert(cell.face_midpoints[3][2] == face_midpoint[2] - cell.x[2]);
-
-  face_area = voronoi_get_box_face(VORONOI3D_BOX_BACK, face_midpoint);
-  assert(cell.face_areas[4] == face_area);
-  assert(cell.face_midpoints[4][0] == face_midpoint[0] - cell.x[0]);
-  assert(cell.face_midpoints[4][1] == face_midpoint[1] - cell.x[1]);
-  assert(cell.face_midpoints[4][2] == face_midpoint[2] - cell.x[2]);
-
-  face_area = voronoi_get_box_face(VORONOI3D_BOX_RIGHT, face_midpoint);
-  assert(cell.face_areas[5] == face_area);
-  assert(cell.face_midpoints[5][0] == face_midpoint[0] - cell.x[0]);
-  assert(cell.face_midpoints[5][1] == face_midpoint[1] - cell.x[1]);
-  assert(cell.face_midpoints[5][2] == face_midpoint[2] - cell.x[2]);
-}
-
-void test_paths(void) {
-  float u, l, q;
-  int up, us, uw, lp, ls, lw, qp, qs, qw;
-  float r2, dx[3];
-  struct voronoi_cell cell;
-
-  /* PATH 1.0 */
-  // the first vertex is above the cutting plane and its first edge is below the
-  // plane
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -1.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.nvert = 2;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.edges[0] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edges[3] = 0;
-    cell.edgeindices[3] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 0);
-    assert(us == 0);
-    assert(uw == 1);
-    assert(u == 0.25f);
-    assert(lp == 1);
-    assert(ls == 0);
-    assert(lw == -1);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.1 */
-  // the first vertex is above the cutting plane and its second edge is below
-  // the plane
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 2.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.edges[0] = 1;
-    cell.edges[1] = 2;
-    cell.edges[6] = 0;
-    cell.edgeindices[1] = 0;
-    cell.edgeindices[6] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 0);
-    assert(us == 1);
-    assert(uw == 1);
-    assert(u == 0.25f);
-    assert(lp == 2);
-    assert(ls == 0);
-    assert(lw == -1);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.2 */
-  // the first vertex is above the cutting plane and its second and last edge
-  // is below the plane
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 2.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 2;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 2;
-    cell.offsets[2] = 5;
-    cell.edges[0] = 1;
-    cell.edges[1] = 2;
-    cell.edges[6] = 0;
-    cell.edgeindices[1] = 0;
-    cell.edgeindices[5] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 0);
-    assert(us == 1);
-    assert(uw == 1);
-    assert(u == 0.25f);
-    assert(lp == 2);
-    assert(ls == 0);
-    assert(lw == -1);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.3 */
-  // the first vertex is above the cutting plane and is the closest vertex to
-  // the plane. The code should crash.
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 2.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = 2.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.offsets[0] = 0;
-    cell.edges[0] = 1;
-    cell.edges[1] = 2;
-    cell.edges[2] = 3;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == -1);
-  }
-
-  /* PATH 1.4.0 */
-  // first vertex is above the plane, second vertex is closer and third vertex
-  // lies below
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.75f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.edges[0] = 1;
-    cell.edges[3] = 2;
-    cell.edges[6] = 1;
-    cell.edgeindices[0] = 2;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[6] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 1);
-    assert(us == 0);
-    assert(u == 0.125f);
-    assert(lp == 2);
-    assert(ls == 0);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.4.1 */
-  // first vertex is above the plane, second vertex is closer and fourth vertex
-  // is below
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.75f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = -1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.offsets[3] = 9;
-    cell.edges[0] = 1;
-    cell.edges[3] = 2;
-    cell.edges[4] = 3;
-    cell.edges[5] = 0;
-    cell.edges[6] = 1;
-    cell.edges[9] = 1;
-    // this is the only difference between PATH 1.4.1 and PATH 1.4.2
-    cell.edgeindices[0] = 3;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[4] = 0;
-    cell.edgeindices[9] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 1);
-    assert(us == 1);
-    assert(u == 0.125f);
-    assert(lp == 3);
-    assert(ls == 0);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.4.2 */
-  // first vertex is above the plane, second is closer, fourth is below
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.75f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = -1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.offsets[3] = 9;
-    cell.edges[0] = 1;
-    cell.edges[3] = 2;
-    cell.edges[4] = 3;
-    cell.edges[5] = 0;
-    cell.edges[6] = 1;
-    cell.edges[9] = 1;
-    // this is the only difference between PATH 1.4.1 and PATH 1.4.2
-    cell.edgeindices[0] = 2;
-    cell.edgeindices[3] = 1;
-    cell.edgeindices[4] = 0;
-    cell.edgeindices[9] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 1);
-    assert(us == 1);
-    assert(u == 0.125f);
-    assert(lp == 3);
-    assert(ls == 0);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.4.3 */
-  // first vertex is above the plane, second is closer, third is below
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.75f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.edges[0] = 1;
-    // this is the difference between PATH 1.4.0 and this path
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edges[6] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[6] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 1);
-    assert(us == 1);
-    assert(u == 0.125f);
-    assert(lp == 2);
-    assert(ls == 0);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.4.4 */
-  // first vertex is above the plane, second is closer, fourth is below
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.75f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = -1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 4;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 7;
-    cell.offsets[3] = 10;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edges[5] = 3;
-    cell.edges[7] = 1;
-    cell.edges[10] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[5] = 0;
-    cell.edgeindices[10] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 1);
-    assert(us == 2);
-    assert(u == 0.125f);
-    assert(lp == 3);
-    assert(ls == 0);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.4.5 */
-  // same as 1.4.4, but with an order 3 second vertex
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.75f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = -1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.offsets[3] = 9;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edges[5] = 3;
-    cell.edges[6] = 1;
-    cell.edges[9] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[5] = 0;
-    cell.edgeindices[9] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 1);
-    assert(us == 2);
-    assert(u == 0.125f);
-    assert(lp == 3);
-    assert(ls == 0);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 1.4.6 */
-  // first vertex is above the plane, second is closer and is the closest
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.75f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 2;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 5;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == -1);
-  }
-
-  /* PATH 1.5 */
-  // first vertex is above the plane, second vertex is too close to call
-  {
-    cell.vertices[0] = 1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.nvert = 2;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 2);
-    assert(up == 1);
-  }
-
-  /* PATH 2.0 */
-  // the first vertex is below the plane and its first edge is above the plane
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 1.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.nvert = 2;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.edges[0] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edges[3] = 0;
-    cell.edgeindices[3] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 1);
-    assert(us == 0);
-    assert(uw == -1);
-    assert(u == 0.25f);
-    assert(lp == 0);
-    assert(ls == 0);
-    assert(qw == 1);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 2.1 */
-  // the first vertex is below the plane and its second edge is above the plane
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -2.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.edges[0] = 1;
-    cell.edges[1] = 2;
-    cell.edges[6] = 0;
-    cell.edgeindices[1] = 0;
-    cell.edgeindices[6] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 2);
-    assert(us == 0);
-    assert(uw == -1);
-    assert(u == 0.25f);
-    assert(lp == 0);
-    assert(ls == 1);
-    assert(qw == 1);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 2.2 */
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -2.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 2;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 2;
-    cell.offsets[2] = 5;
-    cell.edges[0] = 1;
-    cell.edges[1] = 2;
-    cell.edges[5] = 0;
-    cell.edgeindices[1] = 0;
-    cell.edgeindices[5] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 2);
-    assert(us == 0);
-    assert(uw == -1);
-    assert(u == 0.25f);
-    assert(lp == 0);
-    assert(ls == 1);
-    assert(qw == 1);
-    assert(l == -0.75f);
-  }
-
-  /* PATH 2.3 */
-  // the first vertex is below the plane and is the closest vertex to the plane
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -2.0f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = -2.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.offsets[0] = 0;
-    cell.edges[0] = 1;
-    cell.edges[1] = 2;
-    cell.edges[2] = 3;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 0);
-  }
-
-  /* PATH 2.4.0 */
-  // first vertex is below the plane, second is closer and third is above
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.edges[0] = 1;
-    cell.edges[3] = 2;
-    cell.edges[5] = 0;
-    cell.edges[6] = 1;
-    cell.edgeindices[0] = 2;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[5] = 0;
-    cell.edgeindices[6] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 2);
-    assert(u == 0.25f);
-    assert(lp == 1);
-    assert(l == -0.5f);
-  }
-
-  /* PATH 2.4.1 */
-  // first vertex is below, second is closer and fourth is above
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = 1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 4;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 7;
-    cell.offsets[3] = 10;
-    cell.edges[0] = 1;
-    cell.edges[3] = 2;
-    cell.edges[4] = 3;
-    cell.edges[6] = 0;
-    cell.edges[10] = 1;
-    cell.edgeindices[0] = 3;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[4] = 0;
-    cell.edgeindices[6] = 0;
-    cell.edgeindices[10] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 3);
-    assert(us == 0);
-    assert(u == 0.25f);
-    assert(lp == 1);
-    assert(ls == 1);
-    assert(l == -0.5f);
-  }
-
-  /* PATH 2.4.2 */
-  // first vertex is below, second is closer and fourth is above
-  // same as 2.4.1, but with order 3 second vertex
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = 1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.offsets[3] = 9;
-    cell.edges[0] = 1;
-    cell.edges[3] = 2;
-    cell.edges[4] = 3;
-    cell.edges[5] = 0;
-    cell.edges[9] = 1;
-    cell.edgeindices[0] = 3;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[4] = 0;
-    cell.edgeindices[5] = 0;
-    cell.edgeindices[9] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 3);
-    assert(us == 0);
-    assert(u == 0.25f);
-    assert(lp == 1);
-    assert(ls == 1);
-    assert(l == -0.5f);
-  }
-
-  /* PATH 2.4.3 */
-  // first vertex is below, second is closer, third is above
-  // first vertex is first edge of second
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = 1.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edges[6] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[4] = 0;
-    cell.edgeindices[6] = 1;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 2);
-    assert(us == 0);
-    assert(u == 0.25f);
-    assert(lp == 1);
-    assert(ls == 1);
-    assert(l == -0.5f);
-  }
-
-  /* PATH 2.4.4 */
-  // first vertex is below, second is closer, fourth is above
-  // first vertex is first edge of second
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = 1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 4;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 7;
-    cell.offsets[3] = 10;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edges[5] = 3;
-    cell.edges[10] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[5] = 0;
-    cell.edgeindices[10] = 2;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 3);
-    assert(us == 0);
-    assert(u == 0.25f);
-    assert(lp == 1);
-    assert(ls == 2);
-    assert(l == -0.5f);
-  }
-
-  /* PATH 2.4.5 */
-  // first vertex is below, second is closer, fourth is above
-  // first vertex is first edge of second
-  // second vertex is order 3 vertex (and not order 4 like 2.4.4)
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.vertices[9] = 1.0f;
-    cell.vertices[10] = 0.0f;
-    cell.vertices[11] = 0.0f;
-    cell.nvert = 4;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.orders[2] = 3;
-    cell.orders[3] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 6;
-    cell.offsets[3] = 9;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edges[5] = 3;
-    cell.edges[9] = 1;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[5] = 0;
-    cell.edgeindices[9] = 2;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 1);
-    assert(up == 3);
-    assert(us == 0);
-    assert(u == 0.25f);
-    assert(lp == 1);
-    assert(ls == 2);
-    assert(l == -0.5f);
-  }
-
-  /* PATH 2.4.6 */
-  // first vertex is below, second is closer and is closest
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = -0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.vertices[6] = -2.0f;
-    cell.vertices[7] = 0.0f;
-    cell.vertices[8] = 0.0f;
-    cell.nvert = 3;
-    cell.orders[0] = 3;
-    cell.orders[1] = 2;
-    cell.orders[2] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.offsets[2] = 5;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edges[4] = 2;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    cell.edgeindices[4] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 0);
-  }
-
-  /* PATH 2.5 */
-  // first vertex is below, second is too close to call
-  {
-    cell.vertices[0] = -1.0f;
-    cell.vertices[1] = 0.0f;
-    cell.vertices[2] = 0.0f;
-    cell.vertices[3] = 0.5f;
-    cell.vertices[4] = 0.0f;
-    cell.vertices[5] = 0.0f;
-    cell.nvert = 2;
-    cell.orders[0] = 3;
-    cell.orders[1] = 3;
-    cell.offsets[0] = 0;
-    cell.offsets[1] = 3;
-    cell.edges[0] = 1;
-    cell.edges[3] = 0;
-    cell.edgeindices[0] = 0;
-    cell.edgeindices[3] = 0;
-    dx[0] = 0.5f;
-    dx[1] = 0.0f;
-    dx[2] = 0.0f;
-    r2 = 0.25f;
-    int result = voronoi_intersect_find_closest_vertex(
-        &cell, dx, r2, &u, &up, &us, &uw, &l, &lp, &ls, &lw, &q, &qp, &qs, &qw);
-    assert(result == 2);
-  }
-}
-
-#ifdef SHADOWFAX_SPH
-void set_coordinates(struct part *p, double x, double y, double z,
-                     unsigned int id) {
-
-  double box_anchor[3] = {VORONOI3D_BOX_ANCHOR_X, VORONOI3D_BOX_ANCHOR_Y,
-                          VORONOI3D_BOX_ANCHOR_Z};
-  double box_side[3] = {VORONOI3D_BOX_SIDE_X, VORONOI3D_BOX_SIDE_Y,
-                        VORONOI3D_BOX_SIDE_Z};
-
-  p->x[0] = x;
-  p->x[1] = y;
-  p->x[2] = z;
-  p->id = id;
-  voronoi_cell_init(&p->cell, p->x, box_anchor, box_side);
-}
-#endif
-
-void test_degeneracies(void) {
-#ifdef SHADOWFAX_SPH
-  int idx = 0;
-  /* make a small cube */
-  struct part particles[100];
-  set_coordinates(&particles[idx], 0.1, 0.1, 0.1, idx);
-  idx++;
-  set_coordinates(&particles[idx], 0.2, 0.1, 0.1, idx);
-  idx++;
-  set_coordinates(&particles[idx], 0.1, 0.2, 0.1, idx);
-  idx++;
-  set_coordinates(&particles[idx], 0.1, 0.1, 0.2, idx);
-  idx++;
-  /* corner on cutting plane */
-  set_coordinates(&particles[idx], 0.2, 0.2, 0.2, idx);
-  idx++;
-  /* edge on cutting plane */
-  set_coordinates(&particles[idx], 0.2, 0.1, 0.2, idx);
-  idx++;
-  set_coordinates(&particles[idx], 0.2, 0.2, 0.1, idx);
-  idx++;
-  /* cutting plane is diagonal */
-  set_coordinates(&particles[idx], 0.05, 0.1, 0.05, idx);
-  idx++;
-  /* order 4 vertex (found after an impressive display of analytical geometry
-     of which I'm rather proud) */
-  float t = 0.5 / 0.0475;
-  set_coordinates(&particles[idx], 0.0075 * t + 0.1, 0.0075 * t + 0.1,
-                  0.1 - 0.0025 * t, idx);
-  idx++;
-  /* order 4 vertex with float edge */
-  t = 0.35 / 0.06125;
-  set_coordinates(&particles[idx], 0.0075 * t + 0.1, 0.015 * t + 0.1,
-                  0.1 - 0.005 * t, idx);
-  idx++;
-  /* plane that was already encountered */
-  t = 0.5 / 0.0475;
-  set_coordinates(&particles[idx], 0.0075 * t + 0.1, 0.0075 * t + 0.1,
-                  0.1 - 0.0025 * t, idx);
-  idx++;
-  /* no intersection (just to cover all code) */
-  set_coordinates(&particles[idx], 0.3, 0.3, 0.3, idx);
-  idx++;
-  set_coordinates(&particles[idx], 0.3, 0.1, 0.3, idx);
-  idx++;
-  /* order 5 vertex */
-  t = 0.04 / 0.0175;
-  set_coordinates(&particles[idx], 0.1 - 0.0075 * t, 0.1 + 0.00375 * t,
-                  0.1 + 0.00625 * t, idx);
-  idx++;
-  /* plane with order 5 vertex */
-  set_coordinates(&particles[idx], 0.1, 0.2, 0.1, idx);
-  idx++;
-  /* edge with order 5 vertex that looses an edge */
-  t = -0.1 / 0.095;
-  set_coordinates(&particles[idx], 0.1 - 0.015 * t, 0.1 + 0.015 * t,
-                  0.1 - 0.005 * t, idx);
-  idx++;
-  for (int i = 1; i < idx; i++) {
-    float dx[3];
-    dx[0] = particles[0].x[0] - particles[i].x[0];
-    dx[1] = particles[0].x[1] - particles[i].x[1];
-    dx[2] = particles[0].x[2] - particles[i].x[2];
-    voronoi_cell_interact(&particles[0].cell, dx, particles[i].id);
-  }
-#endif
-}
-
-int main(int argc, char *argv[]) {
-
-  /* Set the all enclosing simulation box dimensions */
-  double box_anchor[3] = {VORONOI3D_BOX_ANCHOR_X, VORONOI3D_BOX_ANCHOR_Y,
-                          VORONOI3D_BOX_ANCHOR_Z};
-  double box_side[3] = {VORONOI3D_BOX_SIDE_X, VORONOI3D_BOX_SIDE_Y,
-                        VORONOI3D_BOX_SIDE_Z};
-
-  /* Check basic Voronoi cell functions */
-  test_voronoi_volume_tetrahedron();
-  test_voronoi_centroid_tetrahedron();
-  test_calculate_cell();
-
-  /* Test the different paths */
-  test_paths();
-
-  /* Test the interaction and geometry algorithms */
-  {
-    /* Create a Voronoi cell */
-    double x[3] = {0.5f, 0.5f, 0.5f};
-    struct voronoi_cell cell;
-    voronoi_cell_init(&cell, x, box_anchor, box_side);
-
-    /* Interact with neighbours */
-    float x0[3] = {0.5f, 0.0f, 0.0f};
-    float x1[3] = {-0.5f, 0.0f, 0.0f};
-    float x2[3] = {0.0f, 0.5f, 0.0f};
-    float x3[3] = {0.0f, -0.5f, 0.0f};
-    float x4[3] = {0.0f, 0.0f, 0.5f};
-    float x5[3] = {0.0f, 0.0f, -0.5f};
-    voronoi_cell_interact(&cell, x0, 1);
-    voronoi_cell_interact(&cell, x1, 2);
-    voronoi_cell_interact(&cell, x2, 3);
-    voronoi_cell_interact(&cell, x3, 4);
-    voronoi_cell_interact(&cell, x4, 5);
-    voronoi_cell_interact(&cell, x5, 6);
-    float expected_midpoints[6][3], expected_areas[6];
-    expected_areas[0] = 0.25f;
-    expected_midpoints[0][0] = 0.25f;
-    expected_midpoints[0][1] = 0.5f;
-    expected_midpoints[0][2] = 0.5f;
-    expected_areas[1] = 0.25f;
-    expected_midpoints[1][0] = 0.75f;
-    expected_midpoints[1][1] = 0.5f;
-    expected_midpoints[1][2] = 0.5f;
-    expected_areas[2] = 0.25f;
-    expected_midpoints[2][0] = 0.5f;
-    expected_midpoints[2][1] = 0.25f;
-    expected_midpoints[2][2] = 0.5f;
-    expected_areas[3] = 0.25f;
-    expected_midpoints[3][0] = 0.5f;
-    expected_midpoints[3][1] = 0.75f;
-    expected_midpoints[3][2] = 0.5f;
-    expected_areas[4] = 0.25f;
-    expected_midpoints[4][0] = 0.5f;
-    expected_midpoints[4][1] = 0.5f;
-    expected_midpoints[4][2] = 0.25f;
-    expected_areas[5] = 0.25f;
-    expected_midpoints[5][0] = 0.5f;
-    expected_midpoints[5][1] = 0.5f;
-    expected_midpoints[5][2] = 0.75f;
-
-    /* Interact with some more neighbours to check if they are properly
-       ignored */
-    float xE0[3] = {0.6f, 0.0f, 0.1f};
-    float xE1[3] = {-0.7f, 0.2f, 0.04f};
-    voronoi_cell_interact(&cell, xE0, 7);
-    voronoi_cell_interact(&cell, xE1, 8);
-
-    /* Finalize cell and check results */
-    voronoi_cell_finalize(&cell);
-
-    if (fabs(cell.volume - 0.125f) > 1.e-5) {
-      error("Wrong volume: %g!", cell.volume);
-    }
-    if (fabs(cell.centroid[0] - 0.5f) > 1.e-5f ||
-        fabs(cell.centroid[1] - 0.5f) > 1.e-5f ||
-        fabs(cell.centroid[2] - 0.5f) > 1.e-5f) {
-      error("Wrong centroid: %g %g %g!", cell.centroid[0], cell.centroid[1],
-            cell.centroid[2]);
-    }
-
-    /* Check faces. */
-    float A, midpoint[3];
-    for (int i = 0; i < 6; ++i) {
-      A = voronoi_get_face(&cell, i + 1, midpoint);
-      if (A) {
-        if (fabs(A - expected_areas[i]) > 1.e-5) {
-          error("Wrong surface area: %g!", A);
-        }
-        if (fabs(midpoint[0] - expected_midpoints[i][0] + cell.x[0]) > 1.e-5 ||
-            fabs(midpoint[1] - expected_midpoints[i][1] + cell.x[1]) > 1.e-5 ||
-            fabs(midpoint[2] - expected_midpoints[i][2] + cell.x[2]) > 1.e-5) {
-          error("Wrong face midpoint: %g %g %g (should be %g %g %g)!",
-                midpoint[0], midpoint[1], midpoint[2], expected_midpoints[i][0],
-                expected_midpoints[i][1], expected_midpoints[i][2]);
-        }
-      } else {
-        error("Neighbour %i not found!", i);
-      }
-    }
-  }
-
-  /* Test degenerate cases */
-  test_degeneracies();
-
-  /* Construct a small random grid */
-  {
-    message("Constructing a small random grid...");
-
-    int i, j;
-    double x[3];
-    float dx[3];
-    float Vtot;
-    struct voronoi_cell cells[TESTVORONOI3D_NUMCELL_RANDOM];
-    struct voronoi_cell *cell_i, *cell_j;
-
-    /* initialize cells with random generator locations */
-    for (i = 0; i < TESTVORONOI3D_NUMCELL_RANDOM; ++i) {
-      x[0] = random_uniform(0., 1.);
-      x[1] = random_uniform(0., 1.);
-      x[2] = random_uniform(0., 1.);
-      voronoi_cell_init(&cells[i], x, box_anchor, box_side);
-    }
-
-    /* interact the cells */
-    for (i = 0; i < TESTVORONOI3D_NUMCELL_RANDOM; ++i) {
-      cell_i = &cells[i];
-      for (j = 0; j < TESTVORONOI3D_NUMCELL_RANDOM; ++j) {
-        if (i != j) {
-          cell_j = &cells[j];
-          dx[0] = cell_i->x[0] - cell_j->x[0];
-          dx[1] = cell_i->x[1] - cell_j->x[1];
-          dx[2] = cell_i->x[2] - cell_j->x[2];
-          voronoi_cell_interact(cell_i, dx, j);
-        }
-      }
-    }
-
-    Vtot = 0.0f;
-    /* print the cells to the stdout */
-    for (i = 0; i < TESTVORONOI3D_NUMCELL_RANDOM; ++i) {
-      /*      voronoi_print_gnuplot_c(&cells[i]);*/
-      voronoi_cell_finalize(&cells[i]);
-      Vtot += cells[i].volume;
-    }
-
-    assert(fabs(Vtot - 1.0f) < 1.e-6);
-
-    message("Done.");
-  }
-
-  /* Construct a small Cartesian grid full of degeneracies */
-  {
-    message("Constructing a Cartesian grid...");
-
-    int i, j, k;
-    double x[3];
-    float dx[3];
-    float Vtot;
-    struct voronoi_cell cells[TESTVORONOI3D_NUMCELL_CARTESIAN_3D];
-    struct voronoi_cell *cell_i, *cell_j;
-
-    /* initialize cells with Cartesian generator locations */
-    for (i = 0; i < TESTVORONOI3D_NUMCELL_CARTESIAN_1D; ++i) {
-      for (j = 0; j < TESTVORONOI3D_NUMCELL_CARTESIAN_1D; ++j) {
-        for (k = 0; k < TESTVORONOI3D_NUMCELL_CARTESIAN_1D; ++k) {
-          x[0] = (i + 0.5f) * 1.0 / TESTVORONOI3D_NUMCELL_CARTESIAN_1D;
-          x[1] = (j + 0.5f) * 1.0 / TESTVORONOI3D_NUMCELL_CARTESIAN_1D;
-          x[2] = (k + 0.5f) * 1.0 / TESTVORONOI3D_NUMCELL_CARTESIAN_1D;
-          voronoi_cell_init(&cells[TESTVORONOI3D_NUMCELL_CARTESIAN_1D *
-                                       TESTVORONOI3D_NUMCELL_CARTESIAN_1D * i +
-                                   TESTVORONOI3D_NUMCELL_CARTESIAN_1D * j + k],
-                            x, box_anchor, box_side);
-        }
-      }
-    }
-
-    /* interact the cells */
-    for (i = 0; i < TESTVORONOI3D_NUMCELL_CARTESIAN_3D; ++i) {
-      cell_i = &cells[i];
-      for (j = 0; j < TESTVORONOI3D_NUMCELL_CARTESIAN_3D; ++j) {
-        if (i != j) {
-          cell_j = &cells[j];
-          dx[0] = cell_i->x[0] - cell_j->x[0];
-          dx[1] = cell_i->x[1] - cell_j->x[1];
-          dx[2] = cell_i->x[2] - cell_j->x[2];
-          voronoi_cell_interact(cell_i, dx, j);
-        }
-      }
-    }
-
-    Vtot = 0.0f;
-    /* print the cells to the stdout */
-    for (i = 0; i < TESTVORONOI3D_NUMCELL_CARTESIAN_3D; ++i) {
-      /*      voronoi_print_gnuplot_c(&cells[i]);*/
-      voronoi_cell_finalize(&cells[i]);
-      Vtot += cells[i].volume;
-    }
-
-    message("Vtot: %g (Vtot-1.0f: %g)", Vtot, (Vtot - 1.0f));
-    assert(fabs(Vtot - 1.0f) < 2.e-6);
-
-    message("Done.");
-  }
-
-  return 0;
-}
diff --git a/theory/MovingMesh/ShadowSWIFT/implementation-details.tex b/theory/MovingMesh/ShadowSWIFT/implementation-details.tex
new file mode 100644
index 0000000000000000000000000000000000000000..9d970d6d5a7bdf0618e6a8c5581edd8f8d97430c
--- /dev/null
+++ b/theory/MovingMesh/ShadowSWIFT/implementation-details.tex
@@ -0,0 +1,39 @@
+\include{./header.tex}  % should be present in directory where build command is invoked
+
+
+
+%------------------------------------------
+%: Metadata
+%------------------------------------------
+
+\title{ShadowSWIFT: Moving mesh hydrodynamics in SWIFT}
+\subtitle{(Implementation details)}
+\author{Yolan Uyttenhove (Yolan.Uyttenhove@UGent.be)}
+\date{}
+
+%------------------------------------------
+
+
+
+
+
+
+\begin{document}
+	
+%--------------------------------------------
+% Stuff that needs to be done before all else
+%--------------------------------------------
+%\pagestyle{plain}
+%\nocite{*} % show all entries of bibliography, even if they are not cited.
+
+
+
+%Titlepage
+\maketitle
+
+
+
+
+\textbf{TODO}
+
+\end{document}
\ No newline at end of file
diff --git a/theory/MovingMesh/VoronoiConstruction/construction-level.tex b/theory/MovingMesh/VoronoiConstruction/construction-level.tex
new file mode 100644
index 0000000000000000000000000000000000000000..d13dbc1c32497da47ed6a982c83b55b100b8628e
--- /dev/null
+++ b/theory/MovingMesh/VoronoiConstruction/construction-level.tex
@@ -0,0 +1,61 @@
+
+
+\section{Setting the construction level for the Voronoi grid}
+
+Like (almost) everything, the construction of the Voronoi grid in SWIFT happens at the granularity of SWIFT cells. 
+No global Voronoi grid is constructed, but parts of the Voronoi grid are constructed independently for SWIFT cells ensuring that neighbouring cells' Voronoi grids mesh together properly.
+
+The chosen construction algorithm, stores the grid information in a cell-global data structure (earlier attempts storing the grid information in the particles resulted in an unusably large memory overhead and very slow performance). 
+This means that the Voronoi grid must be stored at some unique level in the AMR tree of SWIFT cells at every location.
+Special care must be taken when selecting this level. The following constraints \emph{must} be fulfilled:
+\begin{enumerate}
+    \item The cell has neighbouring cells on the same level in every direction
+    \item The part of the Voronoi grid of the SWIFT cell only depends on the locations of the particles of the SWIFT cell and those of its neighbours.
+\end{enumerate}
+
+Specifically, these constraints are needed to be able to identify the neighbouring particles of faces between a local particle of a cell and a particle of a neighbouring cell, which we will call \emph{ghost particles}, e.g. when exchanging fluxes. While this can be achieved using pointers on a single node application, when running SWIFT on multiple nodes over MPI, these pointers would no longer be valid when they are sent to the other node, so we instead store the \texttt{sid} of the neighbouring cell and the index of the ghost particle in that neighbouring cell in the faces.
+
+Besides those constraints, we also try to balance the following opposing effects:
+\begin{itemize}
+    \item We want to construct the parts of the Voronoi grid at a low enough level to achieve fine grained tasking and good load balancing
+    \item Constructing the Voronoi grid in very small parts (i.e. at a very low level in the AMR tree) increases overhead, as faces between neighbouring SWIFT cells (boundary faces) are constructed twice (once for the part of the Voronoi grid of each cell). The fewer particles there are in the SWIFT cells, the more boundary faces relative to internal faces. 
+\end{itemize}
+
+\subsection{The definition of completeness}
+We define a SWIFT cell as \emph{complete} if it satisfies both of the above constraints. 
+It is intuitively clear that if there are not enough particles in the cells, it could happen that information from particles two cells over might be needed to correctly construct the Voronoi grid of a SWIFT cell. Figure \ref{fig:problem-completeness} shows an example in 2D where the second constrained is not fulfilled.
+It can be proven that a sufficient condition for completeness in three dimensions, and for cubic cells, is that a SWIFT cell and its neighbours all have at least one particle in every $1/27^\text{th}$ sub-cube obtained by dividing the cell in three along every direction.
+
+\begin{figure}
+    \centering
+    \input{VoronoiConstruction/figures/problem.tex}
+    \caption{2D example of a problematic configuration arising from SWIFT cells containing too few particles. The dual Delaunay tessellation of the Voronoi grid is drawn. Any particle inside the red circle could influence the Voronoi cell of particle A in the blue SWIFT cell, even particle D, which is not part of a directly neighbouring SWIFT cell of the blue cell, violating the second constraint.}
+    \label{fig:problem-completeness}
+\end{figure}
+
+To determine the completeness of every cell in the AMR tree, we first set the \emph{self-completeness} for every cell. This is just whether the cell itself has at least one particle in every $1/27^\text{th}$ sub-cube. This is done recursively: once all 8 subcells of a SWIFT cell are self-complete, the cell itself is also self complete.
+The self-completeness flag should be invalidated when drifting the particles.
+
+Once the self-completeness flags have been set, and communicated over MPI if necessary, each cell's completeness (\texttt{true} or \texttt{false}) is initialized according to its self-completeness flag. We then recursively check all pairs of neighbouring cells and invalidate the completeness flag of a cell as soon as one of its neighbours is not self-complete. Note that technically, the cell might in theory still be complete at this point (i.e. only depend on particles from its neighbouring cells), but there is no easy way to check this in practice, so we impose this slightly more stringent constraint.
+
+If the \texttt{SHADOWSWIFT\_RELAXED\_COMPLETENESS} directive is defined, we use a slightly more relaxed condition. We only invalidate the completeness of a SWIFT cell \texttt{ci}, if it has a neighbouring cell \texttt{cj} that is not self-complete \emph{and} the maximal search radius of any particle in \texttt{ci} is smaller than half the width of \texttt{cj}, indicating there are still enough particles close-by to completely determine \texttt{ci}'s Voronoi grid and consider the pair (\texttt{ci}, \texttt{cj}) complete.
+
+
+\subsection{Setting the construction level based on completeness} \label{sec:construction-level}
+
+Once the completeness is set for every swift cell in the AMR tree, the \emph{construction level}, i.e. the level at which the Voronoi grid will be stored and constructed, can be decided. This is again done recursively. Starting from the top level cells, we recurse down and set the construction level at the the first level where one of the following conditions is true:
+\begin{enumerate}
+    \item The current cell does not have subcells
+    \item The current cell does not have 8 complete subcells
+    \item The current cell has fewer than \texttt{grid\_split\_threshold} particles
+\end{enumerate} 
+Cells below the construction level store a pointer to their parent cell on the construction level, cells above the construction level store \texttt{NULL} (similar to how the \texttt{super} levels work, but note that the construction level is decided \emph{before} any tasks are constructed).
+Ideally, to have the most flexibility when setting the construction level, \texttt{grid\_split\_threshold} should be larger than \texttt{cell\_split\_size} and the latter should be set to a pretty small value (e.g. 40 instead of the default 400). An example of the resulting configuration of construction levels is shown in Figure \ref{fig:construction-level}
+
+\begin{figure}
+    \centering
+    \input{VoronoiConstruction/figures/construction_level}
+    \caption{Example configuration.  Thick colored borders indicate a level on which the Voronoi grid will be constructed and the appropriately colored shaded areas depict the neighbouring used for construction. Cells always and only use information of neighbouring cells on the same level in the AMR tree (diagonal dependencies have been omitted for clarity). Grid construction may happen at any level in the AMR tree depending on the conditions discussed in \S\ref{sec:construction-level}.}
+    \label{fig:construction-level}
+\end{figure}
+
diff --git a/theory/MovingMesh/VoronoiConstruction/current-status.tex b/theory/MovingMesh/VoronoiConstruction/current-status.tex
new file mode 100644
index 0000000000000000000000000000000000000000..ab81bbc7e2e785a095c7a71644c0b036deec93b2
--- /dev/null
+++ b/theory/MovingMesh/VoronoiConstruction/current-status.tex
@@ -0,0 +1,13 @@
+\section{Current status (merging in master)}
+The following is implemented/being merged:
+\begin{itemize}
+    \item Setting completeness of cells
+    \item Communicating completeness over MPI
+    \item Setting Construction level
+\end{itemize}
+The following is not yet being merged:
+\begin{itemize}
+    \item Invalidating completeness on drifting
+    \item Creating and scheduling tasks for grid construction
+    \item Implementation of construction tasks
+\end{itemize}
\ No newline at end of file
diff --git a/theory/MovingMesh/VoronoiConstruction/figures/construction_level.tex b/theory/MovingMesh/VoronoiConstruction/figures/construction_level.tex
new file mode 100644
index 0000000000000000000000000000000000000000..6a6c39d8135cdad5527c9c2cf90bd36be4b4cdba
--- /dev/null
+++ b/theory/MovingMesh/VoronoiConstruction/figures/construction_level.tex
@@ -0,0 +1,45 @@
+\begin{tikzpicture}
+    % Shaded areas
+    \fill[violet, opacity=0.05] (4, 4) rectangle (8, 8) (0, 0) rectangle (4, 4) (8, 0) rectangle (12, 4);
+    \shade[top color=violet,bottom color=white, opacity=0.05] (4, 0) rectangle (8, -1);
+    \fill[orange, opacity=0.1] (8, 2) rectangle (10, 4) (10, 4) rectangle (12, 6) (6, 4) rectangle (8, 6) (8, 6) rectangle (10, 8);
+    \fill[teal, opacity=0.1] (9, 1) rectangle (10, 2) (10, 2) rectangle (11, 3) (10, 0) rectangle (11, 1) (11, 1) rectangle (12, 2);
+    
+    % Grid
+    \draw[line width=1pt] (0,0) grid[step=4] (12,8);
+    \draw[dashed, line width=0.5pt] (4.001,0.0001) grid[step=2] (8.999,7.999);
+    \draw[line width=1pt] (8.001,0.001) grid[step=2] (9.999,7.999);
+    \draw[dashed, line width=0.5pt] (8.001,0.001) grid[step=1] (9.999,7.999);
+    \draw[line width=1pt] (10.001,0.001) grid[step=1] (11.999,7.999);
+    
+    % Arrows
+    \draw[line width=1.5pt, violet] (4, 0) grid[step=4] (8, 4);
+    \draw[violet, ->, line width=1.5pt] (2, 2) to[bend left=15] (5.9, 2);
+    \draw[violet, ->, line width=1.5pt] (10, 2) to[bend right=15] (6.1, 2);
+    \draw[violet, ->, line width=1.5pt] (6, 6) to[bend left=15] (6, 2.1);
+    \draw[violet, ->, line width=1.5pt] (6, -1) to[bend right=15] (6, 1.9);
+
+    \draw[line width=1.5pt, orange] (8, 4) grid[step=2] (10, 6);
+    \draw[orange, ->, line width=1.5pt] (7, 5) to[bend left=15] (8.95, 5);
+    \draw[orange, ->, line width=1.5pt] (9, 7) to[bend left=15] (9, 5.05);
+    \draw[orange, ->, line width=1.5pt] (11, 5) to[bend right=15] (9.05, 5);
+    \draw[orange, ->, line width=1.5pt] (9, 3) to[bend right=15] (9, 4.95);
+    
+    \draw[line width=1.5pt, teal] (10, 1) grid[step=1] (11, 2);
+    \draw[teal, ->, line width=1.25pt] (9.5, 1.5) to[bend left=15] (10.45, 1.5);
+    \draw[teal, <-, line width=1.25pt] (10.5, 1.55) to[bend right=15] (10.5, 2.5);
+    \draw[teal, <-, line width=1.25pt] (10.55, 1.5) to[bend left=15] (11.5, 1.5);
+    \draw[teal, <-, line width=1.25pt] (10.5, 1.45) to[bend left=15] (10.5, 0.5);
+    
+    % Fadings
+    \draw[line width=1pt, path fading=south] (0, -1) grid[step=4] (12, 0);
+    \draw[dashed, line width=0.5pt, path fading=south] (4.1, -1) grid[step=2] (7.9, 0);
+    \draw[dashed, line width=0.5pt, path fading=south] (8.1, -0.95) grid[step=1] (9.9, 0);
+    \draw[line width=1pt, path fading=south] (10, -0.95) grid[step=1] (11.9, 0);
+    \draw[black, line width=1pt, path fading=east] (12, 0) grid[step=1] (12.8, 8);
+    \draw[black, line width=1pt, path fading=north] (0, 8) grid[step=4] (12, 9);
+    \draw[dashed, line width=0.5pt, path fading=north] (4.1, 8) grid[step=2] (7.9, 9);
+    \draw[dashed, line width=0.5pt, path fading=north] (8.1, 8) grid[step=1] (9.9, 8.95);
+    \draw[line width=1pt, path fading=north] (10, 8) grid[step=1] (11.9, 8.95);
+    \draw[black, line width=1pt, path fading=west] (-1, 0) grid[step=4] (0, 8);
+\end{tikzpicture}
\ No newline at end of file
diff --git a/theory/MovingMesh/VoronoiConstruction/figures/problem.tex b/theory/MovingMesh/VoronoiConstruction/figures/problem.tex
new file mode 100644
index 0000000000000000000000000000000000000000..397c71548ee6892cb07aeb78d50d5a143bba6410
--- /dev/null
+++ b/theory/MovingMesh/VoronoiConstruction/figures/problem.tex
@@ -0,0 +1,22 @@
+\begin{tikzpicture}[scale=3]
+    \draw[step=1.0,black,thin] (0,0) grid (4,3);
+    \draw[line width=1pt, blue] (1,1) -- (2,1) -- (2,2) -- (1,2) -- (1,1);
+    \path coordinate (a) at (1.9, 1.9)
+          coordinate (b) at (2.5, 1.1)
+          coordinate (c) at (2.5, 2.7)
+          coordinate (d) at (2.6,0.35)
+          coordinate (e) at (1.5,0.4)
+          coordinate (f) at (0.3,0.3)
+          coordinate (g) at (0.5,1.5)
+          coordinate (h) at (0.7,2.3)
+          coordinate (i) at (1.5,2.8);
+    \fill [radius=0.75pt] (a) circle[] node[left=1pt] {A} (b) circle[] node[left=1pt] {B} (c) circle[] node[left=1pt] {C} (d) circle[] (e) circle[] (f) circle[] (g) circle[] (h) circle[] (i) circle;
+    \node[circle through 3 points={a}{b}{c},draw=red]{};
+    \draw [black, line width=1pt] (a) -- (b) -- (c) -- (a);
+    \draw [gray] (b) -- (d) -- (e) -- (b);
+    \draw [gray] (b) -- (d) -- (e) -- (b);
+    \draw [gray] (a) -- (e) -- (g) -- (f) -- (e);
+    \draw [gray] (a) -- (g) -- (h) -- (a);
+    \draw [gray] (h) -- (i) -- (a);
+    \draw [gray] (i) -- (c);
+\end{tikzpicture}
\ No newline at end of file
diff --git a/theory/MovingMesh/VoronoiConstruction/implementation-details.tex b/theory/MovingMesh/VoronoiConstruction/implementation-details.tex
new file mode 100644
index 0000000000000000000000000000000000000000..121851ca1ea4c5a94a33f855ae13485f712a6771
--- /dev/null
+++ b/theory/MovingMesh/VoronoiConstruction/implementation-details.tex
@@ -0,0 +1,48 @@
+\include{./header.tex}
+
+
+
+%------------------------------------------
+%: Metadata
+%------------------------------------------
+
+\title{Voronoi grid construction in SWIFT}
+\subtitle{(Implementation details)}
+\author{Yolan Uyttenhove (Yolan.Uyttenhove@UGent.be)}
+\date{}
+
+%------------------------------------------
+
+
+
+
+
+
+\newcommand{\Aij}{$\mathbf{A}_{ij}$}
+\newcommand{\Aijm}{\mathbf{A}_{ij}}		% A_ij math
+\newcommand{\U}{\mathbf{U}}
+\newcommand{\F}{\mathbf{F}}
+\newcommand{\psitilde}{\tilde{\boldsymbol{\psi}}}
+
+
+
+
+
+\begin{document}
+	
+%--------------------------------------------
+% Stuff that needs to be done before all else
+%--------------------------------------------
+%\pagestyle{plain}
+%\nocite{*} % show all entries of bibliography, even if they are not cited.
+
+
+
+%Titlepage
+\maketitle
+
+
+\input{VoronoiConstruction/construction-level}
+\input{VoronoiConstruction/current-status}
+
+\end{document}
\ No newline at end of file
diff --git a/theory/MovingMesh/header.tex b/theory/MovingMesh/header.tex
new file mode 100644
index 0000000000000000000000000000000000000000..00ffbf839800a26532ac5afe324b3b0fc960ea0c
--- /dev/null
+++ b/theory/MovingMesh/header.tex
@@ -0,0 +1,127 @@
+\documentclass[12pt, a4paper, english, singlespacing, parskip]{scrartcl}
+
+%\documentclass[
+%11pt, 				% The default document font size, options: 10pt, 11pt, 12pt
+%oneside, 			% Two side (alternating margins) for binding by default, uncomment to switch to one side
+%chapterinoneline,	% Have the chapter title next to the number in one single line
+%english, 			% ngerman for German
+%singlespacing, 	% Single line spacing, alternatives: onehalfspacing or doublespacing
+%draft, 			% Uncomment to enable draft mode (no pictures, no links, overfull hboxes indicated)
+%nolistspacing, 	% If the document is onehalfspacing or doublespacing, uncomment this to set spacing in lists to single
+%liststotoc, 		% Uncomment to add the list of figures/tables/etc to the table of contents
+%toctotoc, 			% Uncomment to add the main table of contents to the table of contents
+%parskip, 			% Uncomment to add space between paragraphs
+%nohyperref, 		% Uncomment to not load the hyperref package
+%headsepline, 		% Uncomment to get a line under the header
+%]{scrartcl or scrreprt or scrbook} % The class file specifying the document structure
+
+\usepackage{lmodern} 		% Diese beiden packages sorgen für echte 
+\usepackage[T1]{fontenc}	% Umlaute.
+
+\usepackage{amssymb, amsmath, color, graphicx, float, setspace, tipa}
+\usepackage[utf8]{inputenc} 
+\usepackage[english]{babel}
+\usepackage[pdfpagelabels,
+        pdfstartview = FitH,
+        bookmarksopen = true,
+        bookmarksnumbered = true,
+        linkcolor = black,
+        plainpages = false,
+        hypertexnames = false,
+        citecolor = black, 
+        breaklinks]{hyperref}
+\usepackage{url}
+\usepackage{tikz}
+\usetikzlibrary{3d,arrows,calc,fadings,through,fit,shapes.geometric}
+\tikzset{circle through 3 points/.style n args={3}{%
+insert path={let    \p1=($(#1)!0.5!(#2)$),
+                    \p2=($(#1)!0.5!(#3)$),
+                    \p3=($(#1)!0.5!(#2)!1!-90:(#2)$),
+                    \p4=($(#1)!0.5!(#3)!1!90:(#3)$),
+                    \p5=(intersection of \p1--\p3 and \p2--\p4)
+                    in },
+at={(\p5)},
+circle through= {(#1)}
+}}
+
+\usepackage{authblk} 		% titlepage stuff
+\usepackage[titletoc, title]{appendix}
+
+%===================
+% BIBLIOGRAPHY
+%===================
+\usepackage[]{natbib}
+\bibliographystyle{unsrtnat}
+%DONT FORGET TO COMPILE THE BIBLIOGRAPHY WITH BIBTEX WHEN CHANGES ARE MADE.
+
+
+
+
+%--------------------------------------------
+% NEW COMMANDS
+%--------------------------------------------
+
+\newcommand{\corresponds}{\mathrel{\widehat{=}}}		% equals with hat
+
+\newcommand {\arctanh}{\mathrm{arctanh}}				% Atanh
+\newcommand{\arccot}{\mathrm{arccot }}					% Acotanh
+
+\newcommand{\limz}[1]{\lim\limits_{#1 \rightarrow 0}}	% Limes of something towars zero
+
+\newcommand{\bm}{\boldmath}								% Bold font in math
+\newcommand{\dps}{\displaystyle}						
+
+\newcommand{\e}{\mbox{e}}								% e noncursive in math mode
+
+\newcommand{\del}{\partial}								% partial diff operator
+\newcommand{\de}{\mathrm{d}}							% differential d
+\newcommand{\D}{\mathrm{d}}								% differential d
+\newcommand{\GRAD}{\mathrm{grad}\ }						% gradient
+\newcommand{\DIV}{\mathrm{div}\ }						% divergence
+\newcommand{\ROT}{\mathrm{rot}\ }						% rotation
+
+\newcommand{\CONST}{\mathrm{const.\ }}					% constant
+\newcommand{\var}{\mathrm{var}}							% variance
+
+\newcommand{\g}{^\circ}									% degrees
+\newcommand{\degr}{^\circ}								% degrees
+
+\newcommand{\msol}{M_\odot}								% solar mass
+
+
+\newcommand{\x}{\mathbf{x}}								% x vector
+\newcommand{\xdot}{\dot{\mathbf{x}}}					% x dot vector
+\newcommand{\xddot}{\ddot{\mathbf{x}}}					% x doubledot vector
+\newcommand{\R}{\mathbf{r}}								% r vector
+\newcommand{\rdot}{\dot{\mathbf{r}}}					% r dot vector
+\newcommand{\rddot}{\ddot{\mathbf{r}}}					% r doubledot vector
+\newcommand{\vel}{\mathbf{v}}							% v vector
+\newcommand{\V}{\mathbf{v}}								% v vector
+\newcommand{\vdot}{\dot{\mathbf{v}}}					% v dot vector
+\newcommand{\vddot}{\ddot{\mathbf{v}}}					% v doubledot vector
+
+\newcommand{\dete}{\mathrm{d}t}							% dt
+\newcommand{\delte}{\del t}								% partial t
+\newcommand{\dex}{\mathrm{d}x}							% dx
+\newcommand{\delx}{\del x}								% partial x
+\newcommand{\der}{\mathrm{d}r}							% dr
+\newcommand{\delr}{\del r}								% partial r
+
+\newcommand{\deldt}{\frac{\del}{\del t}}				% shortcut partial derivative, in line
+\newcommand{\ddt}{\frac{\de}{\de t}}					% shortcut total derivative, in line
+\newcommand{\DELDT}[1]{\frac{\del  #1}{\del t}}			% shortcut partial derivative, on fraction
+\newcommand{\DDT}[1]{\frac{\de  #1}{\de t}}				% shortcut total derivative, on fraction
+
+\newcommand{\deldx}{\frac{\del}{\del x}}				% shortcut partial derivative, in line
+\newcommand{\ddx}{\frac{\de}{\de x}}					% shortcut total derivative, in line
+\newcommand{\DELDX}[1]{\frac{\del  #1}{\del x}}			% shortcut partial derivative, on fraction
+\newcommand{\DDX}[1]{\frac{\de  #1}{\de x}}				% shortcut total derivative, on fraction
+
+\newcommand{\deldr}{\frac{\del}{\del r}}				% shortcut partial derivative, in line
+\newcommand{\ddr}{\frac{\de}{\de r}}					% shortcut total derivative, in line
+\newcommand{\DELDR}[1]{\frac{\del  #1}{\del r}}			% shortcut partial derivative, on fraction
+\newcommand{\DDR}[1]{\frac{\de  #1}{\de r}}				% shortcut total derivative, on fraction
+
+% replace \sum with \sum\limits
+\let\oldsum\sum
+\renewcommand{\sum}{\oldsum\limits}
diff --git a/theory/MovingMesh/references.bib b/theory/MovingMesh/references.bib
new file mode 100644
index 0000000000000000000000000000000000000000..fd40910d9e70d6412e5e9919bb62a2d649c27a7c
--- /dev/null
+++ b/theory/MovingMesh/references.bib
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/theory/Multipoles/potential_softening.tex b/theory/Multipoles/potential_softening.tex
index 47fe4f392bf9bd11487f270421fba3d0dff69af0..03240097ed7aea21edb35f5c9b4827fb083b90fd 100644
--- a/theory/Multipoles/potential_softening.tex
+++ b/theory/Multipoles/potential_softening.tex
@@ -67,7 +67,20 @@ r^{-2} & \mbox{if} & r \geq H.
 The softened density profile, its corresponding potential and
 resulting forces are shown on Fig. \ref{fig:fmm:softening} (for more
 details about how these are constructed see section 2
-of~\cite{Price2007}). For comparison purposes, we also implemented the
+of~\cite{Price2007}). Finally, we also compute the change in potential
+due to a change in softening length:
+\begin{align}
+\frac{\partial}{\partial H}\varphi(r,H) = 
+\left\lbrace\begin{array}{rcl}
+k(\frac{r}{H}) \times H^{-2} & \mbox{if} & r < H,\\
+0 & \mbox{if} & r \geq H.
+\end{array}
+\right.
+\label{eq:fmm:potential_h_derivative}
+\end{align}
+where $k(u)=-24u^7+105u^6-168u^5+105u^4-21u^2$. This term enters the
+equation of motion when adaptive softening is used for SPH
+\citep[e.g.][]{Price2007}. For comparison purposes, we also implemented the
 more traditional spline-kernel softening in \swift.
 \begin{figure}
 \includegraphics[width=\columnwidth]{potential.pdf}
diff --git a/theory/SinkParticles/ar-1col-S2O.cls b/theory/SinkParticles/ar-1col-S2O.cls
new file mode 100644
index 0000000000000000000000000000000000000000..c3279597d149e03a4d54972a33bab0638c7beca3
--- /dev/null
+++ b/theory/SinkParticles/ar-1col-S2O.cls
@@ -0,0 +1,1162 @@
+%% ar.cls - version 1.1 
+%% Aptara Inc., dated 07 Mar. 2014
+%%
+%% Version 1.1 (History)
+%% ---------------------
+%% 1) Implemented the color.sty to avoide the  
+%%    color related problems.
+%% 2) Used the ifpdf.sty for providing the PDF Paper
+%%    Size in case of pdflatex.
+%% 3) Seperation between author and year removed for Harvard style reference (unnumbered)
+%% 4) Removed the clashing of marginnote environment with amsmath.sty 
+%% 5) Introduced an option to change format of Equation Number and a command ``\firstpagenote''to appear text on first page
+%% 
+%% For AR journals
+%%
+%% Steps to compile: latex latex latex
+%%
+%% \CharacterTable
+%%  {Upper-case    \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z
+%%   Lower-case    \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z
+%%   Digits        \0\1\2\3\4\5\6\7\8\9
+%%   Exclamation   \!     Double quote  \"     Hash (number) \#
+%%   Dollar        \$     Percent       \%     Ampersand     \&
+%%   Acute accent  \'     Left paren    \(     Right paren   \)
+%%   Asterisk      \*     Plus          \+     Comma         \,
+%%   Minus         \-     Point         \.     Solidus       \/
+%%   Colon         \:     Semicolon     \;     Less than     \<
+%%   Equals        \=     Greater than  \>     Question mark \?
+%%   Commercial at \@     Left bracket  \[     Backslash     \\
+%%   Right bracket \]     Circumflex    \^     Underscore    \_
+%%   Grave accent  \`     Left brace    \{     Vertical bar  \|
+%%   Right brace   \}     Tilde         \~}
+\NeedsTeXFormat{LaTeX2e}[1995/12/01]
+\ProvidesClass{styles/ar-1col-S2O}[2013/03/07 v1.1 Standard LaTeX document class]
+%
+\newif\if@restonecol
+\newif\if@DotinEqNum\global\@DotinEqNumtrue
+%
+% Global Variable Declaration
+%
+\def\doi#1{\gdef\@doi{#1}}\doi{}%
+\def\fstpage#1{\gdef\@fstpage{#1}}\fstpage{}%
+\def\endpage#1{\gdef\@endpage{#1}}\endpage{}%
+\def\jvol#1{\gdef\@jvol{#1}}\jvol{00}%
+\def\jyear#1{\gdef\@jyear{#1}}\jyear{0000}%
+\def\jname#1{\gdef\@jname{#1}}\jname{xxxxxx}%
+%
+% Font size declaration
+%
+% \def\rhfont{\fontsize{7.5}{7.5}\itshape\selectfont}
+% \def\titlefont{\fontsize{20}{24}\selectfont\bfseries\leftskip14.25pc\rightskip0\p@ plus1fill}
+% \def\authorfont{\fontsize{12}{15}\selectfont\bfseries\leftskip14.25pc\rightskip0\p@ plus1fill\mathversion{bold}}
+% \def\abstractfont{\fontsize{9}{12}\selectfont}
+% \def\keywordsfont{\fontsize{9}{12}\selectfont\rightskip0\p@ plus1fill\mathversion{normal}}
+% \def\affilfont{\fontsize{7}{9}\selectfont\leftskip14.25pc\rightskip0\p@ plus1fil}
+% \def\jinfofont{\fontsize{7}{9}\selectfont\rightskip12\p@ plus1fil}
+% \def\foliofont{\fontsize{7.5}{7.5}\itshape\selectfont}
+% \def\figcaptionfont{\fontsize{8}{10}\selectfont\mathversion{normal}}
+% \def\figcaptionnumfont{\sffamily\fontsize{8}{10}\selectfont\bfseries\mathversion{bold}}
+% \def\tablecaptionfont{\fontsize{8}{12}\bfseries\selectfont\mathversion{normal}}% 
+% \def\tablecaptionnumfont{\fontsize{8}{12}\bfseries\selectfont}
+% \def\tablefont{\fontsize{8}{11}\selectfont\mathversion{normal}}%
+% \def\tabnotefont{\fontsize{7}{10}\selectfont}%
+% \def\textboxfont{\normalfont\normalsize\leftskip12\p@\rightskip12\p@}
+% \def\textboxheadfont{\fontsize{10}{12}\selectfont\bfseries\leftskip12\p@\rightskip12\p@ plus1fill\mathversion{normal}}
+% %
+% \def\sectionfont{\sffamily\fontsize{10}{12}\selectfont\bfseries\raggedright\mathversion{bold}}
+% \def\subsectionfont{\sffamily\fontsize{10}{12}\selectfont\bfseries\raggedright\mathversion{bold}}
+% \def\subsubsectionfont{\sffamily\fontsize{9}{12}\bfseries\selectfont\mathversion{bold}}
+% \def\paragraphfont{\bfseries\itshape}
+% \def\subparagraphfont{\itshape}
+% \def\extractfont{\fontsize{8}{12}\selectfont\mathversion{normal}}
+% \def\marginnotefont{\fontsize{8}{10}\selectfont\rightskip0\p@ plus1fill\mathversion{normal}}
+% \def\bibmarginnotefont{\fontsize{7}{10}\selectfont\bfseries\rightskip0\p@ plus1fill\mathversion{normal}}
+% \def\bibliofont{\fontsize{8}{11}\selectfont\mathversion{normal}}
+\def\rhfont{\fontsize{7.5}{7.5}\itshape\selectfont}
+\def\titlefont{\fontsize{21}{25}\selectfont\bfseries\leftskip14.25pc\rightskip0\p@ plus1fill}
+\def\authorfont{\fontsize{12}{15}\selectfont\bfseries\leftskip14.25pc\rightskip0\p@ plus1fill\mathversion{bold}}
+\def\abstractfont{\fontsize{10}{12}\selectfont}
+\def\keywordsfont{\fontsize{10}{12}\selectfont\rightskip0\p@ plus1fill\mathversion{normal}}
+\def\affilfont{\fontsize{8}{10}\selectfont\leftskip14.25pc\rightskip0\p@ plus1fil}
+\def\jinfofont{\fontsize{8}{10}\selectfont\rightskip12\p@ plus1fil}
+\def\foliofont{\fontsize{8.5}{8.5}\itshape\selectfont}
+\def\figcaptionfont{\fontsize{9}{10.5}\selectfont\mathversion{normal}}
+\def\figcaptionnumfont{\sffamily\fontsize{9}{10.5}\selectfont\bfseries\mathversion{bold}}
+\def\tablecaptionfont{\fontsize{9}{12}\bfseries\selectfont\mathversion{normal}}% 
+\def\tablecaptionnumfont{\fontsize{9}{12}\bfseries\selectfont}
+\def\tablefont{\fontsize{9}{11.5}\selectfont\mathversion{normal}}%
+\def\tabnotefont{\fontsize{8}{10.5}\selectfont}%
+\def\textboxfont{\normalfont\normalsize\leftskip12\p@\rightskip12\p@}
+\def\textboxheadfont{\fontsize{11}{13}\selectfont\bfseries\leftskip12\p@\rightskip12\p@ plus1fill\mathversion{normal}}
+%
+\def\sectionfont{\sffamily\fontsize{11}{13}\selectfont\bfseries\raggedright\mathversion{bold}}
+\def\subsectionfont{\sffamily\fontsize{11}{13}\selectfont\bfseries\raggedright\mathversion{bold}}
+\def\subsubsectionfont{\sffamily\fontsize{10}{13}\bfseries\selectfont\mathversion{bold}}
+\def\paragraphfont{\bfseries\itshape}
+\def\subparagraphfont{\itshape}
+\def\extractfont{\fontsize{8.5}{12.5}\selectfont\mathversion{normal}}
+\def\marginnotefont{\fontsize{8.5}{10.5}\selectfont\rightskip0\p@ plus1fill\mathversion{normal}}
+\def\bibmarginnotefont{\fontsize{7.5}{10.5}\selectfont\bfseries\rightskip0\p@ plus1fill\mathversion{normal}}
+\def\bibliofont{\fontsize{8.5}{11.5}\selectfont\mathversion{normal}}
+%
+%
+%% \if@compatibility\else
+\DeclareOption{a4paper}
+   {\setlength\paperheight {297mm}%
+    \setlength\paperwidth  {210mm}}
+\DeclareOption{a5paper}
+   {\setlength\paperheight {210mm}%
+    \setlength\paperwidth  {148mm}}
+\DeclareOption{b5paper}
+   {\setlength\paperheight {250mm}%
+    \setlength\paperwidth  {176mm}}
+\DeclareOption{letterpaper}
+   {\setlength\paperheight {11in}%
+    \setlength\paperwidth  {8.5in}}
+\DeclareOption{legalpaper}
+   {\setlength\paperheight {14in}%
+    \setlength\paperwidth  {8.5in}}
+\DeclareOption{executivepaper}
+   {\setlength\paperheight {10.5in}%
+    \setlength\paperwidth  {7.25in}}
+\DeclareOption{landscape}
+   {\setlength\@tempdima   {\paperheight}%
+    \setlength\paperheight {\paperwidth}%
+    \setlength\paperwidth  {\@tempdima}}
+\DeclareOption{fleqn}{\input{fleqn.clo}}
+\DeclareOption{ChEqNum}{\global\@DotinEqNumtrue}
+\ExecuteOptions{letterpaper}
+\ProcessOptions
+%
+\@twosidetrue\@mparswitchtrue
+%
+\renewcommand\normalsize{%
+  % \@setfontsize\normalsize\@ixpt\@xiipt
+  \@setfontsize\normalsize{10}{13}
+   \abovedisplayskip 10\p@ \@plus2\p@ \@minus5\p@
+   \abovedisplayshortskip \z@ \@plus3\p@
+   \belowdisplayshortskip 6\p@ \@plus3\p@ \@minus3\p@
+   \belowdisplayskip \abovedisplayskip
+   \let\@listi\@listI}
+\normalsize
+\newcommand\small{%
+  % \@setfontsize\small\@ixpt{11}%
+  \@setfontsize\small\@ixpt{12}%
+   \abovedisplayskip 8.5\p@ \@plus3\p@ \@minus4\p@
+   \abovedisplayshortskip \z@ \@plus2\p@
+   \belowdisplayshortskip 4\p@ \@plus2\p@ \@minus2\p@
+   \def\@listi{\leftmargin\leftmargini
+               \topsep 4\p@ \@plus2\p@ \@minus2\p@
+               \parsep 2\p@ \@plus\p@ \@minus\p@
+               \itemsep \parsep}%
+   \belowdisplayskip \abovedisplayskip
+}
+\newcommand\footnotesize{%
+  % \@setfontsize\footnotesize\@viiipt{9.5}%
+  \@setfontsize\footnotesize\@ixpt{10.5}%
+   \abovedisplayskip 6\p@ \@plus2\p@ \@minus4\p@
+   \abovedisplayshortskip \z@ \@plus\p@
+   \belowdisplayshortskip 3\p@ \@plus\p@ \@minus2\p@
+   \def\@listi{\leftmargin\leftmargini
+               \topsep 3\p@ \@plus\p@ \@minus\p@
+               \parsep 2\p@ \@plus\p@ \@minus\p@
+               \itemsep \parsep}%
+   \belowdisplayskip \abovedisplayskip
+}
+% \newcommand\scriptsize{\@setfontsize\scriptsize\@viipt\@viiipt}
+% \newcommand\tiny{\@setfontsize\tiny\@vpt\@vipt}
+% \newcommand\large{\@setfontsize\large\@xiipt{14}}
+% \newcommand\Large{\@setfontsize\Large\@xivpt{18}}
+% \newcommand\LARGE{\@setfontsize\LARGE\@xviipt{22}}
+% \newcommand\huge{\@setfontsize\huge\@xxpt{25}}
+% \newcommand\Huge{\@setfontsize\Huge\@xxvpt{30}}
+\newcommand\scriptsize{\@setfontsize\scriptsize\@viiipt\@ixpt}
+\newcommand\tiny{\@setfontsize\tiny\@vipt\@viipt}
+\newcommand\large{\@setfontsize\large{13}{15}}
+\newcommand\Large{\@setfontsize\Large{15.4}{19}}
+\newcommand\LARGE{\@setfontsize\LARGE\@xviipt{22}}
+\newcommand\huge{\@setfontsize\huge\@xxpt{25}}
+\newcommand\Huge{\@setfontsize\Huge\@xxvpt{30}}
+\setlength\parindent{15\p@}
+\setlength\smallskipamount{3\p@ \@plus 1\p@ \@minus 1\p@}
+\setlength\medskipamount{6\p@ \@plus 2\p@ \@minus 2\p@}
+\setlength\bigskipamount{12\p@ \@plus 4\p@ \@minus 4\p@}
+\setlength\headheight{\z@}%
+\setlength\headsep{\z@}%
+\setlength\topskip{6.9\p@}
+\setlength\footskip{24\p@}
+\setlength\maxdepth{.5\topskip}
+%
+\newdimen\typeheight\typeheight47pc%
+\newdimen\typewidth\typewidth38pc%
+% \newdimen\typewidth\typewidth39pc%
+\newlength\extramargin
+\setlength\extramargin{5.5pc}
+\setlength\textheight{59pc}%
+\setlength\textwidth{32.5pc}%
+\setlength\marginparsep {10\p@}
+\setlength\marginparpush{5\p@}
+\setlength\marginparwidth {2.0cm}
+% \setlength\oddsidemargin{46.5\p@}%
+% \setlength\evensidemargin{28.5\p@}%
+\setlength\oddsidemargin{29.5\p@}%
+\setlength\evensidemargin{28.5\p@}%
+
+%
+% \setlength\topmargin{15.5\p@}%
+\setlength\topmargin{0cm}%
+\setlength\footnotesep{7.35\p@}
+\setlength{\skip\footins}{19.5\p@ \@plus 4\p@ \@minus 2\p@}
+\setlength\floatsep    {12\p@ \@plus 2\p@ \@minus 2\p@}
+\setlength\textfloatsep{20\p@ \@plus 2\p@ \@minus 4\p@}
+\setlength\intextsep   {12\p@ \@plus 2\p@ \@minus 2\p@}
+\setlength\dblfloatsep    {12\p@ \@plus 2\p@ \@minus 2\p@}
+\setlength\dbltextfloatsep{20\p@ \@plus 2\p@ \@minus 4\p@}
+\setlength\@fptop{0\p@ \@plus 1fil}
+\setlength\@fpsep{8\p@ \@plus 2fil}
+\setlength\@fpbot{0\p@ \@plus 1fil}
+\setlength\@dblfptop{0\p@ \@plus 1fil}
+\setlength\@dblfpsep{8\p@ \@plus 2fil}
+\setlength\@dblfpbot{0\p@ \@plus 1fil}
+\setlength\partopsep{0\p@ \@plus 1\p@ \@minus 1\p@}
+\def\@listi{\leftmargin\leftmargini
+            \parsep 4\p@ \@plus2\p@ \@minus\p@
+            \topsep 8\p@ \@plus2\p@ \@minus4\p@
+            \itemsep4\p@ \@plus2\p@ \@minus\p@}
+\let\@listI\@listi
+\@listi
+\def\@listii {\leftmargin\leftmarginii
+              \labelwidth\leftmarginii
+              \advance\labelwidth-\labelsep
+              \topsep    4\p@ \@plus2\p@ \@minus\p@
+              \parsep    2\p@ \@plus\p@  \@minus\p@
+              \itemsep   \parsep}
+\def\@listiii{\leftmargin\leftmarginiii
+              \labelwidth\leftmarginiii
+              \advance\labelwidth-\labelsep
+              \topsep    2\p@ \@plus\p@\@minus\p@
+              \parsep    \z@
+              \partopsep \p@ \@plus\z@ \@minus\p@
+              \itemsep   \topsep}
+\def\@listiv {\leftmargin\leftmarginiv
+              \labelwidth\leftmarginiv
+              \advance\labelwidth-\labelsep}
+\def\@listv  {\leftmargin\leftmarginv
+              \labelwidth\leftmarginv
+              \advance\labelwidth-\labelsep}
+\def\@listvi {\leftmargin\leftmarginvi
+              \labelwidth\leftmarginvi
+              \advance\labelwidth-\labelsep}
+%
+\def\@listI{\leftmargin\leftmargini
+            \parsep 0\p@% \@plus2\p@ \@minus\p@
+            \topsep 6\p@ \@plus2\p@% \@minus2\p@
+            \itemsep0\p@}% \@plus2\p@ \@minus\p@}
+%
+\newenvironment{unnumlist}{\list{}{\leftmargin15\p@\itemindent-15\p@}}{\endlist}%
+%
+\setlength\lineskip{1\p@}
+\setlength\normallineskip{1\p@}
+\renewcommand\baselinestretch{}
+\setlength\parskip{0\p@}% \@plus \p@}
+\@lowpenalty   51
+\@medpenalty  151
+\@highpenalty 301
+\setcounter{topnumber}{4}
+\renewcommand\topfraction{1}
+\setcounter{bottomnumber}{4}
+\renewcommand\bottomfraction{1}
+\setcounter{totalnumber}{8}
+\renewcommand\textfraction{0.0001}
+\renewcommand\floatpagefraction{.91}
+\setcounter{dbltopnumber}{4}
+\renewcommand\dbltopfraction{.9}
+\renewcommand\dblfloatpagefraction{.91}%
+%
+\font\cms=cmsy10 at 5.7\p@
+\def\arblcirc{\lower-.6\p@\hbox{\cms\char'17}}%
+%
+  \def\ps@headings{%
+      \let\@mkboth\@gobbletwo
+      \def\@oddfoot{\hbox to\typewidth{\hfill\rhfont\kern3.5\p@{\arblcirc}\kern3.5\p@\rightmark\hbox to \extramargin{\hskip1pc\foliofont\thepage\hfill}}}%
+      \def\@evenfoot{\hbox to\typewidth{\hspace*{-\extramargin}\hbox to \extramargin{\hfill\foliofont\thepage\hskip1pc}\rhfont\leftmark\hss}}
+      \def\@evenhead{}%
+      \def\@oddhead{}%
+    \def\sectionmark##1{}%
+    \def\subsectionmark##1{}}
+%
+\def\ps@plain{%
+    \let\@mkboth\@gobbletwo%
+    \def\@oddhead{}%
+    \let\@evenhead\@oddhead%
+    \def\@oddfoot{\hbox to\typewidth{\hfill\hbox to \extramargin{\hskip2pc\hfill\foliofont\thepage\hfill}}}%
+    \def\@evenfoot{\hbox to\typewidth{\hspace*{-\extramargin}\hbox to \extramargin{\hfill\foliofont\thepage\hfill\hskip2pc}}}%
+  }%
+%
+\newcommand\maketitle{\par
+  \begingroup
+    \renewcommand\thefootnote{}%{\@fnsymbol\c@footnote}%
+    \def\@makefnmark{\rlap{\@textsuperscript{\normalfont\@thefnmark}}}%
+    \long\def\@makefntext##1{\sffamily\raggedright\noindent
+            %\hb@xt@1.8em{\hss\@textsuperscript{\normalfont\@thefnmark}}
+                   ##1\vskip3\p@}%
+      \newpage
+      \global\@topnum\z@   % Prevents figures from going at top of page.
+      \@maketitle%
+     \thispagestyle{plain}\@thanks\clearpage
+  \endgroup
+  \setcounter{footnote}{0}%
+  \global\let\thanks\relax
+  \global\let\maketitle\relax
+  \global\let\@maketitle\relax
+  \global\let\@thanks\@empty
+  \global\let\@author\@empty
+  \global\let\@title\@empty
+  \global\let\title\relax
+  \global\let\author\relax
+  \global\let\date\relax
+  \global\let\and\relax
+}
+%
+\usepackage{color,ifpdf}
+
+\definecolor{titlecolor}{cmyk}{0.4,0.4,0.4,0}
+\definecolor{headcolor}{cmyk}{0,1.0,1.0,0.30}
+\definecolor{shadecolor}{cmyk}{0.03,0.03,0.12,0.0}
+\definecolor{fignumcolor}{cmyk}{1.0,0.4,0,0}
+\definecolor{marginrulecolor@val}{cmyk}{0,0,0,0.30}
+\definecolor{textboxcolor@val}{cmyk}{0.12,0.04,0.08,0.0}
+
+\def\title@color#1{\textcolor{titlecolor}{#1}}
+\def\head@color#1{\textcolor{headcolor}{#1}}
+\def\shade@color#1{\textcolor{shadecolor}{#1}}
+\def\fignum@color#1{\textcolor{fignumcolor}{#1}}
+\def\marginrulecolor#1{\textcolor{marginrulecolor@val}{#1}}
+\def\textboxcolor#1{\textcolor{textboxcolor@val}{#1}}
+
+%\def\title@color#1{\special{color push cmyk 0.4 0.4 0.4 0}#1\special{color pop}}
+%\def\head@color#1{\special{color push cmyk 0 1.0 1.0 0.30}#1\special{color pop}}%
+%\def\shade@color#1{\special{color push cmyk 0.03 0.03 0.12 0.0}#1\special{color pop}}%
+%\def\fignum@color#1{\special{color push cmyk 1.0 0.4 0 0}#1\special{color pop}}
+%\def\marginrulecolor#1{\special{color push cmyk 0 0 0 0.30}#1\special{color pop}}
+%\def\textboxcolor#1{\special{color push cmyk 0.12 0.04 0.08 0.0}#1\special{color pop}}
+
+\def\@maketitle{\newpage%
+  \null%
+  \begingroup\hsize1.05\typewidth\parindent0\p@%
+    \let\footnote\thanks
+    \nointerlineskip%
+    \vskip-6\p@%
+    \noindent{\reset@font\titlefont\title@color{\@title}\par}
+    \vskip15\p@
+    % \hspace{-3cm}
+    \noindent{\authorfont\@author\par\vskip5\p@}%
+    \vfill%
+    \global\setbox\z@\vbox{\hsize\typewidth%
+      % \begin{tabular*}{\typewidth}{@{}p{13.5pc}@{\hskip.75pc}p{22.25pc}@{}}
+      % \nointerlineskip{\vskip5.4\p@\vbox{\journalinfo}\vrule height0\p@ depth6\p@ width\z@}%
+      % &\ifvoid\@kwdbox%
+      %    \ifvoid\@absbbox\else{\box\@absbbox}\fi%
+      % \else%
+      %    {\vskip5.4\p@\box\@kwdbox}\endgraf\nointerlineskip%
+      %    \ifvoid\@absbbox\else\vskip15\p@{\box\@absbbox}\fi%
+      % \fi\\%
+  % \end{tabular*}
+      
+      % \begin{tabular*}{\typewidth}{@{}|p{3.5pc}|@{\hskip.75pc}p{22.25pc}|@{}}
+      \begin{tabular*}{\typewidth}{@{}p{1.5pc}@{\hskip.75pc}p{22.25pc}@{}}
+      \nointerlineskip{\vskip5.4\p@ \vrule height0\p@ depth6\p@ width\z@}%
+      &\ifvoid\@kwdbox%
+         \ifvoid\@absbbox\else{\box\@absbbox}\fi%
+      \else%
+         {\vskip5.4\p@\box\@kwdbox}\endgraf\nointerlineskip%
+         \ifvoid\@absbbox\else\vskip15\p@{\box\@absbbox}\fi%
+      \fi\\%
+      \end{tabular*}
+    }
+    \@tempdima\typewidth\advance\@tempdima77\p@%
+    \vbox{\shade@color{\hskip-46\p@\vrule height\ht\z@ width\@tempdima depth\dp\z@}\vskip-\ht\z@\vskip-\dp\z@%
+    {\box\z@}}%
+  \endgroup}
+%
+% First page element and layout declaration
+%
+\def\firstpagenote#1{\gdef\@firstpagenote{#1}}\firstpagenote{}%
+\def\journalinfo{{\jinfofont%
+    \@jname\ \@jyear. \@jvol:\@fstpage--\@endpage\endgraf%
+    %\ifx\@doi\empty\else\vskip6\p@ This article's doi:\break \@doi\endgraf\vskip6\p@\fi%
+    \ifx\@doi\empty\else\vskip6\p@ https://doi.org/\@doi\endgraf\vskip6\p@\fi%
+    Copyright\ \copyright\ \@jyear\ by the author(s).\endgraf All rights reserved\endgraf%
+    \ifx\@firstpagenote\@empty\else\vskip6\p@\@firstpagenote\endgraf\fi%
+}}
+%
+\def\chk@key{no}%
+\def\YES{yes}%
+\def\affil#1{\par\ifx\chk@key\YES\else\vskip6\p@\fi{\reset@font\affilfont#1\par}\def\chk@key{yes}}
+%
+\newbox\@absbbox
+\newenvironment{abstract}{\parindent0\p@%
+      % \global\setbox\@absbbox\vbox\bgroup\hsize23.75pc\abstractfont\noindent\ignorespaces%
+      \global\setbox\@absbbox\vbox\bgroup\hsize33.75pc\abstractfont\noindent\ignorespaces%
+      \ifx\abstractname\@empty\else{\head@color{\sffamily\bfseries\abstractname}\endgraf\vskip9\p@}\fi%
+      }
+      {\par\egroup}
+
+%
+\newcommand\keywordsname{Keywords}
+\newbox\@kwdbox
+\newenvironment{keywords}{\parindent0\p@%
+  % \global\setbox\@kwdbox\vbox\bgroup\hsize23.75pc\keywordsfont\noindent\ignorespaces%
+  \global\setbox\@kwdbox\vbox\bgroup\hsize33.75pc\keywordsfont\noindent\ignorespaces%
+  \ifx\keywordsname\@empty\else{\head@color{\sffamily\bfseries\keywordsname}\endgraf\vskip4\p@}\fi%
+      }{\par\egroup}%
+%
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+\setcounter{secnumdepth}{3}
+\newcounter {part}
+\newcounter {section}
+\newcounter {subsection}[section]
+\newcounter {subsubsection}[subsection]
+\newcounter {paragraph}[subsubsection]
+\newcounter {subparagraph}[paragraph]
+\renewcommand \thepart {\@Roman\c@part}
+\renewcommand \thesection {\@arabic\c@section}
+\renewcommand\thesubsection   {\thesection.\@arabic\c@subsection}
+\renewcommand\thesubsubsection{\thesubsection .\@arabic\c@subsubsection}
+\renewcommand\theparagraph    {\thesubsubsection.\@arabic\c@paragraph}
+\renewcommand\thesubparagraph {\theparagraph.\@arabic\c@subparagraph}
+\newcommand\part{%
+   \if@noskipsec \leavevmode \fi
+   \par
+   \addvspace{4ex}%
+   \@afterindentfalse
+   \secdef\@part\@spart}
+
+\def\@part[#1]#2{%
+    \ifnum \c@secnumdepth >\m@ne
+      \refstepcounter{part}%
+      \addcontentsline{toc}{part}{\thepart\hspace{1em}#1}%
+    \else
+      \addcontentsline{toc}{part}{#1}%
+    \fi
+    {\parindent \z@ \raggedright
+     \interlinepenalty \@M
+     \normalfont
+     \ifnum \c@secnumdepth >\m@ne
+       \Large\bfseries \partname\nobreakspace\thepart
+       \par\nobreak
+     \fi
+     \huge \bfseries #2%
+     \markboth{}{}\par}%
+    \nobreak
+    \vskip 3ex
+    \@afterheading}
+\def\@spart#1{%
+    {\parindent \z@ \raggedright
+     \interlinepenalty \@M
+     \normalfont
+     \huge \bfseries #1\par}%
+     \nobreak
+     \vskip 3ex
+     \@afterheading}
+%
+\def\@seccntformat#1{\head@color{\csname the#1\endcsname.}\hskip3\p@}
+%
+\newcommand\section{\@startsection {section}{1}{\z@}%
+                                   {-20\p@ \@plus -3\p@ \@minus -1\p@}%
+                                   {4\p@}%
+                                   {\sectionfont}}
+\newcommand\subsection{\@startsection{subsection}{2}{\z@}%
+                                     {-16\p@ \@plus -1\p@ \@minus -.1\p@}%
+                                     {4\p@}%
+                                     {\subsectionfont}}
+\newcommand\subsubsection{\@startsection{subsubsection}{3}{\z@}%
+                                     {-12\p@ \@plus -1\p@ \@minus -0.1\p@}%
+                                     {-0.5em}%
+                                     {\subsubsectionfont}}
+\newcommand\paragraph{\@startsection{paragraph}{4}{\z@}%
+                                    {13\p@ \@plus 3.25\p@ \@minus 1\p@}%
+                                    {-0.5em}%
+                                    {\paragraphfont}}
+\newcommand\subparagraph{\@startsection{subparagraph}{5}{\parindent}%
+                                       {13\p@ \@plus 3.25\p@ \@minus 1\p@}%
+                                       {-0.5em}%
+                                      {\subparagraphfont}}
+%
+\def\@sect#1#2#3#4#5#6[#7]#8{%
+  \ifnum #2>\c@secnumdepth
+    \let\@svsec\@empty
+  \else
+    \refstepcounter{#1}%
+    \protected@edef\@svsec{\@seccntformat{#1}\relax}%
+  \fi
+  \@tempskipa #5\relax
+  \ifdim \@tempskipa>\z@
+    \begingroup
+      #6{%
+        \@hangfrom{\hskip #3\relax\@svsec}%
+          \interlinepenalty \@M \head@color{#8}\@@par}%
+    \endgroup
+    \csname #1mark\endcsname{#7}%
+    \addcontentsline{toc}{#1}{%
+      \ifnum #2>\c@secnumdepth \else
+        \protect\numberline{\csname the#1\endcsname}%
+      \fi
+      #7}%
+  \else
+    \def\@svsechd{%
+      #6{\hskip #3\relax
+      \@svsec \head@color{#8.}}%
+      \csname #1mark\endcsname{#7}%
+      \addcontentsline{toc}{#1}{%
+        \ifnum #2>\c@secnumdepth \else
+          \protect\numberline{\csname the#1\endcsname}%
+        \fi
+        #7}}%
+  \fi
+  \@xsect{#5}}
+%
+\def\@ssect#1#2#3#4#5{%
+  \@tempskipa #3\relax
+  \ifdim \@tempskipa>\z@
+    \begingroup
+      #4{%
+        \@hangfrom{\hskip #1}%
+          \interlinepenalty \@M \head@color{#5}\@@par}%
+    \endgroup
+  \else
+    \def\@svsechd{#4{\hskip #1\relax #5}}%
+  \fi
+  \@xsect{#3}}
+%
+\setlength\leftmargini  {2.65em}
+\leftmargin  \leftmargini
+\setlength\leftmarginii  {2.2em}
+\setlength\leftmarginiii {1.87em}
+\setlength\leftmarginiv  {1.7em}
+\setlength\leftmarginv  {1em}
+\setlength\leftmarginvi {1em}
+\setlength  \labelsep  {.5em}
+\setlength  \labelwidth{\leftmargini}
+\addtolength\labelwidth{-\labelsep}
+\@beginparpenalty -\@lowpenalty
+\@endparpenalty   -\@lowpenalty
+\@itempenalty     -\@lowpenalty
+\renewcommand\theenumi{\@arabic\c@enumi}
+\renewcommand\theenumii{\@alph\c@enumii}
+\renewcommand\theenumiii{\@roman\c@enumiii}
+\renewcommand\theenumiv{\@Alph\c@enumiv}
+\newcommand\labelenumi{\theenumi.}
+\newcommand\labelenumii{(\theenumii)}
+\newcommand\labelenumiii{\theenumiii.}
+\newcommand\labelenumiv{\theenumiv.}
+\renewcommand\p@enumii{\theenumi}
+\renewcommand\p@enumiii{\theenumi(\theenumii)}
+\renewcommand\p@enumiv{\p@enumiii\theenumiii}
+\newcommand\labelitemi{\textbullet}
+\newcommand\labelitemii{\normalfont\bfseries \textendash}
+\newcommand\labelitemiii{\textasteriskcentered}
+\newcommand\labelitemiv{\textperiodcentered}
+\newenvironment{description}
+               {\list{}{\labelwidth\z@ \itemindent-\leftmargin
+                        \let\makelabel\descriptionlabel}}
+               {\endlist}
+\newcommand*\descriptionlabel[1]{\hspace\labelsep
+                                \normalfont\bfseries #1}
+\newenvironment{verse}
+               {\let\\\@centercr
+                \list{}{\itemsep      \z@
+                        \itemindent   -1.5em%
+                        \listparindent\itemindent
+                        \rightmargin  \leftmargin
+                        \advance\leftmargin 1.5em}%
+                \item\relax}
+               {\endlist}
+\newenvironment{quotation}
+               {\list{}{\listparindent 1.5em%
+                        \itemindent    \listparindent
+                        \rightmargin   \leftmargin
+                        \parsep        \z@ \@plus\p@}%
+                \item\relax}
+               {\endlist}
+\newenvironment{quote}
+               {\list{}{\rightmargin\leftmargin}%
+                \item\relax}
+               {\endlist}
+\newenvironment{extract}
+ {\list{}{\leftmargin1pc\topsep12\p@ plus2\p@\rightmargin\leftmargin}\item[]\extractfont}
+ {\endlist}
+\newcommand\appendix{\par
+  \setcounter{section}{0}%
+  \setcounter{subsection}{0}%
+  \gdef\thesection{\@Alph\c@section}}
+\setlength\arraycolsep{2\p@}
+\setlength\tabcolsep{6\p@}
+\setlength\arrayrulewidth{.4\p@}
+\setlength\doublerulesep{2\p@}
+\setlength\tabbingsep{\labelsep}
+\skip\@mpfootins = \skip\footins
+\setlength\fboxsep{3\p@}
+\setlength\fboxrule{.4\p@}
+\renewcommand \theequation {\@arabic\c@equation}
+\newcounter{figure}
+\renewcommand \thefigure {\@arabic\c@figure}
+\def\fps@figure{tbp}
+\def\ftype@figure{1}
+\def\ext@figure{lof}
+\def\fnum@figure{\figurename\nobreakspace\thefigure}
+\newcount\chkfigcnt
+\newenvironment{figure}
+               {\global\advance\chkfigcnt\@ne\@float{figure}}
+               {\label{chk@figure@\the\chkfigcnt}\end@float}
+\newenvironment{figure*}
+               {\global\advance\chkfigcnt\@ne\@dblfloat{figure}}
+               {\label{chk@figure@\the\chkfigcnt}\end@dblfloat}
+\newcounter{table}
+\renewcommand\thetable{\@arabic\c@table}
+\def\fps@table{tbp}
+\def\ftype@table{2}
+\def\ext@table{lot}
+\def\fnum@table{\tablename\nobreakspace\thetable}
+\newenvironment{table}
+               {\@float{table}}
+               {\end@float}
+\newenvironment{table*}
+               {\@dblfloat{table}}
+               {\end@dblfloat}
+\newlength\abovecaptionskip
+\newlength\belowcaptionskip
+\setlength\abovecaptionskip{-6\p@}
+\setlength\belowcaptionskip{0\p@}
+%
+\def\FigName{figure}
+\long\def\@makecaption#1#2{%
+    \ifx\FigName\@captype
+      \vskip\abovecaptionskip
+      \@makefigurecaption{#1}{#2}%
+    \else
+      \@maketablecaption{#1}{#2}%
+      \vskip\belowcaptionskip
+    \fi}
+%
+\newdimen\chk@fig@width\chk@fig@width\z@
+%
+\def\@makefigurecaption#1#2{\edef\chk@fig@temp{\getpagerefnumber{chk@figure@\the\chkfigcnt}}%
+  \ifdim\chk@fig@width>\typewidth%
+    \noindent{\fignum@color{\figcaptionnumfont#1}}\endgraf\vskip-8.5\p@%
+    \normalcolor\noindent\hbox to \hsize{\hrulefill}\endgraf\vskip-.9\p@%
+    {\figcaptionfont\raggedright#2\par}%
+  \else%
+    \ifdim\chk@fig@width>\textwidth%
+      \vbox{\par\vskip5\p@\hsize\typewidth\ifodd\chk@fig@temp\raggedright\else\leftskip-7.5pc\rightskip7.5pc plus1fil\fi
+      \noindent{\fignum@color{\figcaptionnumfont#1}}\endgraf\vskip-8.5\p@%
+      \normalcolor\noindent\hbox to \hsize{\hrulefill}\endgraf\vskip-.9\p@%
+      {\figcaptionfont#2\par}\par}%
+    \else%
+      \noindent{\fignum@color{\figcaptionnumfont#1}}\endgraf\vskip-8.5\p@%
+      \normalcolor\noindent\hbox to \hsize{\hrulefill}\endgraf\vskip-.9\p@%
+      {\figcaptionfont\rightskip0pt plus1fill#2\par}%
+    \fi
+  \fi%
+}
+%
+\def\@maketablecaption#1#2{%
+    \noindent\tablecaptionfont%
+    \fignum@color{{\tablecaptionnumfont#1\quad}{\tablecaptionfont#2}}\par
+}%
+\input epsf.sty
+%
+\def\tabular{\tablefont\let\@halignto\@empty\@tabular}
+\newenvironment{tabnote}{\par\tabnotefont}{\par}
+%
+\DeclareOldFontCommand{\rm}{\normalfont\rmfamily}{\mathrm}
+\DeclareOldFontCommand{\sf}{\normalfont\sffamily}{\mathsf}
+\DeclareOldFontCommand{\tt}{\normalfont\ttfamily}{\mathtt}
+\DeclareOldFontCommand{\bf}{\normalfont\bfseries}{\mathbf}
+\DeclareOldFontCommand{\it}{\normalfont\itshape}{\mathit}
+\DeclareOldFontCommand{\sl}{\normalfont\slshape}{\@nomath\sl}
+\DeclareOldFontCommand{\sc}{\normalfont\scshape}{\@nomath\sc}
+\DeclareRobustCommand*\cal{\@fontswitch\relax\mathcal}
+\DeclareRobustCommand*\mit{\@fontswitch\relax\mathnormal}
+\newcommand\@pnumwidth{1.75em}
+\newcommand\@tocrmarg{6.5em}
+\newcommand\@dotsep{1}
+\setcounter{tocdepth}{2}
+\newcommand\tableofcontents{\par\vspace*{-15.75\p@}%
+\definecolor{shadecolor}{cmyk}{0.03,0.03,0.12,0}
+\fboxsep12\p@\fboxrule0\p@\ifodd\c@page\moveright3.75pc\else\moveright-3.75pc\fi\vbox\bgroup\begin{shaded}\@nobreaktrue\hsize36pc
+    \section*{\contentsname}
+%%         \@mkboth{%
+%%            \MakeUppercase\contentsname}{\MakeUppercase\contentsname}}%
+    \@starttoc{toc}%
+\end{shaded}\egroup}
+\newcommand*\l@part[2]{%
+  \ifnum \c@tocdepth >-2\relax
+    \addpenalty\@secpenalty
+    \addvspace{2.25em \@plus\p@}%
+    \setlength\@tempdima{3em}%
+    \begingroup
+      \parindent \z@ \rightskip \@pnumwidth
+      \parfillskip -\@pnumwidth
+      {\leavevmode
+       \large \bfseries #1\hfil \hb@xt@\@pnumwidth{\hss #2}}\par
+       \nobreak
+       \if@compatibility
+         \global\@nobreaktrue
+         \everypar{\global\@nobreakfalse\everypar{}}%
+      \fi
+    \endgroup
+  \fi}
+\newcommand*\l@section{\@dottedtocline{1}{0em}{1.2em}}
+\newcommand*\l@subsection{\@dottedtocline{2}{1.2em}{2em}}
+\newcommand*\l@subsubsection{\@dottedtocline{3}{3.2em}{2.8em}}
+\newcommand*\l@paragraph{\@dottedtocline{4}{7.0em}{4.1em}}
+\newcommand*\l@subparagraph{\@dottedtocline{5}{10em}{5em}}
+%
+\def\numberline#1{\hb@xt@\@tempdima{#1.\hfil}}
+\def\@dottedtocline#1#2#3#4#5{%
+  \ifnum #1>\c@tocdepth \else
+    \vskip \z@ \@plus.2\p@
+    {\sffamily\small\leftskip #2\relax \rightskip \@tocrmarg \parfillskip -\rightskip
+     \parindent #2\relax\@afterindenttrue
+     \interlinepenalty\@M
+     \leavevmode
+     \@tempdima #3\relax
+     \advance\leftskip \@tempdima \null\nobreak\hskip -\leftskip
+     {#4}\nobreak
+      \leaders\hbox{$\m@th
+         \mkern \@dotsep mu\hbox{.}\mkern \@dotsep
+         mu$}
+     \hfill
+     \nobreak
+     \hb@xt@\@pnumwidth{\hfil#5}%
+     \par}%
+  \fi}
+%
+\newcommand\listoffigures{%
+    \section*{\listfigurename}%
+      \@mkboth{\MakeUppercase\listfigurename}%
+              {\MakeUppercase\listfigurename}%
+    \@starttoc{lof}%
+    }
+\newcommand*\l@figure{\@dottedtocline{1}{1.5em}{2.3em}}
+\newcommand\listoftables{%
+    \section*{\listtablename}%
+      \@mkboth{%
+          \MakeUppercase\listtablename}%
+         {\MakeUppercase\listtablename}%
+    \@starttoc{lot}%
+    }
+\let\l@table\l@figure
+\newdimen\bibindent
+\setlength\bibindent{1.5em}
+\def\@biblabel#1{#1.}
+\newenvironment{thebibliography}[1]
+     {\section*{\refname}%
+      \bibliofont%
+      \def\@tempa{#1}%
+      %\@mkboth{\MakeUppercase\refname}{\MakeUppercase\refname}%
+    \ifx\@tempa\@empty
+      \list{}%
+           {\labelwidth0\p@\labelsep0\p@%
+            \leftmargin12\p@\itemindent-12\p@%
+            \itemsep\z@
+            \@openbib@code
+            \usecounter{enumiv}%
+            \let\p@enumiv\@empty
+            \renewcommand\theenumiv{\@arabic\c@enumiv}%
+           }%
+    \else%
+      \list{\@biblabel{\@arabic\c@enumiv}}%
+           {\settowidth\labelwidth{\@biblabel{#1}}%
+            \leftmargin\labelwidth
+            \itemsep\z@
+            \advance\leftmargin\labelsep
+            \@openbib@code
+            \usecounter{enumiv}%
+            \let\p@enumiv\@empty
+            \renewcommand\theenumiv{\@arabic\c@enumiv}%
+            \def\@biblabel##1{\@ifundefined{bibnote@\romannumeral\theenumiv}{{\reset@font\normalfont\bibliofont##1.}}{{\bfseries##1.}}}
+           }%
+    \fi%
+      \sloppy
+      \clubpenalty4000
+      \@clubpenalty \clubpenalty
+      \widowpenalty4000%
+      \sfcode`\.\@m}
+     {\def\@noitemerr
+       {\@latex@warning{Empty `thebibliography' environment}}%
+      \endlist}
+\newcommand\newblock{\hskip .11em\@plus.33em\@minus.07em}
+\let\@openbib@code\@empty
+\newenvironment{theindex}
+               {\if@twocolumn
+                  \@restonecolfalse
+                \else
+                  \@restonecoltrue
+                \fi
+                \twocolumn[\section*{\indexname}]%
+                \@mkboth{\MakeUppercase\indexname}%
+                        {\MakeUppercase\indexname}%
+                \thispagestyle{plain}\parindent\z@
+                \parskip\z@ \@plus .3\p@\relax
+                \columnseprule \z@
+                \columnsep 35\p@
+                \let\item\@idxitem}
+               {\if@restonecol\onecolumn\else\clearpage\fi}
+\newcommand\@idxitem{\par\hangindent 40\p@}
+\newcommand\subitem{\@idxitem \hspace*{20\p@}}
+\newcommand\subsubitem{\@idxitem \hspace*{30\p@}}
+\newcommand\indexspace{\par \vskip 10\p@ \@plus5\p@ \@minus3\p@\relax}
+\renewcommand\footnoterule{%
+  \kern-3\p@
+  \hrule\@width88.5\p@ height.5\p@
+  \kern3\p@}
+\newcommand\@makefntext[1]{%
+    \parindent 1em%
+    \noindent
+    \hb@xt@1.8em{\hss\@makefnmark}#1}
+\newcommand\contentsname{Contents}
+\newcommand\listfigurename{List of Figures}
+\newcommand\listtablename{List of Tables}
+\newcommand\refname{LITERATURE CITED}
+\newcommand\indexname{Index}
+\newcommand\figurename{Figure}
+\newcommand\tablename{Table}
+\newcommand\partname{Part}
+\newcommand\appendixname{Appendix}
+\newcommand\abstractname{Abstract}
+\def\today{\ifcase\month\or
+  January\or February\or March\or April\or May\or June\or
+  July\or August\or September\or October\or November\or December\fi
+  \space\number\day, \number\year}
+\setlength\columnsep{12\p@}
+\setlength\columnseprule{0\p@}
+%
+\pagestyle{headings}
+\sloppy
+\voffset-1pc
+\hoffset-2.75pc
+%
+\pagenumbering{arabic}
+%
+%%% Body environment declaration
+%
+\newcount\marginnotecnt
+\newbox\marginnotebox%
+\newenvironment{marginnote}[1][\relax]
+  {\def\@tempa{#1}\global\advance\marginnotecnt\@ne\let\entry\marginnoteentry%
+   \edef\marginnote@page{\getpagerefnumber{marginnote@\the\marginnotecnt}}
+   \setbox\marginnotebox\vbox\bgroup\hsize6.5pc\marginnotefont\parindent\z@\if\@tempa\relax\else\vskip\@tempa\fi%
+   \noindent\hbox to 6.5pc{\marginrulecolor{\hrulefill}}\par%
+  }%
+  {\par\vskip-2\p@\noindent\hbox to 6.5pc{\marginrulecolor{\hrulefill}}\egroup%
+   \ifodd\marginnote@page
+     \marginpar{\hspace*{2.5\p@}\box\marginnotebox}%
+   \else
+     \marginpar{\hspace*{-23\p@}\box\marginnotebox}%
+   \fi\label{marginnote@\the\marginnotecnt}}
+% 
+\def\marginnoteentry#1#2{\par\addvspace{4\p@}\noindent{\fignum@color{\sffamily\bfseries#1:}}\ #2\par}
+%
+\newcount\bibnotecnt
+\def\bibnote{\@ifnextchar[{\@bibnote}{\@bibnote[\relax]}}
+\def\@bibnote[#1]#2{\def\chk@tempa{#1}%
+  \global\advance\bibnotecnt\@ne%
+  \edef\bibnote@page{\getpagerefnumber{bibnote@\the\bibnotecnt}}%
+  \ifodd\bibnote@page
+    \marginpar{\hbox{\reset@font\hskip2.5\p@\parbox{6.5pc}{\vbox to \z@{\if\chk@tempa\relax\else\vskip\chk@tempa\fi\noindent\hbox to 6.5pc{\marginrulecolor{\hrulefill}}\par{\bibmarginnotefont\ifx\chk@natbib@nonumref\YES\else\ifx\@tempa\@empty\else\theenumiv.\ \fi\fi#2\par}\par\vskip-6.6\p@%
+    \noindent\hbox to 6.5pc{\marginrulecolor{\hrulefill}}}}}}%
+  \else%
+    \marginpar{\hbox{\reset@font\hskip-23\p@\parbox{6.5pc}{\vbox to \z@{\if\chk@tempa\relax\else\vskip\chk@tempa\fi\noindent\hbox to 6.5pc{\marginrulecolor{\hrulefill}}\par{\reset@font\bibmarginnotefont\ifx\chk@natbib@nonumref\YES\else\ifx\@tempa\@empty\else\theenumiv.\ \fi\fi#2\par}\vskip-6.6\p@%
+    \noindent\hbox to 6.5pc{\marginrulecolor{\hrulefill}}}}}}%
+\fi%
+\label{bibnote@\the\bibnotecnt}
+\immediate\write\@mainaux{\string\gdef\string\bibnote@\romannumeral\theenumiv{}}\bfseries\mathversion{bold}}
+%
+\def\@bibitem#1{\item\if@filesw \immediate\write\@auxout
+       {\string\bibcite{#1}{\the\value{\@listctr}}}\fi\ignorespaces\reset@font\normalfont\bibliofont}
+%
+\def\summaryhead#1{\noindent{\title@color{\sffamily\bfseries#1}\par\vskip4.2\p@}}
+\newenvironment{summary}[1][\relax]%
+  {\par\overfullrule0\p@\let\centerline\leftline%
+   \definecolor{shadecolor}{cmyk}{0.03,0.03,0.12,0}
+  \fboxsep10\p@\advance\hsize-2\fboxsep\begin{shaded}\leftmargini12\p@%
+  \if#1\relax\else\summaryhead{#1}\fi}
+  {\end{shaded}}
+%
+\def\issueshead#1{\noindent{\title@color{\sffamily\bfseries#1}\par\vskip4.2\p@}}
+\newenvironment{issues}[1][\relax]%
+  {\par\overfullrule0\p@\let\centerline\leftline%
+   \definecolor{shadecolor}{cmyk}{0.06,0.02,0.04,0}
+  \fboxsep10\p@\advance\hsize-2\fboxsep\begin{shaded}\leftmargini12\p@%
+  \if#1\relax\else\issueshead{#1}\fi}
+  {\end{shaded}}
+%
+\gdef\fps@textbox{t}
+\def\ftype@textbox{3}
+\def\textboxhere{h}
+\def\textboxbottom{b}
+\newcommand\textboxhead[1]{\noindent{\textboxheadfont\fignum@color{\sffamily#1}\par\vskip6\p@}\noindent\ignorespaces}
+\newcommand\textboxsubhead[1]{\par\addvspace{12\p@}\noindent{\textboxheadfont\fignum@color{\sffamily#1}\par\vskip6\p@}\noindent\ignorespaces}
+\newcommand\textboxsubsubhead[1]{\par\addvspace{12\p@}\noindent{\textboxheadfont\fignum@color{\sffamily#1.}\hskip6\p@}\noindent\ignorespaces}
+\newcount\textboxcnt
+\newenvironment{textbox}[1][\relax]%
+  {\gdef\textboxpos{#1}\global\advance\textboxcnt\@ne%
+   \edef\textbox@page{\getpagerefnumber{textbox@\the\textboxcnt}} 
+   \setbox\z@\vbox\bgroup\hsize38pc\textboxfont\vskip12\p@\noindent\ignorespaces%
+   \let\section\textboxhead%
+   \let\subsection\textboxsubhead%
+   \let\subsubsection\textboxsubsubhead%
+  }
+  {\vskip11\p@\egroup%
+   \ifx\textboxpos\textboxbottom\@float{textbox}[b]\else\ifx\textboxpos\textboxhere\@float{textbox}[h]\else\@float{textbox}[t]\fi\fi%
+   \ifodd\textbox@page\else\hskip-7.5pc\fi%
+   \textboxcolor{\vrule height\ht\z@ width\wd\z@ depth\dp\z@}%
+   \llap{\box\z@}
+   \label{textbox@\the\textboxcnt}
+   \end@float}
+%
+\AtEndDocument{\immediate\write\@mainaux{\string\endpage{\thepage}}}%
+%
+\usepackage{graphicx,multicol,refcount,framed}%colortbl,array,
+%
+\def\Gin@setfile#1#2#3{%
+  \ifx\\#2\\\Gread@false\fi
+  \ifGin@bbox\else
+    \ifGread@
+      \csname Gread@%
+         \expandafter\ifx\csname Gread@#1\endcsname\relax
+           eps%
+         \else
+           #1%
+         \fi
+      \endcsname{\Gin@base#2}%
+    \else
+      \Gin@nosize{#3}%
+    \fi
+  \fi
+  \Gin@viewport@code
+  \Gin@nat@height\Gin@ury bp%
+  \advance\Gin@nat@height-\Gin@lly bp%
+  \Gin@nat@width\Gin@urx bp%
+  \advance\Gin@nat@width-\Gin@llx bp%
+  \Gin@req@sizes
+  \expandafter\ifx\csname Ginclude@#1\endcsname\relax
+    \Gin@drafttrue
+    \expandafter\ifx\csname Gread@#1\endcsname\relax
+      \@latex@error{Can not include graphics of type: #1}\@ehc
+      \global\expandafter\let\csname Gread@#1\endcsname\@empty
+    \fi
+  \fi
+  \leavevmode
+  \ifGin@draft
+      \hb@xt@\Gin@req@width{%
+        \vrule\hss
+        \vbox to \Gin@req@height{%
+           \hrule \@width \Gin@req@width
+           \vss
+           \edef\@tempa{#3}%
+           \rlap{ \ttfamily\expandafter\strip@prefix\meaning\@tempa}%
+           \vss
+           \hrule}%
+        \hss\vrule}%
+  \else
+    \@addtofilelist{#3}%
+    \ProvidesFile{#3}[Graphic file (type #1)]%
+    \setbox\z@\hbox{\csname Ginclude@#1\endcsname{#3}}%
+    \dp\z@\z@
+    \ht\z@\Gin@req@height
+    \wd\z@\Gin@req@width
+    \global\chk@fig@width\Gin@req@width
+    \edef\chk@fig@temp{\getpagerefnumber{chk@figure@\the\chkfigcnt}}
+    \ifdim\Gin@req@width>\typewidth%
+      \centerline{\box\z@}
+    \else
+      \ifdim\Gin@req@width>\textwidth%
+        \@tempdima\typewidth\advance\@tempdima-\Gin@req@width\divide\@tempdima by 2%
+        \leftline{\ifodd\chk@fig@temp\hbox{\hspace*{\@tempdima}\box\z@}\else\hbox{\hspace*{-7.5pc}\hspace*{\@tempdima}\box\z@}\fi}
+      \else%
+        \centerline{\box\z@}
+      \fi%  
+    \fi%  
+  \fi}
+%
+%
+\def\chk@natbib@nonumref{no}%
+\AtBeginDocument{\immediate\write\@mainaux{\string\fstpage{\thepage}}%
+  \@ifpackageloaded{natbib}{%
+    \global\let\theenumiv\theNAT@ctr%
+    \ifNAT@numbers%
+      \renewcommand\NAT@open{(}%
+      \renewcommand\NAT@close{)}%
+      \gdef\chk@natbib@nonumref{no}%
+      \gdef\bibnumfmt#1{\@ifundefined{bibnote@\romannumeral\theenumiv}{{\reset@font\normalfont\bibliofont#1.}}{{\bfseries#1.}}}%
+    \else%
+      \gdef\chk@natbib@nonumref{yes}%
+      \gdef\NAT@aysep{}%
+    \fi%
+    \let\bibfont\bibliofont%
+    \global\bibsep\z@%
+    \def\bibitem@fin{\@ifxundefined\@bibstop{}{\csname bibitem@\@bibstop\endcsname}\reset@font\normalfont\bibliofont}%
+  }{\def\@cite#1#2{({#1\if@tempswa , #2\fi})}}%
+%
+\if@DotinEqNum%
+  \@ifpackageloaded{amsmath}{%
+    %\def\tagform@#1{\maketag@@@{(\ignorespaces#1\unskip\@@italiccorr)}}
+    \def\tagform@#1{\maketag@@@{\ignorespaces#1\unskip.}}
+  }{%
+    %\def\@eqnnum{{\normalfont \normalcolor (\theequation)}}
+    \def\@eqnnum{{\normalfont \normalcolor \theequation.}}
+  }%
+\fi% 
+%%
+\@ifpackageloaded{hyperref}{%
+  \def\@linkcolor{green}%
+  \def\termkey#1{\hyperlink{key#1}{#1}}%
+  \def\marginnoteentry#1#2{\par\addvspace{4\p@}\noindent{\fignum@color{\sffamily\bfseries#1:\hypertarget{key#1}{}}}\ #2\par}%
+}{%
+  \def\termkey#1{#1}%
+}
+%
+}
+%
+\def\@outputpage{%
+%\special{color push  cmyk 0 0 0 1.0}%
+\begingroup           % the \endgroup is put in by \aftergroup
+\let\firstmark\botmark
+  \let \protect \noexpand
+  \@resetactivechars
+  \@parboxrestore
+  \shipout \vbox{%
+    \set@typeset@protect
+    \aftergroup \endgroup
+    \aftergroup \set@typeset@protect
+                                % correct? or just restore by ending
+                                % the group?
+  \if@specialpage
+    \global\@specialpagefalse\@nameuse{ps@\@specialstyle}%
+  \fi
+  \if@twoside
+    \ifodd\count\z@ \let\@thehead\@oddhead \let\@thefoot\@oddfoot
+         \let\@themargin\oddsidemargin
+    \else \let\@thehead\@evenhead
+       \let\@thefoot\@evenfoot \let\@themargin\evensidemargin
+    \fi
+  \fi
+  \reset@font
+  \normalsize
+  \baselineskip\z@skip \lineskip\z@skip \lineskiplimit\z@
+    \@begindvi\trimmarks
+    \vskip \topmargin
+    \ifodd\c@page\else\advance\@themargin\extramargin\fi%
+    \moveright\@themargin \vbox {%
+      \setbox\@tempboxa \vbox to\headheight{%
+        \vfil
+        \color@hbox
+          \normalcolor
+          \hb@xt@\textwidth {%
+            \let \label \@gobble
+            \let \index \@gobble
+            \let \glossary \@gobble %% 21 Jun 91
+            \@thehead
+            }%
+        \color@endbox
+        }%                        %% 22 Feb 87
+      \dp\@tempboxa \z@
+%      \box\@tempboxa
+%      \vskip \headsep
+      \box\@outputbox
+      \baselineskip \footskip
+      \color@hbox
+        \normalcolor
+        \hb@xt@\textwidth{%
+          \let \label \@gobble
+          \let \index \@gobble      %% 22 Feb 87
+          \let \glossary \@gobble   %% 21 Jun 91
+          \@thefoot
+          }%
+      \color@endbox
+      }%
+    }%
+\global \@colht \textheight
+\stepcounter{page}%
+%\special{color pop}
+}
+\def\trimmarks{}%
+%
+\ifpdf%
+  \setlength{\pdfpagewidth}{8.5in}%
+  \setlength{\pdfpageheight}{11in}%
+\fi%
+%%
+
+%% Journal shortcuts for the bibliography
+\newcommand*\aap{A\&A}
+\let\astap=\aap
+\newcommand*\aapr{A\&A~Rev.}
+\newcommand*\aaps{A\&AS}
+\newcommand*\actaa{Acta Astron.}
+\newcommand*\aj{AJ}
+\newcommand*\ao{Appl.~Opt.}
+\let\applopt\ao
+\newcommand*\apj{ApJ}
+\newcommand*\apjl{ApJ}
+\let\apjlett\apjl
+\newcommand*\apjs{ApJS}
+\let\apjsupp\apjs
+\newcommand*\aplett{Astrophys.~Lett.}
+\newcommand*\apspr{Astrophys.~Space~Phys.~Res.}
+\newcommand*\apss{Ap\&SS}
+\newcommand*\araa{ARA\&A}
+\newcommand*\azh{AZh}
+\newcommand*\baas{BAAS}
+\newcommand*\bac{Bull. astr. Inst. Czechosl.}
+\newcommand*\bain{Bull.~Astron.~Inst.~Netherlands}
+\newcommand*\caa{Chinese Astron. Astrophys.}
+\newcommand*\cjaa{Chinese J. Astron. Astrophys.}
+\newcommand*\fcp{Fund.~Cosmic~Phys.}
+\newcommand*\gca{Geochim.~Cosmochim.~Acta}
+\newcommand*\grl{Geophys.~Res.~Lett.}
+\newcommand*\iaucirc{IAU~Circ.}
+\newcommand*\icarus{Icarus}
+\newcommand*\jcap{J. Cosmology Astropart. Phys.}
+\newcommand*\jcp{J.~Chem.~Phys.}
+\newcommand*\jgr{J.~Geophys.~Res.}
+\newcommand*\jqsrt{J.~Quant.~Spectr.~Rad.~Transf.}
+\newcommand*\jrasc{JRASC}
+\newcommand*\memras{MmRAS}
+\newcommand*\memsai{Mem.~Soc.~Astron.~Italiana}
+\newcommand*\mnras{MNRAS}
+\newcommand*\na{New A}
+\newcommand*\nar{New A Rev.}
+\newcommand*\nat{Nature}
+\newcommand*\nphysa{Nucl.~Phys.~A}
+\newcommand*\pasa{PASA}
+\newcommand*\pasj{PASJ}
+\newcommand*\pasp{PASP}
+\newcommand*\physrep{Phys.~Rep.}
+\newcommand*\physscr{Phys.~Scr}
+\newcommand*\planss{Planet.~Space~Sci.}
+\newcommand*\pra{Phys.~Rev.~A}
+\newcommand*\prb{Phys.~Rev.~B}
+\newcommand*\prc{Phys.~Rev.~C}
+\newcommand*\prd{Phys.~Rev.~D}
+\newcommand*\pre{Phys.~Rev.~E}
+\newcommand*\prl{Phys.~Rev.~Lett.}
+\newcommand*\procspie{Proc.~SPIE}
+\newcommand*\qjras{QJRAS}
+\newcommand*\rmxaa{Rev. Mexicana Astron. Astrofis.}
+\newcommand*\skytel{S\&T}
+\newcommand*\solphys{Sol.~Phys.}
+\newcommand*\sovast{Soviet~Ast.}
+\newcommand*\ssr{Space~Sci.~Rev.}
+\newcommand*\zap{ZAp}
+
+
+\endinput
+%%
+%% End of file `ar.cls'.
diff --git a/theory/SinkParticles/ar-style2.bst b/theory/SinkParticles/ar-style2.bst
new file mode 100644
index 0000000000000000000000000000000000000000..f365f2341562e95d139483772f103ecc21e90240
--- /dev/null
+++ b/theory/SinkParticles/ar-style2.bst
@@ -0,0 +1,1565 @@
+%
+% Changed \begin{thebibliography} argument as {}
+% Changed format.lab.names to include 3 authors citation.
+%%
+%% This is file `ar-style2.bst',
+%% generated with the docstrip utility.
+%%
+%% The original source files were:
+%%
+%% merlin.mbs  (with options: `,ay,nat,nm-rvx,nmlm,x6,m5,dt-beg,yr-per,yrp-per,note-yr,jtit-x,vnum-x,pp-last,jnm-x,add-pub,edby,edbyx,fin-bare,ppx,ed,abr,ednx,ord,jabr,amper,and-xcom,xand,em-it,nfss')
+%% ----------------------------------------
+%% *** Annual Reviews: Harvard Style (not numbered, excluding titles) ***
+%% 
+ %-------------------------------------------------------------------
+ % The original source file contains the following version information:
+ % \ProvidesFile{merlin.mbs}[1998/02/25 3.85a (PWD)]
+ %
+ % NOTICE:
+ % This file may be used for non-profit purposes.
+ % It may not be distributed in exchange for money,
+ %   other than distribution costs.
+ %
+ % The author provides it `as is' and does not guarantee it in any way.
+ %
+ % Copyright (C) 1994-98 Patrick W. Daly
+ %-------------------------------------------------------------------
+ %   For use with BibTeX version 0.99a or later
+ %-------------------------------------------------------------------
+ % This bibliography style file is intended for texts in ENGLISH
+ % This is an author-year citation style bibliography. As such, it is
+ % non-standard LaTeX, and requires a special package file to function properly.
+ % Such a package is    natbib.sty   by Patrick W. Daly
+ % The form of the \bibitem entries is
+ %   \bibitem[Jones et al.(1990)]{key}...
+ %   \bibitem[Jones et al.(1990)Jones, Baker, and Smith]{key}...
+ % The essential feature is that the label (the part in brackets) consists
+ % of the author names, as they should appear in the citation, with the year
+ % in parentheses following. There must be no space before the opening
+ % parenthesis!
+ % With natbib v5.3, a full list of authors may also follow the year.
+ % In natbib.sty, it is possible to define the type of enclosures that is
+ % really wanted (brackets or parentheses), but in either case, there must
+ % be parentheses in the label.
+ % The \cite command functions as follows:
+ %   \citet{key} ==>>                Jones et al. (1990)
+ %   \citet*{key} ==>>               Jones, Baker, and Smith (1990)
+ %   \citep{key} ==>>                (Jones et al., 1990)
+ %   \citep*{key} ==>>               (Jones, Baker, and Smith, 1990)
+ %   \citep[chap. 2]{key} ==>>       (Jones et al., 1990, chap. 2)
+ %   \citep[e.g.][]{key} ==>>        (e.g. Jones et al., 1990)
+ %   \citep[e.g.][p. 32]{key} ==>>   (e.g. Jones et al., p. 32)
+ %   \citeauthor{key} ==>>           Jones et al.
+ %   \citeauthor*{key} ==>>          Jones, Baker, and Smith
+ %   \citeyear{key} ==>>             1990
+ %---------------------------------------------------------------------
+
+ENTRY
+  { address
+    author
+    booktitle
+    chapter
+    edition
+    editor
+    howpublished
+    institution
+    journal
+    key
+    month
+    note
+    number
+    organization
+    pages
+    publisher
+    school
+    series
+    title
+    type
+    volume
+	issue
+    year
+  }
+  {}
+  { label extra.label sort.label short.list }
+
+INTEGERS { output.state before.all mid.sentence after.sentence after.block }
+
+FUNCTION {init.state.consts}
+{ #0 'before.all :=
+  #1 'mid.sentence :=
+  #2 'after.sentence :=
+  #3 'after.block :=
+}
+
+STRINGS { s t }
+
+FUNCTION {output.nonnull}
+{ 's :=
+  output.state mid.sentence =
+    { ", " * write$ }
+    { output.state after.block =
+        { add.period$ write$
+          newline$
+%          "\newblock " write$
+        }
+        { output.state before.all =
+            'write$
+            { add.period$ " " * write$ }
+          if$
+        }
+      if$
+      mid.sentence 'output.state :=
+    }
+  if$
+  s
+}
+
+FUNCTION {output}
+{ duplicate$ empty$
+    'pop$
+    'output.nonnull
+  if$
+}
+
+FUNCTION {output.check}
+{ 't :=
+  duplicate$ empty$
+    { pop$ "empty " t * " in " * cite$ * warning$ }
+    'output.nonnull
+  if$
+}
+
+FUNCTION {fin.entry}
+{ duplicate$ empty$
+    'pop$
+    'write$
+  if$
+  newline$
+}
+
+FUNCTION {new.block}
+{ output.state before.all =
+    'skip$
+    { after.block 'output.state := }
+  if$
+}
+
+FUNCTION {new.sentence}
+{ output.state after.block =
+    'skip$
+    { output.state before.all =
+        'skip$
+        { after.sentence 'output.state := }
+      if$
+    }
+  if$
+}
+
+FUNCTION {add.blank}
+{  " " * before.all 'output.state :=
+}
+
+FUNCTION {add.wsblank}
+{  "" * before.all 'output.state :=
+}
+
+FUNCTION {date.block}
+{
+  new.block
+}
+
+FUNCTION {not}
+{   { #0 }
+    { #1 }
+  if$
+}
+
+FUNCTION {and}
+{   'skip$
+    { pop$ #0 }
+  if$
+}
+
+FUNCTION {or}
+{   { pop$ #1 }
+    'skip$
+  if$
+}
+
+FUNCTION {new.block.checkb}
+{ empty$
+  swap$ empty$
+  and
+    'skip$
+    'new.block
+  if$
+}
+
+FUNCTION {field.or.null}
+{ duplicate$ empty$
+    { pop$ "" }
+    'skip$
+  if$
+}
+
+FUNCTION {emphasize}
+{ duplicate$ empty$
+    { pop$ "" }
+    { "\textit{" swap$ * "}" * }
+  if$
+}
+
+FUNCTION {capitalize}
+{ "u" change.case$ "t" change.case$ }
+
+FUNCTION {space.word}
+{ " " swap$ * " " * }
+
+ % Here are the language-specific definitions for explicit words.
+ % Each function has a name bbl.xxx where xxx is the English word.
+ % The language selected here is ENGLISH
+FUNCTION {bbl.and}
+{ "and"}
+
+FUNCTION {bbl.editors}
+{ "eds." }
+
+FUNCTION {bbl.editor}
+{ "ed." }
+
+FUNCTION {bbl.edby}
+{ "edited by" }
+
+FUNCTION {bbl.edition}
+{ "ed." }
+
+FUNCTION {bbl.volume}
+{ "vol." }
+
+FUNCTION {bbl.of}
+{ "of" }
+
+FUNCTION {bbl.number}
+{ "no." }
+
+FUNCTION {bbl.nr}
+{ "no." }
+
+FUNCTION {bbl.in}
+{ "in" }
+
+FUNCTION {bbl.pages}
+{ "" }
+
+FUNCTION {bbl.page}
+{ "" }
+
+FUNCTION {bbl.chapter}
+{ "chap." }
+
+FUNCTION {bbl.techrep}
+{ "Tech. Rep." }
+
+FUNCTION {bbl.mthesis}
+{ "Master's thesis" }
+
+FUNCTION {bbl.phdthesis}
+{ "Ph.D. thesis" }
+
+FUNCTION {bbl.first}
+{ "1st" }
+
+FUNCTION {bbl.second}
+{ "2nd" }
+
+FUNCTION {bbl.third}
+{ "3rd" }
+
+FUNCTION {bbl.fourth}
+{ "4th" }
+
+FUNCTION {bbl.fifth}
+{ "5th" }
+
+FUNCTION {bbl.st}
+{ "st" }
+
+FUNCTION {bbl.nd}
+{ "nd" }
+
+FUNCTION {bbl.rd}
+{ "rd" }
+
+FUNCTION {bbl.th}
+{ "th" }
+
+MACRO {jan} {"Jan."}
+
+MACRO {feb} {"Feb."}
+
+MACRO {mar} {"Mar."}
+
+MACRO {apr} {"Apr."}
+
+MACRO {may} {"May"}
+
+MACRO {jun} {"Jun."}
+
+MACRO {jul} {"Jul."}
+
+MACRO {aug} {"Aug."}
+
+MACRO {sep} {"Sep."}
+
+MACRO {oct} {"Oct."}
+
+MACRO {nov} {"Nov."}
+
+MACRO {dec} {"Dec."}
+
+FUNCTION {eng.ord}
+{ duplicate$ "1" swap$ *
+  #-2 #1 substring$ "1" =
+     { bbl.th * }
+     { duplicate$ #-1 #1 substring$
+       duplicate$ "1" =
+         { pop$ bbl.st * }
+         { duplicate$ "2" =
+             { pop$ bbl.nd * }
+             { "3" =
+                 { bbl.rd * }
+                 { bbl.th * }
+               if$
+             }
+           if$
+          }
+       if$
+     }
+   if$
+}
+
+MACRO {acmcs} {"ACM Comput. Surv."}
+
+MACRO {acta} {"Acta Inf."}
+
+MACRO {cacm} {"Commun. ACM"}
+
+MACRO {ibmjrd} {"IBM J. Res. Dev."}
+
+MACRO {ibmsj} {"IBM Syst.~J."}
+
+MACRO {ieeese} {"IEEE Trans. Softw. Eng."}
+
+MACRO {ieeetc} {"IEEE Trans. Comput."}
+
+MACRO {ieeetcad}
+ {"IEEE Trans. Comput.-Aided Design Integrated Circuits"}
+
+MACRO {ipl} {"Inf. Process. Lett."}
+
+MACRO {jacm} {"J.~ACM"}
+
+MACRO {jcss} {"J.~Comput. Syst. Sci."}
+
+MACRO {scp} {"Sci. Comput. Programming"}
+
+MACRO {sicomp} {"SIAM J. Comput."}
+
+MACRO {tocs} {"ACM Trans. Comput. Syst."}
+
+MACRO {tods} {"ACM Trans. Database Syst."}
+
+MACRO {tog} {"ACM Trans. Gr."}
+
+MACRO {toms} {"ACM Trans. Math. Softw."}
+
+MACRO {toois} {"ACM Trans. Office Inf. Syst."}
+
+MACRO {toplas} {"ACM Trans. Prog. Lang. Syst."}
+
+MACRO {tcs} {"Theoretical Comput. Sci."}
+
+INTEGERS { nameptr namesleft numnames }
+
+FUNCTION {format.names}
+{ 's :=
+  #1 'nameptr :=
+  s num.names$ 'numnames :=
+  numnames 'namesleft :=
+    { namesleft #0 > }
+    { s nameptr
+      "{vv~}{ll}{ jj}{ f{}}" format.name$
+    't :=
+      nameptr #1 >
+        {
+          nameptr #6 =
+          numnames #6 > and
+            { "others" 't :=
+              #1 'namesleft := }
+            'skip$
+          if$
+          namesleft #1 >
+            { ", " * t * }
+            {
+              "," *
+              s nameptr "{ll}" format.name$ duplicate$ "others" =
+                { 't := }
+                { pop$ }
+              if$
+              t "others" =
+                {
+                  " et~al." *
+                }
+                { " " * t * }
+              if$
+            }
+          if$
+        }
+        't
+      if$
+      nameptr #1 + 'nameptr :=
+      namesleft #1 - 'namesleft :=
+    }
+  while$
+}
+FUNCTION {format.names.ed}
+{ 's :=
+  #1 'nameptr :=
+  s num.names$ 'numnames :=
+  numnames 'namesleft :=
+    { namesleft #0 > }
+    { s nameptr
+      "{f{}~}{vv~}{ll}{ jj}"
+      format.name$
+      't :=
+      nameptr #1 >
+        {
+          namesleft #1 >
+            { ", " * t * }
+            {
+              "," *
+              s nameptr "{ll}" format.name$ duplicate$ "others" =
+                { 't := }
+                { pop$ }
+              if$
+              t "others" =
+                {
+                  " et~al." *
+                }
+                { " " * t * }
+              if$
+            }
+          if$
+        }
+        't
+      if$
+      nameptr #1 + 'nameptr :=
+      namesleft #1 - 'namesleft :=
+    }
+  while$
+}
+
+FUNCTION {format.key}
+{ empty$
+    { key field.or.null }
+    { "" }
+  if$
+}
+
+FUNCTION {format.authors}
+{ author empty$
+    { "" }
+    { author format.names }
+  if$
+}
+
+FUNCTION {format.issues}
+{ issue empty$
+    { "" }
+    { "(" * issue ")" * }
+  if$
+}
+
+FUNCTION {format.numbers}
+{ number empty$
+    { "" }
+    { "(" * number ")" * }
+  if$ 
+}
+
+FUNCTION {format.editors}
+{ editor empty$
+    { "" }
+    { editor format.names
+      editor num.names$ #1 >
+        { ", " * bbl.editors * }
+        { ", " * bbl.editor * }
+      if$
+    }
+  if$
+}
+
+FUNCTION {format.in.editors}
+{ editor empty$
+    { "" }
+    { editor format.names.ed
+    }
+  if$
+}
+
+FUNCTION {format.note}
+{ note empty$
+    { "" }
+    { note #1 #1 substring$
+      duplicate$ "{" =
+        'skip$
+        { output.state mid.sentence =
+          { "l" }
+          { "u" }
+        if$
+        change.case$
+        }
+      if$
+      note #2 global.max$ substring$ *
+    }
+  if$
+}
+
+FUNCTION {format.title}
+{ title empty$
+    { "" }
+    { title "t" change.case$
+    }
+  if$
+}
+
+FUNCTION {format.full.names}
+{'s :=
+  #1 'nameptr :=
+  s num.names$ 'numnames :=
+  numnames 'namesleft :=
+    { namesleft #0 > }
+    { s nameptr
+      "{vv~}{ll}" format.name$
+      't :=
+      nameptr #1 >
+        {
+          nameptr #6 =
+          numnames #6 > and
+            { "others" 't :=
+              #1 'namesleft := }
+            'skip$
+          if$
+          namesleft #1 >
+            { ", " * t * }
+            {
+              s nameptr "{ll}" format.name$ duplicate$ "others" =
+                { 't := }
+                { pop$ }
+              if$
+              t "others" =
+                {
+                  " et~al." *
+                }
+                { " \& " * t * }
+              if$
+            }
+          if$
+        }
+        't
+      if$
+      nameptr #1 + 'nameptr :=
+      namesleft #1 - 'namesleft :=
+    }
+  while$
+}
+
+FUNCTION {author.editor.key.full}
+{ author empty$
+    { editor empty$
+        { key empty$
+            { cite$ #1 #3 substring$ }
+            'key
+          if$
+        }
+        { editor format.full.names }
+      if$
+    }
+    { author format.full.names }
+  if$
+}
+
+FUNCTION {author.key.full}
+{ author empty$
+    { key empty$
+         { cite$ #1 #3 substring$ }
+          'key
+      if$
+    }
+    { author format.full.names }
+  if$
+}
+
+FUNCTION {editor.key.full}
+{ editor empty$
+    { key empty$
+         { cite$ #1 #3 substring$ }
+          'key
+      if$
+    }
+    { editor format.full.names }
+  if$
+}
+
+FUNCTION {make.full.names}
+{ type$ "book" =
+  type$ "inbook" =
+  or
+    'author.editor.key.full
+    { type$ "proceedings" =
+        'editor.key.full
+        'author.key.full
+      if$
+    }
+  if$
+}
+
+FUNCTION {output.bibitem}
+{ newline$
+  "\bibitem[{" write$
+  label write$
+  ")" make.full.names duplicate$ short.list =
+     { pop$ }
+     { * }
+   if$
+  "}]{" * write$
+  cite$ write$
+  "}" write$
+  newline$
+  ""
+  before.all 'output.state :=
+}
+
+FUNCTION {n.dashify}
+{
+  't :=
+  ""
+    { t empty$ not }
+    { t #1 #1 substring$ "-" =
+        { t #1 #2 substring$ "--" = not
+            { "--" *
+              t #2 global.max$ substring$ 't :=
+            }
+            {   { t #1 #1 substring$ "-" = }
+                { "-" *
+                  t #2 global.max$ substring$ 't :=
+                }
+              while$
+            }
+          if$
+        }
+        { t #1 #1 substring$ *
+          t #2 global.max$ substring$ 't :=
+        }
+      if$
+    }
+  while$
+}
+
+FUNCTION {word.in}
+{ bbl.in capitalize
+  " " * }
+
+FUNCTION {format.date}
+{ year duplicate$ empty$
+    { "empty year in " cite$ * "; set to ????" * warning$
+       pop$ "????" }
+    'skip$
+  if$
+  extra.label *
+  before.all 'output.state :=
+  after.sentence 'output.state :=
+}
+
+FUNCTION {format.btitle}
+{ title emphasize
+}
+
+FUNCTION {tie.or.space.connect}
+{ duplicate$ text.length$ #3 <
+    { "~" }
+    { " " }
+  if$
+  swap$ * *
+}
+
+FUNCTION {either.or.check}
+{ empty$
+    'pop$
+    { "can't use both " swap$ * " fields in " * cite$ * warning$ }
+  if$
+}
+
+FUNCTION {format.bvolume}
+{ volume empty$
+    { "" }
+    { bbl.volume volume tie.or.space.connect
+      series empty$
+        'skip$
+        { bbl.of space.word * series emphasize * }
+      if$
+      "volume and number" number either.or.check
+    }
+  if$
+}
+
+FUNCTION {format.number.series}
+{ volume empty$
+    { number empty$
+        { series field.or.null }
+        { output.state mid.sentence =
+            { bbl.number }
+            { bbl.number capitalize }
+          if$
+          number tie.or.space.connect
+          series empty$
+            { "there's a number but no series in " cite$ * warning$ }
+            { bbl.in space.word * series * }
+          if$
+        }
+      if$
+    }
+    { "" }
+  if$
+}
+
+FUNCTION {is.num}
+{ chr.to.int$
+  duplicate$ "0" chr.to.int$ < not
+  swap$ "9" chr.to.int$ > not and
+}
+
+FUNCTION {extract.num}
+{ duplicate$ 't :=
+  "" 's :=
+  { t empty$ not }
+  { t #1 #1 substring$
+    t #2 global.max$ substring$ 't :=
+    duplicate$ is.num
+      { s swap$ * 's := }
+      { pop$ "" 't := }
+    if$
+  }
+  while$
+  s empty$
+    'skip$
+    { pop$ s }
+  if$
+}
+
+FUNCTION {convert.edition}
+{ edition extract.num "l" change.case$ 's :=
+  s "first" = s "1" = or
+    { bbl.first 't := }
+    { s "second" = s "2" = or
+        { bbl.second 't := }
+        { s "third" = s "3" = or
+            { bbl.third 't := }
+            { s "fourth" = s "4" = or
+                { bbl.fourth 't := }
+                { s "fifth" = s "5" = or
+                    { bbl.fifth 't := }
+                    { s #1 #1 substring$ is.num
+                        { s eng.ord 't := }
+                        { edition 't := }
+                      if$
+                    }
+                  if$
+                }
+              if$
+            }
+          if$
+        }
+      if$
+    }
+  if$
+  t
+}
+
+FUNCTION {format.edition}
+{ edition empty$
+    { "" }
+    { output.state mid.sentence =
+        { convert.edition "l" change.case$ " " * bbl.edition * }
+        { convert.edition "t" change.case$ " " * bbl.edition * }
+      if$
+    }
+  if$
+}
+
+INTEGERS { multiresult }
+
+FUNCTION {multi.page.check}
+{ 't :=
+  #0 'multiresult :=
+    { multiresult not
+      t empty$ not
+      and
+    }
+    { t #1 #1 substring$
+      duplicate$ "-" =
+      swap$ duplicate$ "," =
+      swap$ "+" =
+      or or
+        { #1 'multiresult := }
+        { t #2 global.max$ substring$ 't := }
+      if$
+    }
+  while$
+  multiresult
+}
+
+FUNCTION {format.pages}
+{ pages empty$
+    { "" }
+    { pages multi.page.check
+        { bbl.pages pages n.dashify tie.or.space.connect }
+        { bbl.page pages tie.or.space.connect }
+      if$
+    }
+  if$
+}
+
+FUNCTION {format.journal.pages}
+{ pages empty$
+    'skip$
+    { duplicate$ empty$
+        { pop$ format.pages }
+        {
+          ":" *
+          pages n.dashify *
+        }
+      if$
+    }
+  if$
+}
+
+FUNCTION {format.vol.num.pages}
+{ 
+  volume field.or.null
+}
+
+FUNCTION {format.chapter.pages}
+{ chapter empty$
+    { "" }
+    { type empty$
+        { bbl.chapter }
+        { type "l" change.case$ }
+      if$
+      chapter tie.or.space.connect
+    }
+  if$
+}
+
+FUNCTION {format.in.ed.booktitle}
+{ booktitle empty$
+    { "" }
+    { editor empty$
+        { word.in booktitle emphasize * }
+        { word.in booktitle emphasize *
+          ", " *
+          editor num.names$ #1 >
+            { bbl.editors }
+            { bbl.editor }
+          if$
+          * " " *
+          format.in.editors *
+        }
+      if$
+    }
+  if$
+}
+
+FUNCTION {format.thesis.type}
+{ type empty$
+    'skip$
+    { pop$
+      type "t" change.case$
+    }
+  if$
+}
+
+FUNCTION {format.tr.number}
+{ type empty$
+    { bbl.techrep }
+    'type
+  if$
+  number empty$
+    { "t" change.case$ }
+    { number tie.or.space.connect }
+  if$
+}
+
+FUNCTION {format.article.crossref}
+{
+  word.in
+  " \cite{" * crossref * "}" *
+}
+
+FUNCTION {format.book.crossref}
+{ volume empty$
+    { "empty volume in " cite$ * "'s crossref of " * crossref * warning$
+      word.in
+    }
+    { bbl.volume capitalize
+      volume tie.or.space.connect
+      bbl.of space.word *
+    }
+  if$
+  " \cite{" * crossref * "}" *
+}
+
+FUNCTION {format.incoll.inproc.crossref}
+{
+  word.in
+  " \cite{" * crossref * "}" *
+}
+
+FUNCTION {format.publisher}
+{ publisher empty$
+    { "empty publisher in " cite$ * warning$ }
+    'skip$
+  if$
+  ""
+  address empty$ publisher empty$ and
+    'skip$
+    {
+      address empty$
+        'skip$
+        { address * }
+      if$
+      publisher empty$
+        'skip$
+        { address empty$
+            'skip$
+            { ": " * }
+          if$
+          publisher *
+        }
+      if$
+    }
+  if$
+  output
+}
+
+FUNCTION {article}
+{ output.bibitem
+  format.authors "author" output.check
+  author format.key output
+  format.date "year" output.check
+  date.block
+  crossref missing$
+    { journal
+      emphasize
+      "journal" output.check
+      add.blank
+      format.vol.num.pages output
+	  add.wsblank
+      format.numbers output
+	  add.wsblank
+      format.issues output
+    }
+    { format.article.crossref output.nonnull
+      format.pages output
+    }
+  if$
+  format.journal.pages
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {book}
+{ output.bibitem
+  author empty$
+    { format.editors "author and editor" output.check
+      editor format.key output
+    }
+    { format.authors output.nonnull
+      crossref missing$
+        { "author and editor" editor either.or.check }
+        'skip$
+      if$
+    }
+  if$
+  format.date "year" output.check
+  date.block
+  format.btitle "title" output.check
+  date.block
+  crossref missing$
+    { format.bvolume output
+      new.block
+      format.number.series output
+      new.sentence
+      format.publisher
+    }
+    {
+      new.block
+      format.book.crossref output.nonnull
+    }
+  if$
+  format.edition output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {booklet}
+{ output.bibitem
+  format.authors output
+  author format.key output
+  format.date "year" output.check
+  date.block
+  format.title "title" output.check
+  new.block
+  howpublished output
+  address output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {inbook}
+{ output.bibitem
+  author empty$
+    { format.editors "author and editor" output.check
+      editor format.key output
+    }
+    { format.authors output.nonnull
+      crossref missing$
+        { "author and editor" editor either.or.check }
+        'skip$
+      if$
+    }
+  if$
+  format.date "year" output.check
+  date.block
+  format.btitle "title" output.check
+  crossref missing$
+    {
+      format.bvolume output
+      format.chapter.pages "chapter and pages" output.check
+      new.block
+      format.number.series output
+      new.sentence
+      format.publisher
+    }
+    {
+      format.chapter.pages "chapter and pages" output.check
+      new.block
+      format.book.crossref output.nonnull
+    }
+  if$
+  format.edition output
+  format.pages "pages" output.check
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {incollection}
+{ output.bibitem
+  format.authors "author" output.check
+  author format.key output
+  format.date "year" output.check
+  date.block
+  crossref missing$
+    { format.btitle "title" output.check new.sentence
+	  format.in.ed.booktitle "booktitle" output.check
+      format.bvolume output
+      format.number.series output
+      format.chapter.pages output
+      new.sentence
+      format.publisher
+      format.edition output
+    }
+    { format.incoll.inproc.crossref output.nonnull
+      format.chapter.pages output
+    }
+  if$
+  format.pages "pages" output.check
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {inproceedings}
+{ output.bibitem
+  format.authors "author" output.check
+  author format.key output
+  format.date "year" output.check
+  date.block
+  crossref missing$
+    { format.btitle "title" output.check new.sentence
+	  format.in.ed.booktitle "booktitle" output.check
+      format.bvolume output
+      format.number.series output
+      new.sentence
+      publisher empty$
+        { organization output
+          address output
+        }
+        { organization output
+          format.publisher
+        }
+      if$
+    }
+    { format.incoll.inproc.crossref output.nonnull
+      format.pages output
+    }
+  if$
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {conference} { inproceedings }
+
+FUNCTION {manual}
+{ output.bibitem
+  format.authors output
+  author format.key output
+  format.date "year" output.check
+  date.block
+  format.btitle "title" output.check
+  organization address new.block.checkb
+  organization output
+  address output
+  format.edition output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {mastersthesis}
+{ output.bibitem
+  format.authors "author" output.check
+  author format.key output
+  format.date "year" output.check
+  date.block
+  format.btitle "title" output.check
+  new.block
+  bbl.mthesis format.thesis.type output.nonnull
+  school "school" output.check
+  address output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {misc}
+{ output.bibitem
+  format.authors output
+  author format.key output
+  format.date "year" output.check
+  date.block
+  format.title output
+  new.block
+  howpublished output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {phdthesis}
+{ output.bibitem
+  format.authors "author" output.check
+  author format.key output
+  format.date "year" output.check
+  date.block
+  format.btitle "title" output.check
+  new.block
+  bbl.phdthesis format.thesis.type output.nonnull
+  school "school" output.check
+  address output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {proceedings}
+{ output.bibitem
+  format.editors output
+  editor format.key output
+  format.date "year" output.check
+  date.block
+  format.btitle "title" output.check
+  format.bvolume output
+  format.number.series output
+  address output
+  new.sentence
+  organization output
+  publisher output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {techreport}
+{ output.bibitem
+  format.authors "author" output.check
+  author format.key output
+  format.date "year" output.check
+  date.block
+  format.title "title" output.check
+  new.block
+  format.tr.number output.nonnull
+  institution "institution" output.check
+  address output
+  new.block
+  format.note output
+  fin.entry
+}
+
+FUNCTION {unpublished}
+{ output.bibitem
+  format.authors "author" output.check
+  author format.key output
+  format.date "year" output.check
+  date.block
+  format.title "title" output.check
+  new.block
+  format.note "note" output.check
+  fin.entry
+}
+
+FUNCTION {default.type} { misc }
+
+READ
+
+FUNCTION {sortify}
+{ purify$
+  "l" change.case$
+}
+
+INTEGERS { len }
+
+FUNCTION {chop.word}
+{ 's :=
+  'len :=
+  s #1 len substring$ =
+    { s len #1 + global.max$ substring$ }
+    's
+  if$
+}
+
+FUNCTION {format.lab.names}
+{ 's :=
+  s #1 "{vv~}{ll}" format.name$
+  s num.names$ duplicate$
+  #2 >
+    { pop$
+      " et~al." *
+    }
+    { #3 <
+        { s num.names$ #2 <
+           'skip$
+           { s #2 "{ff }{vv }{ll}{ jj}" format.name$ "others" =
+               {
+                 " et~al." *
+               }
+               { " \& " * s #2 "{vv~}{ll}" format.name$
+                 * }
+             if$
+           }
+         if$
+        }
+        {  s #3 "{ff }{vv }{ll}{ jj}" format.name$ "others" =
+           'skip$
+           { s #3 "{ff }{vv }{ll}{ jj}" format.name$ "others" =
+               {
+                 " et~al." *
+               }
+               { ", " * s #2 "{vv~}{ll}" format.name$ * " \& " * s #3 "{vv~}{ll}" format.name$
+                 * }
+             if$
+           }
+          if$
+        }
+      if$
+    }
+  if$
+}
+
+FUNCTION {author.key.label}
+{ author empty$
+    { key empty$
+        { cite$ #1 #3 substring$ }
+        'key
+      if$
+    }
+    { author format.lab.names }
+  if$
+}
+
+FUNCTION {author.editor.key.label}
+{ author empty$
+    { editor empty$
+        { key empty$
+            { cite$ #1 #3 substring$ }
+            'key
+          if$
+        }
+        { editor format.lab.names }
+      if$
+    }
+    { author format.lab.names }
+  if$
+}
+
+FUNCTION {editor.key.label}
+{ editor empty$
+    { key empty$
+        { cite$ #1 #3 substring$ }
+        'key
+      if$
+    }
+    { editor format.lab.names }
+  if$
+}
+
+FUNCTION {calc.short.authors}
+{ type$ "book" =
+  type$ "inbook" =
+  or
+    'author.editor.key.label
+    { type$ "proceedings" =
+        'editor.key.label
+        'author.key.label
+      if$
+    }
+  if$
+  'short.list :=
+}
+
+FUNCTION {calc.label}
+{ calc.short.authors
+  short.list
+  "("
+  *
+  year duplicate$ empty$
+     { pop$ "????" }
+     'skip$
+  if$
+  *
+  'label :=
+}
+
+FUNCTION {sort.format.names}
+{ 's :=
+  #1 'nameptr :=
+  ""
+  s num.names$ 'numnames :=
+  numnames 'namesleft :=
+    { namesleft #0 > }
+    { s nameptr
+      "{vv{ } }{ll{ }}{  f{ }}{  jj{ }}"
+      format.name$ 't :=
+      nameptr #1 >
+        {
+          nameptr #6 =
+          numnames #6 > and
+            { "others" 't :=
+              #1 'namesleft := }
+            'skip$
+          if$
+          "   "  *
+          namesleft #1 = t "others" = and
+            { "zzzzz" * }
+            { t sortify * }
+          if$
+        }
+        { t sortify * }
+      if$
+      nameptr #1 + 'nameptr :=
+      namesleft #1 - 'namesleft :=
+    }
+  while$
+}
+
+FUNCTION {sort.format.title}
+{ 't :=
+  "A " #2
+    "An " #3
+      "The " #4 t chop.word
+    chop.word
+  chop.word
+  sortify
+  #1 global.max$ substring$
+}
+
+FUNCTION {author.sort}
+{ author empty$
+    { key empty$
+        { "to sort, need author or key in " cite$ * warning$
+          ""
+        }
+        { key sortify }
+      if$
+    }
+    { author sort.format.names }
+  if$
+}
+
+FUNCTION {author.editor.sort}
+{ author empty$
+    { editor empty$
+        { key empty$
+            { "to sort, need author, editor, or key in " cite$ * warning$
+              ""
+            }
+            { key sortify }
+          if$
+        }
+        { editor sort.format.names }
+      if$
+    }
+    { author sort.format.names }
+  if$
+}
+
+FUNCTION {editor.sort}
+{ editor empty$
+    { key empty$
+        { "to sort, need editor or key in " cite$ * warning$
+          ""
+        }
+        { key sortify }
+      if$
+    }
+    { editor sort.format.names }
+  if$
+}
+
+FUNCTION {presort}
+{ calc.label
+  label sortify
+  "    "
+  *
+  type$ "book" =
+  type$ "inbook" =
+  or
+    'author.editor.sort
+    { type$ "proceedings" =
+        'editor.sort
+        'author.sort
+      if$
+    }
+  if$
+  #1 entry.max$ substring$
+  'sort.label :=
+  sort.label
+  *
+  "    "
+  *
+  title field.or.null
+  sort.format.title
+  *
+  #1 entry.max$ substring$
+  'sort.key$ :=
+}
+
+ITERATE {presort}
+
+SORT
+
+STRINGS { last.label next.extra }
+
+INTEGERS { last.extra.num number.label }
+
+FUNCTION {initialize.extra.label.stuff}
+{ #0 int.to.chr$ 'last.label :=
+  "" 'next.extra :=
+  #0 'last.extra.num :=
+  #0 'number.label :=
+}
+
+FUNCTION {forward.pass}
+{ last.label label =
+    { last.extra.num #1 + 'last.extra.num :=
+      last.extra.num int.to.chr$ 'extra.label :=
+    }
+    { "a" chr.to.int$ 'last.extra.num :=
+      "" 'extra.label :=
+      label 'last.label :=
+    }
+  if$
+  number.label #1 + 'number.label :=
+}
+
+FUNCTION {reverse.pass}
+{ next.extra "b" =
+    { "a" 'extra.label := }
+    'skip$
+  if$
+  extra.label 'next.extra :=
+  extra.label
+  duplicate$ empty$
+    'skip$
+    { "{\natexlab{" swap$ * "}}" * }
+  if$
+  'extra.label :=
+  label extra.label * 'label :=
+}
+
+EXECUTE {initialize.extra.label.stuff}
+
+ITERATE {forward.pass}
+
+REVERSE {reverse.pass}
+
+FUNCTION {bib.sort.order}
+{ sort.label
+  "    "
+  *
+  year field.or.null sortify
+  *
+  "    "
+  *
+  title field.or.null
+  sort.format.title
+  *
+  #1 entry.max$ substring$
+  'sort.key$ :=
+}
+
+ITERATE {bib.sort.order}
+
+SORT
+
+FUNCTION {begin.bib}
+{ preamble$ empty$
+    'skip$
+    { preamble$ write$ newline$ }
+  if$
+  "\begin{thebibliography}{}"
+  write$ newline$
+  "\expandafter\ifx\csname natexlab\endcsname\relax\def\natexlab#1{#1}\fi"
+  write$ newline$
+}
+
+EXECUTE {begin.bib}
+
+EXECUTE {init.state.consts}
+
+ITERATE {call.type$}
+
+FUNCTION {end.bib}
+{ newline$
+  "\end{thebibliography}" write$ newline$
+}
+
+EXECUTE {end.bib}
+%% End of customized bst file
+%%
+%% End of file `ar-style2.bst'.
diff --git a/theory/SinkParticles/gear_imf_sampling.tex b/theory/SinkParticles/gear_imf_sampling.tex
new file mode 100644
index 0000000000000000000000000000000000000000..4aeb36227d517ac065b17085eebcb096f9ae0092
--- /dev/null
+++ b/theory/SinkParticles/gear_imf_sampling.tex
@@ -0,0 +1,97 @@
+% Note: The content of this file was taken and adapted from Darwin Roduit's master thesis (June 2024).
+% The algorithm was written based on documents of Yves Revaz. 
+% The latex file was simplified to be part of SWIFT. 
+
+\documentclass[a4paper]{ar-1col-S2O}
+
+% Math packages
+\usepackage{amsmath}
+\usepackage{amssymb}
+\usepackage{amsfonts}
+
+% Physics package
+\usepackage{physics}
+
+% Algorithm packages
+\usepackage{algorithm}
+\usepackage{algpseudocodex} 
+
+\begin{document}  %------------------------------------------------
+\section{IMF Sampling}
+\label{sec:imf_sampling}
+
+Until now, we haven't explored the details of the IMF sampling and the math behind it. It is time to change that. 
+
+Assume that we have an IMF. We want to sample it correctly to produce star populations. The challenge is to produce a fast algorithm that provides the correct targeted masses. The algorithm must be computationally efficient; otherwise, we may not gain any computational time by creating sinks compared to physically detailed and motivated star formation schemes. 
+
+We do not want to produce too many low-mass stars (low-mass stars dominate the IMF) since it would require immense memory and computational power without significant benefits. Indeed, low-mass stars have a weaker impact on galaxy formation and evolution compared to massive stars that undergo supernovae explosions. To this end, the IMF is split into two parts: continuous and discrete. The separating mass is called $m_t$. In the continuous part, a star particle represents a star population with masses below $m_t$. \\
+Moreover, such particles all have the same mass $m_{\text{SP}}$. In the discrete IMF part, star particles represent individual stars with different masses. Notice that $m_{\text{SP}}$ and $m_t$ are parameters the user sets. Figure \ref{fig:sink_imf} shows the IMF in two parts.
+
+\begin{figure}[b]
+  \includegraphics[scale=0.7]{sink_imf}
+  \caption{This figure shows an IMF split into two parts: the continuous (orange) with mass and the discrete (blue) part with respective mass $M_c$ and $M_d$. The separating mass is called $m_t$. \emph{Source}: Roduit Darwin}
+  \label{fig:sink_imf}
+\end{figure}
+
+Let us define $M_c$ the mass of the continuous part of the IMF and $M_d$ the mass of the discrete part by the following equation:
+%
+\begin{equation}
+    M_c =  \int_{m_\text{min}}^{m_t} \Phi(m) \dd m \quad \text{and} \quad  M_d =  \int_{m_t}^{m_\text{max}} \Phi(m) \dd m \, ,
+\end{equation}
+%
+where $\Phi(m)$ is the initial mass function. 
+
+Similarly, we can define $N_c$ as the number of stars in the continuous part of the IMF and $N_d$ as the one in the discrete part:
+%
+\begin{equation}
+    N_c =  \int_{m_\text{min}}^{m_t} \frac{\Phi(m)}{m} \dd m \quad \text{and} \quad  N_d =  \int_{m_t}^{m_\text{max}} \frac{\Phi(m)}{m} \dd m \, .
+\end{equation}
+%
+Another important definition is the number of particles (not stars) $N_{\text{SP}}$ of mass $m_{\text{SP}}$ that will populate the continuous part of the IMF. Thus, the total number of stars that the IMF will generate is:
+%
+\begin{equation}
+    N_{\text{tot}} = N_{\text{SP}} + N_d \, .
+\end{equation}
+%
+$N_{\text{SP}}$ is obtained by dividing the mass $M_c$ of the continuous part of the IMF by the mass of the stellar particle $m_{\text{SP}}$:
+%
+\begin{equation}
+    N_{\text{SP}} = \frac{M_c}{m_{\text{SP}}} \, .
+\end{equation}
+%
+Now, we need to find the probability $P_c$ to spawn a star particle of mass $m_\text{SP}$ and $P_c$ the probability to spawn individual stars representing the discrete part of the IMF. With all the above definitions, it is easy to find those probabilities. Indeed, $P_c$ is given by:
+%
+\begin{equation}
+    P_c = \frac{N_{\text{SP}}}{N_{\text{tot}}} = \frac{N_{\text{SP}}}{N_{\text{SP}} + N_d} \quad \text{and} \quad P_d = 1 - P_c \, .
+\end{equation}
+%
+If we assume that only a star particle will contain all stars below $m_t$, i.e. $N_{\text{SP}} = 1$, we find:
+%
+\begin{equation}
+    P_c = \frac{1}{1 + N_d} \, . 
+\end{equation}
+%
+Therefore, the algorithm of the IMF sampling is presented in Algorithm \ref{algo:imf_sampling}. In this algorithm, we have assumed to have a function \texttt{sample\_IMF\_high()} that correctly samples the IMF for the discrete part. This algorithm is called whenever we need to set the target mass of the sink. So, it is called once a new sink is formed, if a sink exists in the inital conditions or after having spawned a star. \\
+
+This algorithm is the same for populations II and III stars. However, the IMF parameters (mainly the minimal IMF mass and maximal IMF mass) and the two free parameters $m_t$ and $m_{SP}$ can vary.
+
+\begin{algorithm}
+  \begin{algorithmic}
+      \State $\mathtt{random\_number} \gets \text{draw an random number in the interval }\left(0, 1 \right]$ 
+      \If{$\mathtt{random\_number} < P_c$}
+        \State $\mathtt{target\_mass} \gets m_{\text{SP}}$
+      \Else
+        \State $\mathtt{target\_mass} \gets \mathtt{sample\_IMF\_high()}$
+      \EndIf
+      \Return{$\mathtt{target\_mass}$}
+\end{algorithmic}
+\caption{IMF sampling algorithm, also called \texttt{sink\_update\_target\_mass()}. }\label{algo:imf_sampling}
+\end{algorithm}
+
+
+\end{document} %------------------------------------------------
+
+%%% Local Variables:
+%%% mode: latex
+%%% TeX-master: t
+%%% End:
diff --git a/theory/SinkParticles/plot_imf_image.py b/theory/SinkParticles/plot_imf_image.py
new file mode 100644
index 0000000000000000000000000000000000000000..bc1da9cccf1c3fe60af2661e4cf65a96fb949338
--- /dev/null
+++ b/theory/SinkParticles/plot_imf_image.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Jul  12 13:28 2024
+
+This script creates an image of some IMF that we split into the continuous part and discrete part.
+This image illustrates the star spawning algorithm with GEAR sink particles
+
+@author: Darwin Roduit
+"""
+
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib
+
+# For swift doc, use the following and not the styleheet
+plt.rcParams["text.usetex"] = True
+plt.rcParams["font.family"] = "serif"
+plt.rcParams["font.serif"] = ["Computer Modern Roman"]
+
+mmin = 0.5
+mmax = 300
+
+matplotlib.rcParams.update({"font.size": 16})
+
+figsize = (6.4, 4.8)
+fig, ax = plt.subplots(num=1, ncols=1, nrows=1, figsize=figsize, layout="tight")
+ax.set_xlim([0.3, 400])
+ax.set_ylim([1, 5e4])
+
+ax.set_xticks([1, 2, 4, 8, 20, 50, 100, 300])
+ax.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())
+
+ax.set_xlabel("$M_{\star}$ $[M_\odot]$")
+ax.set_ylabel("d$N/$d$M$ [arbitrary units]")
+ax.set_xscale("log")
+ax.set_yscale("log")
+
+# theoretical imf
+s = -1.3
+bins = 10 ** np.linspace(np.log10(mmin), np.log10(mmax), 100)
+n = 0.9 * 10000 * bins ** s
+ax.plot(bins, n, "k--")
+
+bins = 10 ** np.linspace(np.log10(mmin), np.log10(8), 100)
+n = 0.9 * 10000 * bins ** s
+ax.fill_between(bins, 0.1, n, color="red", alpha=0.1)
+
+bins = 10 ** np.linspace(np.log10(8), np.log10(mmax), 100)
+n = 0.9 * 10000 * bins ** s
+ax.fill_between(bins, 0.1, n, color="blue", alpha=0.1)
+
+ax.text(2, 1e2, r"$M_{\rm c}$", horizontalalignment="center")
+ax.text(50, 2, r"$M_{\rm d}$", horizontalalignment="center")
+
+# Add limit
+ax.vlines(x=8, ymin=0, ymax=600, color="k", linestyle="-")
+
+# Add text to the vertical line
+ax.text(8, 800, r"$m_{t}$", horizontalalignment="center")
+
+# fig.patch.set_facecolor('none')  # Remove figure background
+# ax.set_facecolor('none')         # Remove axes background
+
+plt.savefig("sink_imf.png", dpi=300, bbox_inches="tight")
+plt.close()
diff --git a/theory/SinkParticles/run.sh b/theory/SinkParticles/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bafbd115760a1f5e6fd01c079344a8574fe0c907
--- /dev/null
+++ b/theory/SinkParticles/run.sh
@@ -0,0 +1,6 @@
+# First plot the IMF image
+python3 ./plot_imf_image.py
+
+#Then, run latex to produce the output
+pdflatex -jobname=gear_imf_sampling gear_imf_sampling.tex
+
diff --git a/tools/analyse_runtime.py b/tools/analyse_runtime.py
index bfb501d814eb32c5a571a59ed0d789f944659633..e80015972aa42118b464e0a455354d0b0ea43935 100755
--- a/tools/analyse_runtime.py
+++ b/tools/analyse_runtime.py
@@ -152,7 +152,7 @@ for i in range(num_files):
         for i in range(len(tasks)):
 
             # Extract the different blocks
-            if re.search("scheduler_report_task_times: \*\*\*  ", line):
+            if re.search("scheduler_report_task_times: \\*\\*\\*  ", line):
                 if re.search("%s" % tasks[i], line):
                     counts_tasks[i] += 1.0
                     times_tasks[i] += float(
@@ -205,7 +205,7 @@ important_is_rebuild = [0]
 important_is_fof = [0]
 important_is_VR = [0]
 important_is_mesh = [0]
-important_labels = ["Others (all below %.1f\%%)" % (threshold * 100)]
+important_labels = ["Others (all below %.1f\\%%)" % (threshold * 100)]
 need_print = True
 print("Time spent in the different code sections:")
 for i in range(len(labels)):
diff --git a/tools/convert_snapshot_to_ICs.py b/tools/convert_snapshot_to_ICs.py
new file mode 100755
index 0000000000000000000000000000000000000000..f3648a081186a6f0ccb12df606c252aa60354916
--- /dev/null
+++ b/tools/convert_snapshot_to_ICs.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+
+"""
+Script to convert the NIFTY ICs to those that are compatible with SWIFT.
+Note that this leaves h-factors as-is to be fixed in-place by SWIFT.
+
+You will need:
+
+    + swiftsimio
+"""
+
+from swiftsimio import Writer, load
+import numpy as np
+import unyt
+import os
+
+# Which file to read
+#  filename = "./eagle_0000.hdf5"
+filename = "output_0001.hdf5"
+# What to call the output IC file
+output_filename = "ICs.hdf5"
+
+
+# -----------------------------------------------------------------------
+# -----------------------------------------------------------------------
+
+
+if not os.path.exists(filename):
+    raise FileNotFoundError(filename)
+
+warning = """
+SWIFT snapshots are not written in a way that should be used as initial
+conditions. By using this script, you're trying to do exactly that. Are
+you sure you know what you're doing? [y/n]
+"""
+
+answer = input(warning)
+if not answer.startswith("y") or answer.startswith("Y"):
+    print("Quitting.")
+    quit()
+
+# Load data from snapshot.
+
+snap = load(filename)
+
+# Get units and create unyt.unitsystem.
+length = snap.units.length
+mass = snap.units.mass
+time = snap.units.time
+temperature = snap.units.temperature
+
+my_units = unyt.UnitSystem("copiedFromSnapshot", length, mass, time, temperature)
+
+
+# Store additional info about the snapshot in the ICs. Can't hurt to have the data lying around.
+extra_header = {}
+
+if snap.metadata.cosmology is not None:
+
+    #  Accessing data through metadata.cosmology_raw works on older snapshots as
+    #  well, so let's do it this way.
+
+    extra_header[
+        "snapshot cosmology Critical density [internal_units]"
+    ] = snap.metadata.cosmology_raw["Critical density [internal units]"]
+    extra_header["snapshot cosmology H [internal units]"] = snap.metadata.cosmology_raw[
+        "H [internal units]"
+    ]
+    extra_header[
+        "snapshot cosmology H0 [internal units]"
+    ] = snap.metadata.cosmology_raw["H0 [internal units]"]
+    extra_header[
+        "snapshot cosmology Hubble time [internal units]"
+    ] = snap.metadata.cosmology_raw["Hubble time [internal units]"]
+    extra_header[
+        "snapshot cosmology Lookback time [internal units]"
+    ] = snap.metadata.cosmology_raw["Lookback time [internal units]"]
+    extra_header["snapshot cosmology N_eff"] = snap.metadata.cosmology_raw["N_eff"]
+    extra_header["snapshot cosmology N_nu"] = snap.metadata.cosmology_raw["N_nu"]
+    extra_header["snapshot cosmology N_ur"] = snap.metadata.cosmology_raw["N_ur"]
+    extra_header["snapshot cosmology Omega_b"] = snap.metadata.cosmology_raw["Omega_b"]
+    extra_header["snapshot cosmology Omega_cdm"] = snap.metadata.cosmology_raw[
+        "Omega_cdm"
+    ]
+    extra_header["snapshot cosmology Omega_g"] = snap.metadata.cosmology_raw["Omega_g"]
+    extra_header["snapshot cosmology Omega_k"] = snap.metadata.cosmology_raw["Omega_k"]
+    extra_header["snapshot cosmology Omega_lambda"] = snap.metadata.cosmology_raw[
+        "Omega_lambda"
+    ]
+    extra_header["snapshot cosmology Omega_m"] = snap.metadata.cosmology_raw["Omega_m"]
+    extra_header["snapshot cosmology Omega_nu"] = snap.metadata.cosmology_raw[
+        "Omega_nu"
+    ]
+    extra_header["snapshot cosmology Omega_nu_0"] = snap.metadata.cosmology_raw[
+        "Omega_nu_0"
+    ]
+    extra_header["snapshot cosmology Omega_r"] = snap.metadata.cosmology_raw["Omega_r"]
+    extra_header["snapshot cosmology Omega_ur"] = snap.metadata.cosmology_raw[
+        "Omega_ur"
+    ]
+    extra_header["snapshot cosmology T_CMB_0 [K]"] = snap.metadata.cosmology_raw[
+        "T_CMB_0 [K]"
+    ]
+    extra_header[
+        "snapshot cosmology T_CMB_0 [internal_units]"
+    ] = snap.metadata.cosmology_raw["T_CMB_0 [internal units]"]
+    extra_header["snapshot cosmology T_nu_0 [eV]"] = snap.metadata.cosmology_raw[
+        "T_nu_0 [eV]"
+    ]
+    extra_header[
+        "snapshot cosmology T_nu_0 [internal_units]"
+    ] = snap.metadata.cosmology_raw["T_nu_0 [internal units]"]
+    extra_header["snapshot cosmology h"] = snap.metadata.cosmology_raw["h"]
+    extra_header["snapshot cosmology w"] = snap.metadata.cosmology_raw["w"]
+    extra_header["snapshot cosmology w_0"] = snap.metadata.cosmology_raw["w_0"]
+    extra_header["snapshot cosmology w_a"] = snap.metadata.cosmology_raw["w_a"]
+
+else:
+    print("Found no cosmology metadata in snapshot. Continuing without.")
+
+
+extra_header["snapshot a"] = snap.metadata.a
+extra_header["snapshot date"] = snap.metadata.snapshot_date
+extra_header["snapshot dimension"] = snap.metadata.dimension
+extra_header["snapshot filename"] = snap.metadata.filename
+extra_header["snapshot gas gamma"] = snap.metadata.gas_gamma
+extra_header["snapshot gravity scheme"] = snap.metadata.gravity_scheme
+extra_header["snapshot header"] = snap.metadata.header
+extra_header["snapshot hydro info"] = snap.metadata.hydro_info
+extra_header["snapshot hydro scheme"] = snap.metadata.hydro_scheme
+extra_header["snapshot initial mass table"] = snap.metadata.initial_mass_table
+extra_header["snapshot library info"] = snap.metadata.library_info
+extra_header["snapshot mass table"] = snap.metadata.mass_table
+extra_header["snapshot redshift"] = snap.metadata.redshift
+extra_header["snapshot run name"] = snap.metadata.run_name
+extra_header["snapshot scale_factor"] = snap.metadata.scale_factor
+extra_header["snapshot subgrid scheme"] = snap.metadata.subgrid_scheme
+extra_header["snapshot stars properties"] = snap.metadata.stars_properties
+extra_header["snapshot stars scheme"] = snap.metadata.stars_scheme
+extra_header["snapshot system name"] = snap.metadata.system_name
+extra_header["snapshot time"] = snap.metadata.time
+
+
+# Initialize the Writer
+writer = Writer(
+    unit_system=my_units,
+    box_size=snap.metadata.boxsize,
+    dimension=snap.metadata.dimension,
+    compress=True,
+    extra_header=extra_header,
+    scale_factor=snap.metadata.scale_factor,
+)
+
+#  /PartType0/ - Gas
+#  /PartType1/ - Dark Matter
+#  /PartType2/ - Background Dark Matter
+#  /PartType3/ - Sinks
+#  /PartType4/ - Stars
+#  /PartType5/ - Black Holes
+#  /PartType6/ - Neutrino Dark Matter
+
+if snap.metadata.has_type[0]:
+    # Get and write gas
+    print("Adding gas data to ICs.")
+    writer.gas.coordinates = snap.gas.coordinates
+    writer.gas.velocities = snap.gas.velocities
+    writer.gas.masses = snap.gas.masses
+    writer.gas.internal_energy = snap.gas.internal_energies
+    writer.gas.smoothing_length = snap.gas.smoothing_lengths
+    writer.gas.particle_ids = snap.gas.particle_ids
+else:
+    print("Found no gas data in snapshot. Continuing without.")
+
+if snap.metadata.has_type[1]:
+    # Get and write dark matter
+    print("Adding dark matter data to ICs.")
+    writer.dark_matter.coordinates = snap.dark_matter.coordinates
+    writer.dark_matter.velocities = snap.dark_matter.velocities
+    writer.dark_matter.masses = snap.dark_matter.masses
+    writer.dark_matter.particle_ids = snap.dark_matter.particle_ids
+else:
+    print("Found no dark matter data in snapshot. Continuing without.")
+
+if snap.metadata.has_type[3]:
+    # Get and write sinks
+    print("Adding sinks data to ICs.")
+    writer.sinks.coordinates = snap.sinks.coordinates
+    writer.sinks.velocities = snap.sinks.velocities
+    writer.sinks.masses = snap.sinks.masses
+    writer.sinks.particle_ids = snap.sinks.particle_ids
+else:
+    print("Found no sinks data in snapshot. Continuing without.")
+
+if snap.metadata.has_type[4]:
+    # Get and write stars
+    print("Adding stars data to ICs.")
+    writer.stars.coordinates = snap.stars.coordinates
+    writer.stars.velocities = snap.stars.velocities
+    writer.stars.masses = snap.stars.masses
+    writer.stars.smoothing_length = snap.stars.smoothing_lengths
+    writer.stars.particle_ids = snap.stars.particle_ids
+else:
+    print("Found no star data in snapshot. Continuing without.")
+
+
+if snap.metadata.has_type[5]:
+    # Get and write black holes
+    print("Adding black holes data to ICs.")
+    writer.black_holes.coordinates = snap.black_holes.coordinates
+    writer.black_holes.velocities = snap.black_holes.velocities
+    writer.black_holes.masses = snap.black_holes.masses
+    writer.black_holes.particle_ids = snap.black_holes.particle_ids
+    writer.black_holes.smoothing_length = snap.black_holes.smoothing_lengths
+else:
+    print("Found no black hole data in snapshot. Continuing without.")
+
+
+if snap.metadata.has_type[6]:
+    # Get and write neutrinos
+    # TODO
+    raise NotImplementedError("Can't write neutrino ICs yet.")
+
+
+# Finally, write everything.
+writer.write(output_filename)
diff --git a/tools/data/cell_hierarchy.html b/tools/data/cell_hierarchy.html
index 6c9bcdb995be7970948cf9b9544ae0a59d41caf0..051451091d70d7e6bcb000b44b71f73c2779caa6 100644
--- a/tools/data/cell_hierarchy.html
+++ b/tools/data/cell_hierarchy.html
@@ -162,6 +162,7 @@
 		      "MPI rank:" + n.mpi_rank + "<br/>" + 
 		      "Part: " + n.hydro_count + "<br/>" + 
 		      "Spart: " + n.stars_count + "<br/>" +
+		      "Sink: " + n.sinks_count + "<br/>" +
 		      "Super: " + n.super + "<br/>" +
 		      "Super Hydro: " + n.hydro_super + "<br/>" +
 		      "Loc: " + n.loc1 + ", " + n.loc2 + ", " + n.loc3 + "<br/>" +
diff --git a/tools/plot_task_dependencies.py b/tools/plot_task_dependencies.py
index fade3d5de847cb33b4796b650b0e6f1aba4af79f..fa9fdb7fd1e5a04eb340f9fee0801918a6f3370c 100755
--- a/tools/plot_task_dependencies.py
+++ b/tools/plot_task_dependencies.py
@@ -171,7 +171,7 @@ def task_is_black_holes(name):
     name: str
         Task name
     """
-    if "bh" in name or "bpart" in name or "swallow" in name:
+    if "bh" in name or "bpart" in name or ("swallow" in name and not "sink" in name):
         return True
     return False
 
@@ -208,7 +208,12 @@ def task_is_hydro(name):
         return True
     if "_part" in name:
         return True
-    if "density" in name and "stars" not in name and "bh" not in name:
+    if (
+        "density" in name
+        and "stars" not in name
+        and "sink" not in name
+        and "bh" not in name
+    ):
         return True
     if "rho" in name and "bpart" not in name:
         return True
diff --git a/tools/task_plots/iplot_tasks.py b/tools/task_plots/iplot_tasks.py
index 50e65ebf9b7b285ce6f8c55f20d1006db1c30e67..f136ec78782a167e4aa3403375fa38308c27d116 100755
--- a/tools/task_plots/iplot_tasks.py
+++ b/tools/task_plots/iplot_tasks.py
@@ -49,7 +49,7 @@ import sys
 import argparse
 
 # import hardcoded data
-from swift_hardcoded_data import TASKTYPES, SUBTYPES
+from swift_hardcoded_data import TASKTYPES, SUBTYPES, TASKCOLOURS, SUBCOLOURS
 
 #  Handle the command line.
 parser = argparse.ArgumentParser(description="Plot task graphs")
@@ -127,145 +127,6 @@ PLOT_PARAMS = {
 }
 pl.rcParams.update(PLOT_PARAMS)
 
-#  Task/subtypes of interest.
-FULLTYPES = [
-    "self/limiter",
-    "self/force",
-    "self/gradient",
-    "self/density",
-    "self/grav",
-    "sub_self/limiter",
-    "sub_self/force",
-    "sub_self/gradient",
-    "sub_self/density",
-    "pair/limiter",
-    "pair/force",
-    "pair/gradient",
-    "pair/density",
-    "pair/grav",
-    "sub_pair/limiter",
-    "sub_pair/force",
-    "sub_pair/gradient",
-    "sub_pair/density",
-    "recv/xv",
-    "send/xv",
-    "recv/rho",
-    "send/rho",
-    "recv/tend_part",
-    "send/tend_part",
-    "recv/tend_gpart",
-    "send/tend_gpart",
-    "recv/tend_spart",
-    "send/tend_spart",
-    "recv/tend_bpart",
-    "send/tend_bpart",
-    "recv/gpart",
-    "send/gpart",
-    "recv/spart",
-    "send/spart",
-    "send/sf_counts",
-    "recv/sf_counts",
-    "recv/bpart",
-    "send/bpart",
-    "recv/limiter",
-    "send/limiter",
-    "pack/limiter",
-    "unpack/limiter",
-    "self/stars_density",
-    "pair/stars_density",
-    "sub_self/stars_density",
-    "sub_pair/stars_density",
-    "self/stars_prep1",
-    "pair/stars_prep1",
-    "sub_self/stars_prep1",
-    "sub_pair/stars_prep1",
-    "self/stars_prep2",
-    "pair/stars_prep2",
-    "sub_self/stars_prep2",
-    "sub_pair/stars_prep2",
-    "self/stars_feedback",
-    "pair/stars_feedback",
-    "sub_self/stars_feedback",
-    "sub_pair/stars_feedback",
-    "self/bh_density",
-    "pair/bh_density",
-    "sub_self/bh_density",
-    "sub_pair/bh_density",
-    "self/bh_swallow",
-    "pair/bh_swallow",
-    "sub_self/bh_swallow",
-    "sub_pair/bh_swallow",
-    "self/do_swallow",
-    "pair/do_swallow",
-    "sub_self/do_swallow",
-    "sub_pair/do_swallow",
-    "self/bh_feedback",
-    "pair/bh_feedback",
-    "sub_self/bh_feedback",
-    "sub_pair/bh_feedback",
-]
-
-#  A number of colours for the various types. Recycled when there are
-#  more task types than colours...
-colours = [
-    "cyan",
-    "lightgray",
-    "darkblue",
-    "yellow",
-    "tan",
-    "dodgerblue",
-    "sienna",
-    "aquamarine",
-    "bisque",
-    "blue",
-    "green",
-    "lightgreen",
-    "brown",
-    "purple",
-    "moccasin",
-    "olivedrab",
-    "chartreuse",
-    "olive",
-    "darkgreen",
-    "green",
-    "mediumseagreen",
-    "mediumaquamarine",
-    "darkslategrey",
-    "mediumturquoise",
-    "black",
-    "cadetblue",
-    "skyblue",
-    "red",
-    "slategray",
-    "gold",
-    "slateblue",
-    "blueviolet",
-    "mediumorchid",
-    "firebrick",
-    "magenta",
-    "hotpink",
-    "pink",
-    "orange",
-    "lightgreen",
-]
-maxcolours = len(colours)
-
-#  Set colours of task/subtype.
-TASKCOLOURS = {}
-ncolours = 0
-for task in TASKTYPES:
-    TASKCOLOURS[task] = colours[ncolours]
-    ncolours = (ncolours + 1) % maxcolours
-
-SUBCOLOURS = {}
-for task in FULLTYPES:
-    SUBCOLOURS[task] = colours[ncolours]
-    ncolours = (ncolours + 1) % maxcolours
-
-for task in SUBTYPES:
-    SUBCOLOURS[task] = colours[ncolours]
-    ncolours = (ncolours + 1) % maxcolours
-
 #  For fiddling with colours...
 if args.verbose:
     print("#Selected colours:")
diff --git a/tools/task_plots/plot_tasks.py b/tools/task_plots/plot_tasks.py
index 2f6f6733bc88ff62c8120a5887f8a45f354eb6be..7cdb93846db3c88fe794901597d2058311c7e55d 100755
--- a/tools/task_plots/plot_tasks.py
+++ b/tools/task_plots/plot_tasks.py
@@ -50,7 +50,7 @@ import sys
 import argparse
 
 # import hardcoded data
-from swift_hardcoded_data import TASKTYPES, SUBTYPES
+from swift_hardcoded_data import TASKTYPES, SUBTYPES, TASKCOLOURS, SUBCOLOURS
 
 #  Handle the command line.
 parser = argparse.ArgumentParser(description="Plot task graphs")
@@ -150,144 +150,6 @@ PLOT_PARAMS = {
 }
 pl.rcParams.update(PLOT_PARAMS)
 
-#  Task/subtypes of interest.
-FULLTYPES = [
-    "self/limiter",
-    "self/force",
-    "self/gradient",
-    "self/density",
-    "self/grav",
-    "sub_self/limiter",
-    "sub_self/force",
-    "sub_self/gradient",
-    "sub_self/density",
-    "pair/limiter",
-    "pair/force",
-    "pair/gradient",
-    "pair/density",
-    "pair/grav",
-    "sub_pair/limiter",
-    "sub_pair/force",
-    "sub_pair/gradient",
-    "sub_pair/density",
-    "recv/xv",
-    "send/xv",
-    "recv/rho",
-    "send/rho",
-    "recv/tend_part",
-    "send/tend_part",
-    "recv/tend_gpart",
-    "send/tend_gpart",
-    "recv/tend_spart",
-    "send/tend_spart",
-    "recv/tend_bpart",
-    "send/tend_bpart",
-    "recv/gpart",
-    "send/gpart",
-    "recv/spart",
-    "send/spart",
-    "send/sf_counts",
-    "recv/sf_counts",
-    "recv/bpart",
-    "send/bpart",
-    "recv/limiter",
-    "send/limiter",
-    "pack/limiter",
-    "unpack/limiter",
-    "self/stars_density",
-    "pair/stars_density",
-    "sub_self/stars_density",
-    "sub_pair/stars_density",
-    "self/stars_prep1",
-    "pair/stars_prep1",
-    "sub_self/stars_prep1",
-    "sub_pair/stars_prep1",
-    "self/stars_prep2",
-    "pair/stars_prep2",
-    "sub_self/stars_prep2",
-    "sub_pair/stars_prep2",
-    "self/stars_feedback",
-    "pair/stars_feedback",
-    "sub_self/stars_feedback",
-    "sub_pair/stars_feedback",
-    "self/bh_density",
-    "pair/bh_density",
-    "sub_self/bh_density",
-    "sub_pair/bh_density",
-    "self/bh_swallow",
-    "pair/bh_swallow",
-    "sub_self/bh_swallow",
-    "sub_pair/bh_swallow",
-    "self/do_swallow",
-    "pair/do_swallow",
-    "sub_self/do_swallow",
-    "sub_pair/do_swallow",
-    "self/bh_feedback",
-    "pair/bh_feedback",
-    "sub_self/bh_feedback",
-    "sub_pair/bh_feedback",
-]
-
-#  A number of colours for the various types. Recycled when there are
-#  more task types than colours...
-colours = [
-    "cyan",
-    "lightgray",
-    "darkblue",
-    "yellow",
-    "tan",
-    "dodgerblue",
-    "sienna",
-    "aquamarine",
-    "bisque",
-    "blue",
-    "green",
-    "lightgreen",
-    "brown",
-    "purple",
-    "moccasin",
-    "olivedrab",
-    "chartreuse",
-    "olive",
-    "darkgreen",
-    "green",
-    "mediumseagreen",
-    "mediumaquamarine",
-    "darkslategrey",
-    "mediumturquoise",
-    "black",
-    "cadetblue",
-    "skyblue",
-    "red",
-    "slategray",
-    "gold",
-    "slateblue",
-    "blueviolet",
-    "mediumorchid",
-    "firebrick",
-    "magenta",
-    "hotpink",
-    "pink",
-    "orange",
-    "lightgreen",
-]
-maxcolours = len(colours)
-
-#  Set colours of task/subtype.
-TASKCOLOURS = {}
-ncolours = 0
-for task in TASKTYPES:
-    TASKCOLOURS[task] = colours[ncolours]
-    ncolours = (ncolours + 1) % maxcolours
-
-SUBCOLOURS = {}
-for task in FULLTYPES:
-    SUBCOLOURS[task] = colours[ncolours]
-    ncolours = (ncolours + 1) % maxcolours
-
-for task in SUBTYPES:
-    SUBCOLOURS[task] = colours[ncolours]
-    ncolours = (ncolours + 1) % maxcolours
 
 #  For fiddling with colours...
 if args.verbose:
@@ -518,4 +380,5 @@ for rank in ranks:
     pl.close()
     print("Graphics done, output written to", outpng)
 
+
 sys.exit(0)
diff --git a/tools/task_plots/swift_hardcoded_data.py b/tools/task_plots/swift_hardcoded_data.py
index 76277d49b2caffc76ebf26a5aa1f8dd2cbb382e1..7d18c657dd859556a6262f4f0b34796727c8bc9d 100644
--- a/tools/task_plots/swift_hardcoded_data.py
+++ b/tools/task_plots/swift_hardcoded_data.py
@@ -1,12 +1,18 @@
 #!/usr/bin/env python3
 
-# ------------------------------------------------------------
-# This file contains data that is hardcoded into swift
-# that needs to be reproduced exactly in order for analysis
-# outputs to make any sense.
-# The data is used in other scripts in this directory, this
-# script is intended for imports only.
-# ------------------------------------------------------------
+# -----------------------------------------------------------------------------
+# This file contains data that is hardcoded into swift that needs to be
+# reproduced exactly in order for analysis outputs to make any sense. The data
+# is used in other scripts in this directory, this script is intended for
+# imports only.
+# Additionally, we add some setup work:
+# - check if a file "task_labels_task_types.txt" exists. If it does, verify that
+#   the hardcoded data in this file corresponds to the output written by SWIFT
+#   in that file.
+# - set up a list of useful task type/subtype combinations. Note that this list
+#   needs to be verified manually for completeness.
+# - assing colours to all task types, subtypes, and combinations thereof.
+# -----------------------------------------------------------------------------
 
 #  Tasks and subtypes. Indexed as in tasks.h.
 TASKTYPES = [
@@ -70,8 +76,11 @@ TASKTYPES = [
     "bh_swallow_ghost3",
     "fof_self",
     "fof_pair",
+    "fof_attach_self",
+    "fof_attach_pair",
     "neutrino_weight",
     "sink_in",
+    "sink_density_ghost",
     "sink_ghost1",
     "sink_ghost2",
     "sink_out",
@@ -82,6 +91,9 @@ TASKTYPES = [
     "rt_ghost2",
     "rt_transport_out",
     "rt_tchem",
+    "rt_advance_cell_time",
+    "rt_sort",
+    "rt_collect_times",
     #  "count",
 ]
 
@@ -122,6 +134,170 @@ SUBTYPES = [
     #  "count",
 ]
 
+#  Task/subtypes of interest.
+FULLTYPES = [
+    "self/limiter",
+    "self/force",
+    "self/gradient",
+    "self/density",
+    "self/grav",
+    "sub_self/limiter",
+    "sub_self/force",
+    "sub_self/gradient",
+    "sub_self/density",
+    "pair/limiter",
+    "pair/force",
+    "pair/gradient",
+    "pair/density",
+    "pair/grav",
+    "sub_pair/limiter",
+    "sub_pair/force",
+    "sub_pair/gradient",
+    "sub_pair/density",
+    "recv/xv",
+    "send/xv",
+    "recv/rho",
+    "send/rho",
+    "recv/tend_part",
+    "send/tend_part",
+    "recv/tend_gpart",
+    "send/tend_gpart",
+    "recv/tend_spart",
+    "send/tend_spart",
+    "recv/tend_bpart",
+    "send/tend_bpart",
+    "recv/gpart",
+    "send/gpart",
+    "recv/spart",
+    "send/spart",
+    "send/sf_counts",
+    "recv/sf_counts",
+    "recv/bpart",
+    "send/bpart",
+    "recv/limiter",
+    "send/limiter",
+    "pack/limiter",
+    "unpack/limiter",
+    "self/stars_density",
+    "pair/stars_density",
+    "sub_self/stars_density",
+    "sub_pair/stars_density",
+    "self/stars_prep1",
+    "pair/stars_prep1",
+    "sub_self/stars_prep1",
+    "sub_pair/stars_prep1",
+    "self/stars_prep2",
+    "pair/stars_prep2",
+    "sub_self/stars_prep2",
+    "sub_pair/stars_prep2",
+    "self/stars_feedback",
+    "pair/stars_feedback",
+    "sub_self/stars_feedback",
+    "sub_pair/stars_feedback",
+    "self/bh_density",
+    "pair/bh_density",
+    "sub_self/bh_density",
+    "sub_pair/bh_density",
+    "self/bh_swallow",
+    "pair/bh_swallow",
+    "sub_self/bh_swallow",
+    "sub_pair/bh_swallow",
+    "self/do_swallow",
+    "pair/do_swallow",
+    "sub_self/do_swallow",
+    "sub_pair/do_swallow",
+    "self/bh_feedback",
+    "pair/bh_feedback",
+    "sub_self/bh_feedback",
+    "sub_pair/bh_feedback",
+    "self/rt_gradient",
+    "pair/rt_gradient",
+    "sub_self/rt_gradient",
+    "sub_pair/rt_gradient",
+    "self/rt_transport",
+    "pair/rt_transport",
+    "sub_self/rt_transport",
+    "sub_pair/rt_transport",
+    "self/sink_density",
+    "pair/sink_density",
+    "sub_self/sink_density",
+    "sub_pair/sink_density",
+    "self/sink_swallow",
+    "pair/sink_swallow",
+    "sub_self/sink_swallow",
+    "sub_pair/sink_swallow",
+    "self/sink_do_swallow",
+    "pair/sink_do_swallow",
+    "sub_self/sink_do_swallow",
+    "sub_pair/sink_do_swallow",
+    "self/sink_do_gas_swallow",
+    "pair/sink_do_gas_swallow",
+    "sub_self/sink_do_gas_swallow",
+    "sub_pair/sink_do_gas_swallow",
+]
+
+#  A number of colours for the various types. Recycled when there are
+#  more task types than colours...
+colours = [
+    "cyan",
+    "lightgray",
+    "darkblue",
+    "yellow",
+    "tan",
+    "dodgerblue",
+    "sienna",
+    "aquamarine",
+    "bisque",
+    "blue",
+    "green",
+    "lightgreen",
+    "brown",
+    "purple",
+    "moccasin",
+    "olivedrab",
+    "chartreuse",
+    "olive",
+    "darkgreen",
+    "green",
+    "mediumseagreen",
+    "mediumaquamarine",
+    "darkslategrey",
+    "mediumturquoise",
+    "black",
+    "cadetblue",
+    "skyblue",
+    "red",
+    "slategray",
+    "gold",
+    "slateblue",
+    "blueviolet",
+    "mediumorchid",
+    "firebrick",
+    "magenta",
+    "hotpink",
+    "pink",
+    "orange",
+    "lightgreen",
+]
+maxcolours = len(colours)
+
+#  Set colours of task/subtype.
+TASKCOLOURS = {}
+ncolours = 0
+for task in TASKTYPES:
+    TASKCOLOURS[task] = colours[ncolours]
+    ncolours = (ncolours + 1) % maxcolours
+
+SUBCOLOURS = {}
+for task in FULLTYPES:
+    SUBCOLOURS[task] = colours[ncolours]
+    ncolours = (ncolours + 1) % maxcolours
+
+for task in SUBTYPES:
+    SUBCOLOURS[task] = colours[ncolours]
+    ncolours = (ncolours + 1) % maxcolours
+
+
 # check if label files are found that have (possibly different) labels
 # output by SWIFT itself
 import os
diff --git a/tools/timed_functions.py b/tools/timed_functions.py
index d4ad20fae9d95219df6bf573b7154f6b49a985e2..f729039edd9fa3eec91ec9831535a5e69cf6073f 100644
--- a/tools/timed_functions.py
+++ b/tools/timed_functions.py
@@ -35,15 +35,16 @@ labels = [
     ["engine_unskip:", 0],
     ["engine_unskip_timestep_communications:", 0],
     ["engine_collect_end_of_step:", 0],
-    ["engine_launch: \(tasks\)", 0],
-    ["engine_launch: \(timesteps\)", 0],
-    ["engine_launch: \(cycles\)", 0],
+    ["engine_launch: \\(tasks\\)", 0],
+    ["engine_launch: \\(timesteps\\)", 0],
+    ["engine_launch: \\(cycles\\)", 0],
+    ["engine_launch: \\(tend\\)", 0],
+    ["Gathering and activating tend", 0],
     ["writing particle properties", 0],
     ["engine_repartition:", 0],
     ["engine_exchange_cells:", 1],
     ["Dumping restart files", 0],
     ["engine_print_stats:", 0],
-    ["engine_marktasks:", 1],
     ["Reading initial conditions", 0],
     ["engine_print_task_counts:", 0],
     ["engine_drift_top_multipoles:", 0],
@@ -63,12 +64,16 @@ labels = [
     ["VR Collecting particle info", 3],
     ["VR Invocation of velociraptor", 3],
     ["VR Copying group information back", 3],
+    ["calc_all_power_spectra:", 0],
     ["fof_allocate:", 2],
     ["engine_make_fof_tasks:", 2],
     ["engine_activate_fof_tasks:", 2],
-    ["fof_search_tree:", 2],
-    ["engine_launch: \(fof\)", 2],
-    ["engine_launch: \(fof comms\)", 2],
+    ["fof_search_foreign_cells\\(\\)", 2],
+    ["link_foreign_fragmens\\(\\)", 2],
+    ["engine_launch: \\(fof\\)", 2],
+    ["engine_launch: \\(fof comms\\)", 2],
+    ["fof_compute_group_props:", 2],
+    ["fof_compute_local_sizes:", 2],
     ["do_line_of_sight:", 0],
     ["csds_log_all_particles:", 0],
     ["csds_ensure_size:", 0],