diff --git a/README.md b/README.md
index 2d941320cb0026ed01ba39550e99141f6047d899..d7be2b1d53bc1dc333a66b6b231aa3f1c7c1f443 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/configure.ac b/configure.ac
index ac8b90002f67c2711db670095da4b6686e040335..b9b3de68843263351de94a86b4df54aa17b4f345 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1448,7 +1448,7 @@ if test "x$with_lustreapi" != "xno"; then
       LUSTREAPI_LIBS="-llustreapi"
       LUSTREAPI_INCS=""
    fi
-   AC_CHECK_LIB([lustreapi], [llapi_obd_statfs], [have_lustreapi="yes"], 
+   AC_CHECK_LIB([lustreapi], [llapi_obd_statfs], [have_lustreapi="yes"],
                 [have_lustreapi="no"],[$LUSTREAPI_LIBS])
 
    if test "$have_lustreapi" = "yes"; then
@@ -1870,14 +1870,12 @@ if test "$have_armv7apmccntr"x = "yes"x; then
 fi
 
 # Check if we have native exp10 and exp10f functions. If not fallback to our
-# implementations. On Apple/CLANG we have __exp10, so also check for that
+# 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 fallback to our
 # implementations. On Apple/CLANG we have __sincos, so also check for that
@@ -1888,7 +1886,15 @@ if test "$ax_cv_c_compiler_vendor" = "clang"; then
       AC_CHECK_LIB([m],[__sincos], [AC_DEFINE([HAVE___SINCOS],1,[The __sincos function is present.])])
       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
@@ -2216,7 +2222,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, shadowswift, 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"]
@@ -2263,6 +2269,9 @@ case "$with_hydro" in
    planetary)
       AC_DEFINE([PLANETARY_SPH], [1], [Planetary SPH])
    ;;
+   remix)
+      AC_DEFINE([REMIX_SPH], [1], [REMIX SPH])
+   ;;
    sphenix)
       AC_DEFINE([SPHENIX_SPH], [1], [SPHENIX SPH])
    ;;
diff --git a/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst b/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst
index ecf82634aed909a77c81cf27ea438aaec9b98338..b802ac5f412e17006719101bc45e0472fd67a8d0 100644
--- a/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst
+++ b/doc/RTD/source/FriendsOfFriends/stand_alone_fof.rst
@@ -35,6 +35,9 @@ 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
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/Planetary/equations_of_state.rst b/doc/RTD/source/Planetary/equations_of_state.rst
index d20cbd83ff0efdbc1f73c3a59a310d71155abda1..37b175c53737288374fe1a4afec07a280c3bbd53 100644
--- a/doc/RTD/source/Planetary/equations_of_state.rst
+++ b/doc/RTD/source/Planetary/equations_of_state.rst
@@ -36,6 +36,8 @@ planetary initial conditions, `WoMa  <https://github.com/srbonilla/WoMa>`_:
     + 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``
@@ -45,6 +47,10 @@ planetary initial conditions, `WoMa  <https://github.com/srbonilla/WoMa>`_:
     + 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``
@@ -56,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``.
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 30b79689a28527f975a739925ae7835f90e398c4..26c8b2474e893221a882fcca41306f57cbf9adf7 100644
--- a/doc/RTD/source/SubgridModels/GEAR/index.rst
+++ b/doc/RTD/source/SubgridModels/GEAR/index.rst
@@ -6,341 +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``). 
-
-.. 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``.
-
-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``).
-
-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.
-
-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 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)
-    cmb_temperature_floor : 1                    # Enable/disable an effective CMB temperature floor
-  
-.. 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``.
-
-
-.. _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 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_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.
-
-Sink particles
-~~~~~~~~~~~~~~
-
-GEAR now implements sink particles for star formation. Instead of stochastically transforming gas particles into stars as is done in the star formation scheme above when some criteria are met, 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. 
-
-This was a brief overview of the model. More details can be found in :ref:`sink_GEAR_model` pages. Please refer to these for configuration, compilations and details of the model. 
-
-
-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. If ``GEARChemistry:initial_metallicity`` is negative, then the metallicities are read from the initial conditions.
-
-.. 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?
-
-
-.. _gear_feedback:
-   
-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
-
-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/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
index 699dfa53dcdb7991c914bcc4c5c91ff2382758ea..aa4313dda9ee78a0c9b22d57c98aeac7dca2aa24 100644
--- a/doc/RTD/source/SubgridModels/GEAR/sinks/index.rst
+++ b/doc/RTD/source/SubgridModels/GEAR/sinks/index.rst
@@ -15,4 +15,3 @@ Sink particles and star formation in GEAR model
    theory
    sink_timesteps
    params
-   output
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/output.rst b/doc/RTD/source/SubgridModels/GEAR/sinks/output.rst
deleted file mode 100644
index fda4c6334cbef571c55cbb4d562830a9eef7b9c6..0000000000000000000000000000000000000000
--- a/doc/RTD/source/SubgridModels/GEAR/sinks/output.rst
+++ /dev/null
@@ -1,73 +0,0 @@
-.. 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 and particle IDs.
-
-Sink particles
-~~~~~~~~~~~~~~
-
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| Name                                  | Description                         | Units     | Comments                                          |
-+=======================================+=====================================+===========+===================================================+
-| ``NumberOfSinkSwallows``              | | Number of sink merger events      | [-]       |                                                   |
-|                                       | |                                   |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``NumberOfGasSwallows``               | | Number of gas swallowed           | [-]       |                                                   |
-|                                       | |                                   |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``TargetMass``                        | | Sink target mass to spawn the     | [U_M]     | | You can use it to determine if the target mass  |
-|                                       | | next 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 the    | [-]       |                                                   |
-|                                       | | the sink particles                |           |                                                   |
-|                                       | |                                   |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``SwallowedAngularMomentum``          | | Total angular momentum swallowed  | [U_M U_L  |                                                   |
-|                                       | | by the sink particles             |  ^2 U_T   |                                                   |
-|                                       | |                                   |  ^-1]     |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``MetalMassFractions``                | | Mass fraction of each metal       | [-]       | | Array of length ``N`` for each particles. The   |
-|                                       | | element                           |           | | number of elements ``N`` is determined at       |
-|                                       | |                                   |           | | compile time by ``--with-chemistry=GEAR_N``.    |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``BirthScaleFactors``                 | | Scale-factors when the sinks were | [-]       | Only used in cosmological runs.                   |
-|                                       | | born                              |           |                                                   |
-|                                       | |                                   |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``BirthTimes``                        | | Time when the sinks were          | [U_T]     | Only used in non-cosmological runs.               |
-|                                       | | born                              |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-
-
-
-Stars
-~~~~~
-
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| Name                                  | Description                         | Units     | Comments                                          |
-+=======================================+=====================================+===========+===================================================+
-| ``BirthScaleFactors``                 | | Scale-factors when the stars were | [-]       | Only used in cosmological runs.                   |
-|                                       | | born                              |           |                                                   |
-|                                       | |                                   |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``BirthTimes``                        | | Time when the stars were          | [U_T]     | Only used in non-cosmological runs.               |
-|                                       | | born                              |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``BirthMasses``                       | | Masses of the stars at brith time | [U_M]     |                                                   |
-|                                       | |                                   |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``ProgenitorIDs``                     | | ID of the progenitor sinks        | [-]       |                                                   |
-|                                       | |                                   |           |                                                   |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
-| ``StellarParticleType``               | | Type of the stellar particle:     | [-]       | | The last type correspond to star particles in   |
-|                                       | | 0: (discrete) single star         |           | | the previous model, i.e. representing the full  |
-|                                       | | 1: continuous IMF part star       |           | | IMF                                             |
-|                                       | | 2: single population star         |           | |                                                 |
-+---------------------------------------+-------------------------------------+-----------+---------------------------------------------------+
diff --git a/doc/RTD/source/SubgridModels/GEAR/sinks/theory.rst b/doc/RTD/source/SubgridModels/GEAR/sinks/theory.rst
index cbb8337a5bf3afc665f10feb76f2f9d5a26e9dd3..5991e330666414e33c2dfb4aff698cfbc95c9c76 100644
--- a/doc/RTD/source/SubgridModels/GEAR/sinks/theory.rst
+++ b/doc/RTD/source/SubgridModels/GEAR/sinks/theory.rst
@@ -239,7 +239,7 @@ 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_feedback` section. Here, we only provide the changes from the previous model. 
+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.
 
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/SubgridModels/index.rst b/doc/RTD/source/SubgridModels/index.rst
index 69daf0eb2c0435b488b3ce9cb6b7c971d615787f..017614e5fcc3adc84fcabd48bc4ce0945aa35878 100644
--- a/doc/RTD/source/SubgridModels/index.rst
+++ b/doc/RTD/source/SubgridModels/index.rst
@@ -16,7 +16,6 @@ be use as an empty canvas to be copied to create additional models.
    EAGLE/index
    QuickLymanAlpha/index
    GEAR/index
-   GEAR/sinks/index
    Basic/index	     
    AGNSpinJets/index
    AGORA/index
diff --git a/examples/Planetary/DemoImpactInitCond/plot_profiles.py b/examples/Planetary/DemoImpactInitCond/plot_profiles.py
index a03fa9a7c8f065b49d297241115408903b428d25..e60b2e3201495017a86642bd498bef98c1efc6e8 100755
--- a/examples/Planetary/DemoImpactInitCond/plot_profiles.py
+++ b/examples/Planetary/DemoImpactInitCond/plot_profiles.py
@@ -22,7 +22,7 @@ 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. (2024), Ruiz-Bonilla el al. (2022), and Kegerreis et al. (2019).
+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.
 """
 
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 6eb154aaa49a9355b3240656417c1b170743b1a5..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,6 +101,7 @@ 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("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")
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/gresho.yml b/examples/Planetary/GreshoVortex_3D/gresho.yml
deleted file mode 100644
index dac2f29b5b1e3bb5040de3215c30574930e7012b..0000000000000000000000000000000000000000
--- a/examples/Planetary/GreshoVortex_3D/gresho.yml
+++ /dev/null
@@ -1,43 +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
-
-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).
-
-# Parameters governing the snapshots
-Snapshots:
-  basename:            gresho # 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
-  
-# 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:  ./greshoVortex.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/GreshoVortex_3D/makeIC.py b/examples/Planetary/GreshoVortex_3D/makeIC.py
deleted file mode 100644
index f7178e6be48d8b11eec97dcbd24a3bbc52170715..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("SmoothingLength", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergy", (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 2037b8072515591309e2190a476c2f9882d6b2e8..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("SmoothingLength", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergy", (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 8b34ea96a393ed48bbf530b5e99133a85774bca1..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,6 +115,7 @@ 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("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")
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 ee89fe46b16ae8f9577dd251b1ef1078d4b27f83..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("SmoothingLength", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergy", (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 ea6f79637dd1979e52ebba031afb0aa35f4ea377..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("SmoothingLength", (numPart, 1), "f")
-ds[()] = h.reshape((numPart, 1))
-ds = grp.create_dataset("InternalEnergy", (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_2D/square.yml b/examples/Planetary/SquareTest_2D/square.yml
deleted file mode 100644
index 284ebad4156d3d56dd7ea653107acadcaddd7c6a..0000000000000000000000000000000000000000
--- a/examples/Planetary/SquareTest_2D/square.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.    # 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 # 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)
-
-# 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:  ./square.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/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/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/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/HomogeneousBoxSinkParticles/makeIC.py b/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py
index 80ee002e2de865068e0a1190a9814d7c12f58d48..4f5d518f13152faecf55d258c7726708811c549e 100755
--- a/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py
@@ -18,11 +18,11 @@
 #
 ################################################################################
 
+import argparse
+
 import h5py
 import numpy as np
-import argparse
-from astropy import units
-from astropy import constants
+from astropy import constants, units
 
 
 class store_as_array(argparse._StoreAction):
@@ -34,7 +34,6 @@ class store_as_array(argparse._StoreAction):
 
 
 def parse_options():
-
     usage = "usage: %prog [options] file"
     parser = argparse.ArgumentParser(description=usage)
 
@@ -143,11 +142,11 @@ UnitVelocity = UnitVelocity_in_cgs * units.cm / units.s
 np.random.seed(1)
 
 # Number of particles
-N = (2 ** opt.level) ** 3  # 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
+rho = rho * constants.m_p / units.cm**3
 
 # Gas particle mass
 m = opt.mass  # in solar mass
@@ -179,14 +178,14 @@ print("Equivalent velocity dispertion        : {}".format(sigma.to(units.m / uni
 # Convert to code units
 m = m.to(UnitMass).value
 L = L.to(UnitLength).value
-rho = rho.to(UnitMass / UnitLength ** 3).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
+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
@@ -214,7 +213,7 @@ 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)
+h_sink = np.ones(N_sink) * 3 * L / (N + N_sink) ** (1 / 3.0)
 ids_sink = np.arange(N, N + N_sink)
 
 #####################
diff --git a/examples/parameter_example.yml b/examples/parameter_example.yml
index b5a370b6695128b8d80802ac3176209ade46e18c..2c0148c5afeab99abbbbbe28f9d5317c336e0f59 100644
--- a/examples/parameter_example.yml
+++ b/examples/parameter_example.yml
@@ -315,6 +315,17 @@ EoS:
   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
@@ -322,6 +333,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
@@ -336,6 +351,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
diff --git a/src/Makefile.am b/src/Makefile.am
index 1a5e034885bceabe6735b3845d2d4c7325afae9d..c0430117b50f769e7a25829d16f5509fc94e6318 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -223,7 +223,7 @@ nobase_noinst_HEADERS = align.h approx_math.h atomic.h barrier.h cycle.h error.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_functions_sinks.h runner_doiact_limiter.h units.h intrinsics.h minmax.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
diff --git a/src/black_holes/SPIN_JET/black_holes.h b/src/black_holes/SPIN_JET/black_holes.h
index b4894f42bebc119399a07d962569a135558b8b40..526dff63197fda91b067dc9880b34d38baa1c260 100644
--- a/src/black_holes/SPIN_JET/black_holes.h
+++ b/src/black_holes/SPIN_JET/black_holes.h
@@ -1494,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;
diff --git a/src/black_holes/SPIN_JET/black_holes_io.h b/src/black_holes/SPIN_JET/black_holes_io.h
index db6c491a07bb6bb701f20b1804c67b7f9a880784..6fc5f789649a13681e822b7ca76e158f331e899f 100644
--- a/src/black_holes/SPIN_JET/black_holes_io.h
+++ b/src/black_holes/SPIN_JET/black_holes_io.h
@@ -189,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 = 62;
+  *num_fields = 63;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_bpart(
@@ -237,11 +237,10 @@ 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,
-      "Physcial energy contained in the thermal feedback reservoir of the "
-      "particles");
+      energy_reservoir, /*can convert to comoving=*/0,
+      "Physcial energy contained in the feedback reservoir of the particles");
 
   list[10] = io_make_output_field(
       "AccretionRates", FLOAT, 1, UNIT_CONV_MASS_PER_UNIT_TIME, 0.f, bparts,
@@ -289,7 +288,7 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
         "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.");
   }
@@ -301,14 +300,14 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
         "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.");
 
@@ -385,9 +384,9 @@ 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.");
 
@@ -429,12 +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,
-      "Total (cumulative) physical thermal energies injected into gas "
-      "particles in thermal AGN feedback, including the effects of both "
-      "radiation and winds.");
+      AGN_cumulative_energy, /*can convert to comoving=*/0,
+      "Total (cumulative) physical energies injected into gas particles "
+      "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,
@@ -442,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,
@@ -456,9 +454,7 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
 
   list[40] = io_make_output_field(
       "RadiativeEfficiencies", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
-      radiative_efficiency,
-      "The radiative efficiencies of the BHs, i.e. the "
-      "AGN luminosity divided by accretion rate.");
+      radiative_efficiency, "AGN luminosity divided by accretion rate.");
 
   list[41] = io_make_output_field("CosAccretionDiskAngle", FLOAT, 1,
                                   UNIT_CONV_NO_UNITS, 0.f, bparts,
@@ -475,9 +471,10 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "Total jet energy waiting to be released (once it "
       "grows large enough to kick a single particle).");
 
-  list[44] = 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[45] = io_make_output_field(
       "JetTimeSteps", FLOAT, 1, UNIT_CONV_TIME, 0.f, bparts, dt_jet,
@@ -524,17 +521,19 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "The total accreted mass in each accretion mode. The components to the "
       "mass accreted in the thick, thin and slim disc modes, respectively.");
 
-  list[52] = io_make_output_field(
+  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[53] = io_make_output_field(
+  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 jet energy "
       "dumped in the thick, thin and slim disc modes, respectively.");
@@ -543,27 +542,29 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       "WindEfficiencies", FLOAT, 1, UNIT_CONV_NO_UNITS, 0.f, bparts,
       wind_efficiency, "The wind efficiencies of the black holes.");
 
-  list[55] = io_make_output_field(
+  list[55] = io_make_physical_output_field(
       "TotalRadiatedEnergies", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts,
-      radiated_energy,
+      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_output_field(
+  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_output_field(
+  list[57] = io_make_physical_output_field(
       "TotalWindEnergies", FLOAT, 1, UNIT_CONV_ENERGY, 0.f, bparts, wind_energy,
-      "The total energy launched into accretion disc winds by the black "
-      "holes, in all accretion modes. ");
+      /*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_output_field(
+  list[58] = io_make_physical_output_field(
       "WindEnergiesByMode", FLOAT, BH_accretion_modes_count, UNIT_CONV_ENERGY,
-      0.f, bparts, wind_energy_by_mode,
+      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, "
@@ -581,7 +582,14 @@ INLINE static void black_holes_write_particles(const struct bpart* bparts,
       convert_bpart_gas_temperatures,
       "Temperature of the gas surrounding the black holes.");
 
-  list[61] = io_make_output_field(
+  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 "
diff --git a/src/black_holes/SPIN_JET/black_holes_part.h b/src/black_holes/SPIN_JET/black_holes_part.h
index 7e66da192bb937fea1d23c104c63e99d0d47f84c..99d147e4ea5844ac2a42d8d009292a88f4d19a93 100644
--- a/src/black_holes/SPIN_JET/black_holes_part.h
+++ b/src/black_holes/SPIN_JET/black_holes_part.h
@@ -167,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;
 
diff --git a/src/cell_drift.c b/src/cell_drift.c
index 2b6e1643ed04edc3bfd8a76b9052dd6c949288bb..474248907e3e742abed1fe5b25b2201c9b7a4078 100644
--- a/src/cell_drift.c
+++ b/src/cell_drift.c
@@ -674,6 +674,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 int convert_at_h_max = e->hydro_properties->convert_at_h_max;
@@ -922,7 +923,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);
       }
     }
 
@@ -1414,7 +1416,6 @@ 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);
diff --git a/src/debug.c b/src/debug.c
index 0b1538b5c4c72e1b292fb29e57a6526c38a4ff7a..2db8f69dff21c88ddd3f5ef4d9639e02ce394d89 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -74,6 +74,8 @@
 #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)
diff --git a/src/equation_of_state/barotropic/equation_of_state.h b/src/equation_of_state/barotropic/equation_of_state.h
index d91b0c0ce4cfe8b864f773c877d755f8dfcf77a7..71be2944437879bb0fa65142d7674fa830ed824c 100644
--- a/src/equation_of_state/barotropic/equation_of_state.h
+++ b/src/equation_of_state/barotropic/equation_of_state.h
@@ -56,7 +56,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -76,7 +76,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -96,7 +96,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -116,7 +116,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -135,7 +135,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -155,7 +155,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -175,7 +175,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -195,7 +195,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -214,7 +214,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) {
 
   const float density_frac = density * eos.inverse_core_density;
   const float density_factor = pow_gamma(density_frac);
@@ -241,7 +241,7 @@ INLINE static void eos_init(struct eos_parameters *e,
       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. / parser_get_param_float(params, "EoS:barotropic_core_density");
+      1.f / parser_get_param_float(params, "EoS:barotropic_core_density");
 }
 /**
  * @brief Print the equation of state
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 a6d1a4e336a03de108dd6787edb9e66346c1bb43..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->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);
+    /* 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/feedback/GEAR/initial_mass_function.c b/src/feedback/GEAR/initial_mass_function.c
index 58cf13dcfe2213bc7eefdf30c2b1c1430b20e09f..6706fd35c7561148c36f313f3345e3d7829e695f 100644
--- a/src/feedback/GEAR/initial_mass_function.c
+++ b/src/feedback/GEAR/initial_mass_function.c
@@ -20,6 +20,7 @@
 /* local headers */
 #include "initial_mass_function.h"
 
+#include "exp10.h"
 #include "hdf5_functions.h"
 #include "stellar_evolution_struct.h"
 
diff --git a/src/feedback/GEAR/stellar_evolution.c b/src/feedback/GEAR/stellar_evolution.c
index fd6ec03f193cd1b549cd02409bf43287dcf8c0a8..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"
@@ -635,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.
  *
@@ -688,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.
  *
@@ -706,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");
diff --git a/src/feedback/GEAR/stellar_evolution.h b/src/feedback/GEAR/stellar_evolution.h
index b2229b2c42040fd651eccbe28e3c25600d8fa015..a5afdf9079c07f42d673a6ec62b0954b8d3044f8 100644
--- a/src/feedback/GEAR/stellar_evolution.h
+++ b/src/feedback/GEAR/stellar_evolution.h
@@ -62,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,
@@ -79,5 +82,4 @@ 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 d3de0f1aa215c181bcd3f93ce3f7247f82d70c6e..7103c277d3f85cd528675a6e079a405e0ae14516 100644
--- a/src/feedback/GEAR/stellar_evolution_struct.h
+++ b/src/feedback/GEAR/stellar_evolution_struct.h
@@ -191,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;
 
diff --git a/src/hydro.h b/src/hydro.h
index 048be9f314a22f14246cd39434dfcbb66ec610bf..c82c402e81f2937f9beaf389b4a0a68506236dfc 100644
--- a/src/hydro.h
+++ b/src/hydro.h
@@ -70,6 +70,10 @@
 #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"
@@ -89,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/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_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 ed78acf691d2ac172cd2b8fa81456a2514fdcb98..5a64a284cc4b7121bb50dec4f92df717cdef3ad5 100644
--- a/src/hydro_io.h
+++ b/src/hydro_io.h
@@ -43,6 +43,8 @@
 #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 645a5fe5af9be5b67e4e1c9f17acc13c95938d96..46d93ad43a212f3db84597d3a4cfccbaff834605 100644
--- a/src/hydro_parameters.h
+++ b/src/hydro_parameters.h
@@ -52,6 +52,8 @@
 #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/part.h b/src/part.h
index bb574651b5a1fdb2005beeeacdc00208adb09f96..c68c91cfa804e476cc41cd26db55d5e1397790e2 100644
--- a/src/part.h
+++ b/src/part.h
@@ -80,6 +80,11 @@ struct threadpool;
 #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
diff --git a/src/rt/GEAR/rt_iact.h b/src/rt/GEAR/rt_iact.h
index 9fce2eeb39b4b10f6985cb8ff306f08064e807d0..b356dd4d3ba5fc8a90a5a78964ec6a72f3394416 100644
--- a/src/rt/GEAR/rt_iact.h
+++ b/src/rt/GEAR/rt_iact.h
@@ -97,7 +97,7 @@ runner_iact_nonsym_rt_injection_prep(const float r2, const float dx[3],
  * @param rt_props Properties of the RT scheme.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
-    const float r2, float dx[3], const float hi, const float hj,
+    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) {
 
diff --git a/src/rt/SPHM1RT/rt_iact.h b/src/rt/SPHM1RT/rt_iact.h
index bead428d752b2cd83c4e462b48c2910ad23abc20..5b8ddd84999351111faebddde9d00d04a95d24e0 100644
--- a/src/rt/SPHM1RT/rt_iact.h
+++ b/src/rt/SPHM1RT/rt_iact.h
@@ -100,7 +100,7 @@ runner_iact_nonsym_rt_injection_prep(const float r2, const float dx[3],
  * @param rt_props Properties of the RT scheme.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
-    const float r2, float dx[3], const float hi, const float hj,
+    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) {
 
diff --git a/src/rt/debug/rt_iact.h b/src/rt/debug/rt_iact.h
index d4a4f0d08c8fe004259035c25dbe7138a30ede54..29538711ef033baa8eae5e3f75fe2302a7df5549 100644
--- a/src/rt/debug/rt_iact.h
+++ b/src/rt/debug/rt_iact.h
@@ -66,7 +66,7 @@ runner_iact_nonsym_rt_injection_prep(const float r2, const float dx[3],
  * @param rt_props Properties of the RT scheme.
  */
 __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
-    const float r2, float dx[3], const float hi, const float hj,
+    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) {
 
diff --git a/src/runner_ghost.c b/src/runner_ghost.c
index 77d584a37cee45869fc2055e48e7018af131db37..830298e2cae955fbc4756e922b41e2a00092a94c 100644
--- a/src/runner_ghost.c
+++ b/src/runner_ghost.c
@@ -99,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;
 
@@ -297,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;
           }
@@ -561,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
@@ -602,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;
 
@@ -721,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;
           }
@@ -1125,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;
 
@@ -1327,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;
           }
@@ -1749,7 +1766,7 @@ void runner_do_sinks_density_ghost(struct runner *r, struct cell *c,
 
   /* Running value of the maximal smoothing length */
   float h_max = c->sinks.h_max;
-  float h_max_active = c->sinks.h_max_active;
+  float h_max_active = 0.f;
 
   TIMER_TIC;
 
@@ -1887,6 +1904,10 @@ void runner_do_sinks_density_ghost(struct runner *r, struct cell *c,
             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;
             }
diff --git a/src/runner_main.c b/src/runner_main.c
index a6f0de3b6c7860fe982e2e1be9d53059bf8d4293..a77912f84a4ed37407dc89a4ace9683488cafec9 100644
--- a/src/runner_main.c
+++ b/src/runner_main.c
@@ -223,8 +223,13 @@ void *runner_main(void *data) {
                                           /*limit_h_max=*/0);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
+#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, /*limit_h_min=*/0,
@@ -283,8 +288,13 @@ void *runner_main(void *data) {
                                           /*limit_h_max=*/0);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
+#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, /*limit_h_min=*/0,
@@ -340,7 +350,11 @@ void *runner_main(void *data) {
             runner_dosub_self1_density(r, ci, /*below_h_max=*/0, 1);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
+#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, /*below_h_max=*/0, 1);
@@ -388,7 +402,11 @@ void *runner_main(void *data) {
             runner_dosub_pair1_density(r, ci, cj, /*below_h_max=*/0, 1);
 #ifdef EXTRA_HYDRO_LOOP
           else if (t->subtype == task_subtype_gradient)
+#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, /*below_h_max=*/0, 1);
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/scheduler.c b/src/scheduler.c
index 2948b3dd843642f8d88fffac25efe65ac0bb16c2..4683cca3082dcc354707a8d5efffc7443266d838 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"
diff --git a/src/sink/GEAR/sink_io.h b/src/sink/GEAR/sink_io.h
index fe86573da2656253910489946350e2aa42b3e3da..79a427be5d705c1c3d30d96eaaca06bdffc3453a 100644
--- a/src/sink/GEAR/sink_io.h
+++ b/src/sink/GEAR/sink_io.h
@@ -127,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 = 10;
+  *num_fields = 11;
 
   /* List what we want to write */
   list[0] = io_make_output_field_convert_sink(
@@ -179,12 +179,12 @@ INLINE static void sink_write_particles(const struct sink* sinks,
       "Physical swallowed angular momentum of the particles");
 
   if (with_cosmology) {
-    list[9] = io_make_physical_output_field(
+    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[9] =
+    list[10] =
         io_make_output_field("BirthTimes", FLOAT, 1, UNIT_CONV_TIME, 0.f, sinks,
                              birth_time, "Times at which the sinks were born");
   }
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/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/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/tools.c b/src/tools.c
index 65d7d976ccc060e8a2111acf88a3a3cc862811bf..44c8fbdc39dc51c5dc6361b0a9a5936d46f63054 100644
--- a/src/tools.c
+++ b/src/tools.c
@@ -55,6 +55,7 @@
 #include "runner.h"
 #include "sink_iact.h"
 #include "sink_properties.h"
+#include "space_getsid.h"
 #include "star_formation_iact.h"
 #include "stars.h"
 
@@ -205,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) {
@@ -223,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)) {
@@ -258,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)) {
@@ -287,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) {
@@ -305,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 */
       }
     }
   }
@@ -338,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 */
       }
     }
   }
@@ -365,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) {
@@ -383,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) {
@@ -416,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) {
@@ -520,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;
@@ -534,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];
@@ -542,37 +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);
+        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);
+        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);
       }
     }
   }
@@ -580,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;
@@ -594,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];
@@ -602,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 */
       }
     }
   }
@@ -632,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;
@@ -646,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];
@@ -654,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);
       }
     }
   }
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/tests/Makefile.am b/tests/Makefile.am
index c0793bd0bfe2d0a2def6a590fb0a09b78ba36ea6..e1c2e905dcedf2cea1a4abfae6f4056913b6a680 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -182,10 +182,11 @@ EXTRA_DIST = testReading.sh makeInput.py testActivePair.sh \
              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 \
+             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 testZoomInit.sh \
-	     zoom_params_small.yml zoom_params_large.yml testGetZoomIJKFunc.sh testZoomRegrid.sh testZoomVoidTree.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 \
+             testZoomInit.sh zoom_params_small.yml zoom_params_large.yml testGetZoomIJKFunc.sh \
+             testZoomRegrid.sh testZoomVoidTree.sh
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 063d414098e2a82f45a5679eb19cc53be36bd00e..affbdf306743f7d0f1b46eb0a05a93a0bcc6a3a1 100644
--- a/tests/test125cells.c
+++ b/tests/test125cells.c
@@ -115,7 +115,9 @@ 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);
@@ -219,6 +221,11 @@ 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);
     adaptive_softening_init_part(p);
@@ -289,6 +296,9 @@ struct cell *make_cell(size_t n, const double offset[3], double size, double h,
 #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);
@@ -379,7 +389,7 @@ void dump_particle_fields(char *fileName, struct cell *main_cell,
 #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(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,
@@ -442,11 +452,20 @@ void runner_dopair1_branch_density(struct runner *r, struct cell *ci,
 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, 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, int limit_h_min,
@@ -673,7 +692,7 @@ int main(int argc, char *argv[]) {
     for (int j = 0; j < 125; ++j)
       runner_do_hydro_sort(&runner, cells[j], 0x1FFF, 0, 0, 0, 0);
 
-      /* Do the density calculation */
+    /* Do the density calculation */
 
 /* Initialise the particle cache. */
 #ifdef WITH_VECTORIZATION
@@ -750,247 +769,260 @@ int main(int argc, char *argv[]) {
 
                 struct cell *cj = cells[iii * 25 + jjj * 5 + kkk];
 
-                if (cj > ci)
+                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
+                }
               }
             }
           }
         }
       }
-    }
 
-    /* And now the self-interaction for the central cells */
-    for (int j = 0; j < 27; ++j)
-      runner_doself1_branch_gradient(&runner, inner_cells[j], /*limit_h_min=*/0,
-                                     /*limit_h_max=*/0);
+      /* And now the self-interaction for the central cells */
+      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)
-      runner_do_extra_ghost(&runner, inner_cells[j], 0);
+      /* Extra ghost to finish everything on the central cells */
+      for (int j = 0; j < 27; ++j)
+        runner_do_extra_ghost(&runner, inner_cells[j], 0);
 
 #endif /* EXTRA_HYDRO_LOOP */
 
       /* Do the force calculation */
 
 #ifdef WITH_VECTORIZATION
-    /* Initialise the cache. */
-    cache_clean(&runner.ci_cache);
-    cache_clean(&runner.cj_cache);
-    cache_init(&runner.ci_cache, 512);
-    cache_init(&runner.cj_cache, 512);
+      /* Initialise the cache. */
+      cache_clean(&runner.ci_cache);
+      cache_clean(&runner.cj_cache);
+      cache_init(&runner.ci_cache, 512);
+      cache_init(&runner.cj_cache, 512);
 #endif
 
-    int ctr = 0;
-    /* Do the pairs (for the central 27 cells) */
-    for (int i = 1; i < 4; i++) {
-      for (int j = 1; j < 4; j++) {
-        for (int k = 1; k < 4; k++) {
+      int ctr = 0;
+      /* Do the pairs (for the central 27 cells) */
+      for (int i = 1; i < 4; i++) {
+        for (int j = 1; j < 4; j++) {
+          for (int k = 1; k < 4; k++) {
 
-          struct cell *cj = cells[i * 25 + j * 5 + k];
+            struct cell *cj = cells[i * 25 + j * 5 + k];
 
-          if (main_cell != cj) {
+            if (main_cell != cj) {
 
-            const ticks sub_tic = getticks();
+              const ticks sub_tic = getticks();
 
-            runner_dopair2_branch_force(&runner, main_cell, cj,
-                                        /*limit_h_min=*/0, /*limit_h_max=*/0);
+              runner_dopair2_branch_force(&runner, main_cell, cj,
+                                          /*limit_h_min=*/0, /*limit_h_max=*/0);
 
-            timings[ctr++] += getticks() - sub_tic;
+              timings[ctr++] += getticks() - sub_tic;
+            }
           }
         }
       }
-    }
 
-    ticks self_tic = getticks();
+      ticks self_tic = getticks();
 
-    /* And now the self-interaction for the main cell */
-    runner_doself2_branch_force(&runner, main_cell, /*limit_h_min=*/0,
-                                /*limit_h_max=*/0);
+      /* And now the self-interaction for the main cell */
+      runner_doself2_branch_force(&runner, main_cell, /*limit_h_min=*/0,
+                                  /*limit_h_max=*/0);
 
-    timings[26] += getticks() - self_tic;
+      timings[26] += getticks() - self_tic;
 
-    /* Finally, give a gentle kick */
-    runner_do_end_hydro_force(&runner, main_cell, 0);
-    const ticks toc = getticks();
-    time += toc - tic;
+      /* Finally, give a gentle kick */
+      runner_do_end_hydro_force(&runner, main_cell, 0);
+      const ticks toc = getticks();
+      time += toc - tic;
 
-    /* Dump if necessary */
-    if (n == 0) {
-      sprintf(outputFileName, "swift_dopair_125_%.150s.dat",
-              outputFileNameExtension);
-      dump_particle_fields(outputFileName, main_cell, solution, 0);
-    }
+      /* Dump if necessary */
+      if (n == 0) {
+        sprintf(outputFileName, "swift_dopair_125_%.150s.dat",
+                outputFileNameExtension);
+        dump_particle_fields(outputFileName, main_cell, solution, 0);
+      }
 
-    for (int i = 0; i < 125; ++i) {
-      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]);
+      for (int i = 0; i < 125; ++i) {
+        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]);
+        }
       }
     }
-  }
 
-  /* Output timing */
-  ticks corner_time = timings[0] + timings[2] + timings[6] + timings[8] +
-                      timings[17] + timings[19] + timings[23] + timings[25];
+    /* Output timing */
+    ticks corner_time = timings[0] + timings[2] + timings[6] + timings[8] +
+                        timings[17] + timings[19] + timings[23] + timings[25];
 
-  ticks edge_time = timings[1] + timings[3] + timings[5] + timings[7] +
-                    timings[9] + timings[11] + timings[14] + timings[16] +
-                    timings[18] + timings[20] + timings[22] + timings[24];
+    ticks edge_time = timings[1] + timings[3] + timings[5] + timings[7] +
+                      timings[9] + timings[11] + timings[14] + timings[16] +
+                      timings[18] + timings[20] + timings[22] + timings[24];
 
-  ticks face_time = timings[4] + timings[10] + timings[12] + timings[13] +
-                    timings[15] + timings[21];
+    ticks face_time = timings[4] + timings[10] + timings[12] + timings[13] +
+                      timings[15] + timings[21];
 
-  ticks self_time = timings[26];
+    ticks self_time = timings[26];
 
-  message("Corner calculations took:     %.3f %s.",
-          clocks_from_ticks(corner_time / runs), clocks_getunit());
-  message("Edge calculations took:       %.3f %s.",
-          clocks_from_ticks(edge_time / runs), clocks_getunit());
-  message("Face calculations took:       %.3f %s.",
-          clocks_from_ticks(face_time / runs), clocks_getunit());
-  message("Self calculations took:       %.3f %s.",
-          clocks_from_ticks(self_time / runs), clocks_getunit());
-  message("SWIFT calculation took:       %.3f %s.",
-          clocks_from_ticks(time / runs), clocks_getunit());
+    message("Corner calculations took:     %.3f %s.",
+            clocks_from_ticks(corner_time / runs), clocks_getunit());
+    message("Edge calculations took:       %.3f %s.",
+            clocks_from_ticks(edge_time / runs), clocks_getunit());
+    message("Face calculations took:       %.3f %s.",
+            clocks_from_ticks(face_time / runs), clocks_getunit());
+    message("Self calculations took:       %.3f %s.",
+            clocks_from_ticks(self_time / runs), clocks_getunit());
+    message("SWIFT calculation took:       %.3f %s.",
+            clocks_from_ticks(time / runs), clocks_getunit());
 
-  for (int j = 0; j < 125; ++j)
-    reset_particles(cells[j], &space.hs, vel, press, size, rho);
+    for (int j = 0; j < 125; ++j)
+      reset_particles(cells[j], &space.hs, vel, press, size, rho);
 
-  /* NOW BRUTE-FORCE CALCULATION */
+    /* NOW BRUTE-FORCE CALCULATION */
 
-  const ticks tic = getticks();
+    const ticks tic = getticks();
 
-  /* Kick the central cell */
-  // runner_do_kick1(&runner, main_cell, 0);
+    /* Kick the central cell */
+    // runner_do_kick1(&runner, main_cell, 0);
 
-  /* And drift it */
-  // runner_do_drift_particles(&runner, main_cell, 0);
+    /* And drift it */
+    // runner_do_drift_particles(&runner, main_cell, 0);
 
-  /* Initialise the particles */
-  // for (int j = 0; j < 125; ++j) runner_do_drift_particles(&runner, cells[j],
-  // 0);
+    /* Initialise the particles */
+    // for (int j = 0; j < 125; ++j) runner_do_drift_particles(&runner,
+    // cells[j], 0);
 
-  /* Do the density calculation */
+    /* Do the density calculation */
 
-  /* Run all the pairs (only once !)*/
-  for (int i = 0; i < 5; i++) {
-    for (int j = 0; j < 5; j++) {
-      for (int k = 0; k < 5; k++) {
+    /* Run all the pairs (only once !)*/
+    for (int i = 0; i < 5; i++) {
+      for (int j = 0; j < 5; j++) {
+        for (int k = 0; k < 5; k++) {
 
-        struct cell *ci = cells[i * 25 + j * 5 + k];
+          struct cell *ci = cells[i * 25 + j * 5 + k];
 
-        for (int ii = -1; ii < 2; ii++) {
-          int iii = i + ii;
-          if (iii < 0 || iii >= 5) continue;
-          iii = (iii + 5) % 5;
-          for (int jj = -1; jj < 2; jj++) {
-            int jjj = j + jj;
-            if (jjj < 0 || jjj >= 5) continue;
-            jjj = (jjj + 5) % 5;
-            for (int kk = -1; kk < 2; kk++) {
-              int kkk = k + kk;
-              if (kkk < 0 || kkk >= 5) continue;
-              kkk = (kkk + 5) % 5;
+          for (int ii = -1; ii < 2; ii++) {
+            int iii = i + ii;
+            if (iii < 0 || iii >= 5) continue;
+            iii = (iii + 5) % 5;
+            for (int jj = -1; jj < 2; jj++) {
+              int jjj = j + jj;
+              if (jjj < 0 || jjj >= 5) continue;
+              jjj = (jjj + 5) % 5;
+              for (int kk = -1; kk < 2; kk++) {
+                int kkk = k + kk;
+                if (kkk < 0 || kkk >= 5) continue;
+                kkk = (kkk + 5) % 5;
 
-              struct cell *cj = cells[iii * 25 + jjj * 5 + kkk];
+                struct cell *cj = cells[iii * 25 + jjj * 5 + kkk];
 
-              if (cj > ci) pairs_all_density(&runner, ci, cj);
+                if (cj > ci) pairs_all_density(&runner, ci, cj);
+              }
             }
           }
         }
       }
     }
-  }
 
-  /* And now the self-interaction for the central cells*/
-  for (int j = 0; j < 27; ++j) self_all_density(&runner, inner_cells[j]);
+    /* And now the self-interaction for the central cells*/
+    for (int j = 0; j < 27; ++j) self_all_density(&runner, inner_cells[j]);
 
-  /* Ghost to finish everything on the central cells */
-  for (int j = 0; j < 27; ++j) runner_do_ghost(&runner, inner_cells[j], 0);
+    /* Ghost to finish everything on the central cells */
+    for (int j = 0; j < 27; ++j) runner_do_ghost(&runner, inner_cells[j], 0);
 
 #ifdef EXTRA_HYDRO_LOOP
-  /* We need to do the gradient loop and the extra ghost! */
-
-  /* Run all the pairs (only once !)*/
-  for (int i = 0; i < 5; i++) {
-    for (int j = 0; j < 5; j++) {
-      for (int k = 0; k < 5; k++) {
-
-        struct cell *ci = cells[i * 25 + j * 5 + k];
-
-        for (int ii = -1; ii < 2; ii++) {
-          int iii = i + ii;
-          if (iii < 0 || iii >= 5) continue;
-          iii = (iii + 5) % 5;
-          for (int jj = -1; jj < 2; jj++) {
-            int jjj = j + jj;
-            if (jjj < 0 || jjj >= 5) continue;
-            jjj = (jjj + 5) % 5;
-            for (int kk = -1; kk < 2; kk++) {
-              int kkk = k + kk;
-              if (kkk < 0 || kkk >= 5) continue;
-              kkk = (kkk + 5) % 5;
-
-              struct cell *cj = cells[iii * 25 + jjj * 5 + kkk];
-
-              if (cj > ci) pairs_all_gradient(&runner, ci, cj);
+    /* We need to do the gradient loop and the extra ghost! */
+
+    /* Run all the pairs (only once !)*/
+    for (int i = 0; i < 5; i++) {
+      for (int j = 0; j < 5; j++) {
+        for (int k = 0; k < 5; k++) {
+
+          struct cell *ci = cells[i * 25 + j * 5 + k];
+
+          for (int ii = -1; ii < 2; ii++) {
+            int iii = i + ii;
+            if (iii < 0 || iii >= 5) continue;
+            iii = (iii + 5) % 5;
+            for (int jj = -1; jj < 2; jj++) {
+              int jjj = j + jj;
+              if (jjj < 0 || jjj >= 5) continue;
+              jjj = (jjj + 5) % 5;
+              for (int kk = -1; kk < 2; kk++) {
+                int kkk = k + kk;
+                if (kkk < 0 || kkk >= 5) continue;
+                kkk = (kkk + 5) % 5;
+
+                struct cell *cj = cells[iii * 25 + jjj * 5 + kkk];
+
+                if (cj > ci) pairs_all_gradient(&runner, ci, cj);
+              }
             }
           }
         }
       }
     }
-  }
 
-  /* And now the self-interaction for the central cells */
-  for (int j = 0; j < 27; ++j) self_all_gradient(&runner, inner_cells[j]);
+    /* And now the self-interaction for the central cells */
+    for (int j = 0; j < 27; ++j) self_all_gradient(&runner, inner_cells[j]);
 
-  /* Extra ghost to finish everything on the central cells */
-  for (int j = 0; j < 27; ++j)
-    runner_do_extra_ghost(&runner, inner_cells[j], 0);
+    /* Extra ghost to finish everything on the central cells */
+    for (int j = 0; j < 27; ++j)
+      runner_do_extra_ghost(&runner, inner_cells[j], 0);
 
 #endif /* EXTRA_HYDRO_LOOP */
 
-  /* Do the force calculation */
+    /* Do the force calculation */
 
-  /* Do the pairs (for the central 27 cells) */
-  for (int i = 1; i < 4; i++) {
-    for (int j = 1; j < 4; j++) {
-      for (int k = 1; k < 4; k++) {
+    /* Do the pairs (for the central 27 cells) */
+    for (int i = 1; i < 4; i++) {
+      for (int j = 1; j < 4; j++) {
+        for (int k = 1; k < 4; k++) {
 
-        struct cell *cj = cells[i * 25 + j * 5 + k];
+          struct cell *cj = cells[i * 25 + j * 5 + k];
 
-        if (main_cell != cj) pairs_all_force(&runner, main_cell, cj);
+          if (main_cell != cj) pairs_all_force(&runner, main_cell, cj);
+        }
       }
     }
-  }
 
-  /* And now the self-interaction for the main cell */
-  self_all_force(&runner, main_cell);
+    /* And now the self-interaction for the main cell */
+    self_all_force(&runner, main_cell);
 
-  /* Finally, give a gentle kick */
-  runner_do_end_hydro_force(&runner, main_cell, 0);
-  // runner_do_kick2(&runner, main_cell, 0);
+    /* Finally, give a gentle kick */
+    runner_do_end_hydro_force(&runner, main_cell, 0);
+    // runner_do_kick2(&runner, main_cell, 0);
 
-  const ticks toc = getticks();
+    const ticks toc = getticks();
 
-  /* Output timing */
-  message("Brute force calculation took : %.3f %s.",
-          clocks_from_ticks(toc - tic), clocks_getunit());
+    /* Output timing */
+    message("Brute force calculation took : %.3f %s.",
+            clocks_from_ticks(toc - tic), clocks_getunit());
 
-  sprintf(outputFileName, "brute_force_125_%.150s.dat",
-          outputFileNameExtension);
-  dump_particle_fields(outputFileName, main_cell, solution, 0);
+    sprintf(outputFileName, "brute_force_125_%.150s.dat",
+            outputFileNameExtension);
+    dump_particle_fields(outputFileName, main_cell, solution, 0);
 
-  /* Clean things to make the sanitizer happy ... */
-  for (int i = 0; i < 125; ++i) clean_up(cells[i]);
-  free(solution);
+    /* Clean things to make the sanitizer happy ... */
+    for (int i = 0; i < 125; ++i) clean_up(cells[i]);
+    free(solution);
 
 #ifdef WITH_VECTORIZATION
-  cache_clean(&runner.ci_cache);
-  cache_clean(&runner.cj_cache);
+    cache_clean(&runner.ci_cache);
+    cache_clean(&runner.cj_cache);
 #endif
 
-  return 0;
-}
+    return 0;
+  }
diff --git a/tests/test27cells.c b/tests/test27cells.c
index 1a91613f6ae6d3d770ca45252c46544a198f7056..e90d4e1781058c66c71390b357486749ec39c176 100644
--- a/tests/test27cells.c
+++ b/tests/test27cells.c
@@ -160,6 +160,9 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
 #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;
diff --git a/tests/testActivePair.c b/tests/testActivePair.c
index d2eb6b29ebe8eb5bb06a6904f7027b70b799eb1c..1a9f5e849c89731941138957fb51c590a7240219 100644
--- a/tests/testActivePair.c
+++ b/tests/testActivePair.c
@@ -128,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)
@@ -282,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.);
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 005c4666b71bc41f8e31f0afa0bd017358eaa9bb..960c2f92543ee278415842d9682e108da5051d70 100644
--- a/tests/testInteractions.c
+++ b/tests/testInteractions.c
@@ -114,7 +114,8 @@ void prepare_force(struct part *parts, size_t count) {
 #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(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];
diff --git a/tests/testPeriodicBC.c b/tests/testPeriodicBC.c
index 05918b5b5e861a2ffc9342b5248790fd77b3e362..be31b66051e6f5a17902a26b3c069f621731f0f7 100644
--- a/tests/testPeriodicBC.c
+++ b/tests/testPeriodicBC.c
@@ -139,6 +139,9 @@ struct cell *make_cell(size_t n, double *offset, double size, double h,
 #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;
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 */