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/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..e1ab89fd99c42cbfbb0fcc57eabf1c97f3462e93
--- /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:      50         # 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/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..6b710d7675e709ff98f5c4de166266bfa7ea95b2 100755
--- a/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py
+++ b/examples/SinkParticles/HomogeneousBoxSinkParticles/makeIC.py
@@ -214,7 +214,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)
 
 #####################