diff --git a/.gitignore b/.gitignore
index a6b0a250fb2818f954403ad6899dc39d0db16919..7ad337771989f94131f7091d4ed55e285582cf4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -172,6 +172,8 @@ tests/testCooling
 tests/testComovingCooling
 tests/testHashmap
 tests/testNeutrinoCosmology
+tests/testNeutrinoFermiDirac
+tests/testLog
 tests/*.png
 tests/*.txt
 
diff --git a/doc/RTD/source/Neutrinos/index.rst b/doc/RTD/source/Neutrinos/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..df9af9e3ea19853eb4fa9f08ef1156ad9c6c90f5
--- /dev/null
+++ b/doc/RTD/source/Neutrinos/index.rst
@@ -0,0 +1,79 @@
+.. Neutrinos
+   Willem Elbers, 7 April 2021
+
+.. _neutrinos:
+
+Neutrino implementation
+=======================
+
+SWIFT can also accurately model the effects of massive neutrinos in
+cosmological simulations. At the background level, massive neutrinos
+and other relativistic species can be included by specifying their
+number and masses in the cosmology section of the parameter file
+(see :ref:`Parameters_cosmology`).
+
+At the perturbation level, neutrinos can be included as a separate particle
+species (``PartType6``). To facilitate this, SWIFT implements the
+:math:`\delta f` method for shot noise suppression (`Elbers et al. 2020
+<https://ui.adsabs.harvard.edu/abs/2020arXiv201007321E/>`_). The method
+works by statistically weighting the particles during the simulation,
+with weights computed from the Liouville equation using current and
+initial momenta. The method can be activated by specifying
+``Neutrino:use_delta_f`` in the parameter file.
+
+The implementation of the :math:`\delta f` method in SWIFT assumes a
+specific method for generating the initial neutrino momenta (see below).
+This makes it possible to reproduce the initial momentum when it is
+needed without increasing the memory footprint of the neutrino particles.
+If perturbed initial conditions are not needed, the initial momenta can
+be generated internally by specifying ``Neutrino:generate_ics`` in the
+parameter file. This will assign ``PartType6`` particles to each
+neutrino mass specified in the cosmology and generate new velocities
+based on the homogeneous (unperturbed) Fermi-Dirac distribution.
+
+Relativistic Drift
+------------------
+
+At high redshift, neutrino particles move faster than the speed of light
+if the usual Newtonian expressions are used. To rectify this, SWIFT
+implements a relativistic drift correction. In this convention, the
+internal velocity variable (see theory/Cosmology) is
+:math:`v^i=a^2u^i=a^2\dot{x}^i\gamma^{-1}`, where :math:`u^i` is the
+spatial part of the 4-velocity, :math:`a` the scale factor, and
+:math:`x^i` a comoving position vector. The conversion factor to the
+coordinate 3-velocity is :math:`\gamma=ac/\sqrt{a^2c^2+v^2}`. This
+factor is applied to the neutrino particles throughout the simulation.
+
+Generating Fermi-Dirac momenta
+------------------------------
+
+The implementation of the :math:`\delta f` method in SWIFT assumes that
+neutrinos were initially assigned a Fermi-Dirac momentum using the following
+method. Each particle has a fixed 64-bit unsigned integer :math:`\ell` given
+by the particle ID [#f1]_ (plus an optional seed: ``Neutrino:neutrino_seed``).
+This number is transformed into a floating point number :math:`u\in(0,1)`,
+using the following pseudo-code based on splitmix64:
+
+.. code-block:: none
+
+    m = l + 0x9E3779B97f4A7C15
+    m = (m ^ (m >> 30)) * 0xBF58476D1CE4E5B9;
+    m = (m ^ (m >> 27)) * 0x94D049BB133111EB;
+    m = m ^ (m >> 31);
+    u = (m + 0.5) / (UINT64_MAX + 1)
+
+This is subsequently transformed into a Fermi-Dirac momentum
+:math:`q = F^{-1}(u)` by evaluating the quantile function. To generate
+neutrino particle initial conditions with perturbations, one first generates
+momenta from the unperturbed Fermi-Dirac distribution using the above method
+and then applies perturbations in any suitable manner.
+
+When using the :math:`\delta f` method, SWIFT also assumes that ``PartType6``
+particles are assigned to all :math:`N_\nu` massive species present in the
+cosmology, such that the particle with fixed integer :math:`\ell` corresponds
+to species :math:`i = \ell\; \% \;N_\nu\in[0,N_\nu-1]`.
+
+The sampled Fermi-Dirac speeds and neutrino masses are written into the
+snapshot files as ``SampledSpeeds`` and ``MicroscopicMasses``.
+
+.. [#f1] Currently, it is not guaranteed that a particle ID is unique.
diff --git a/doc/RTD/source/ParameterFiles/parameter_description.rst b/doc/RTD/source/ParameterFiles/parameter_description.rst
index 5cdb52cdd74f1870c985d9feab012ec1620fb0c0..34c8f1b68deae0bb030b96202c6662c915156028 100644
--- a/doc/RTD/source/ParameterFiles/parameter_description.rst
+++ b/doc/RTD/source/ParameterFiles/parameter_description.rst
@@ -1481,3 +1481,24 @@ gparts are active after that snapshot output timestep.
 
 
 .. [#f2] which would translate into a constant :math:`G_N=1.5517771\times10^{-9}~cm^{3}\,g^{-1}\,s^{-2}` if expressed in the CGS system.
+
+Neutrinos
+---------
+
+The ``Neutrino`` section of the parameter file controls the behaviour of
+neutrino particles (``PartType6``). This assumes that massive neutrinos have
+been specified in the ``Cosmology`` section described above. Random
+Fermi-Dirac momenta will be generated if ``generate_ics`` is used. The
+:math:`\delta f` method for shot noise reduction can be activated with
+``use_delta_f``. Finally, a random seed for the Fermi-Dirac momenta can
+be set with ``neutrino_seed``.
+
+For mode details on the neutrino implementation, refer to :ref:`Neutrinos`. 
+A complete specification of the model looks like
+
+.. code:: YAML
+
+  Neutrino:
+    generate_ics:  1    # Replace neutrino particle velocities with random Fermi-Dirac momenta at the start
+    use_delta_f:   1    # Use the delta-f method for shot noise reduction
+    neutrino_seed: 1234 # A random seed used for the Fermi-Dirac momenta
diff --git a/doc/RTD/source/conf.py b/doc/RTD/source/conf.py
index 35d5b592800cc8556d074c6a5bbf68d9ec21e54c..f0e354eb06f49d437584f9f5fa2cec42bea5562d 100644
--- a/doc/RTD/source/conf.py
+++ b/doc/RTD/source/conf.py
@@ -19,7 +19,7 @@
 # -- Project information -----------------------------------------------------
 
 project = 'SWIFT: SPH With Inter-dependent Fine-grained Tasking'
-copyright = '2014-2020, SWIFT Collaboration'
+copyright = '2014-2021, SWIFT Collaboration'
 author = 'SWIFT Team'
 
 # The short X.Y version
diff --git a/doc/RTD/source/index.rst b/doc/RTD/source/index.rst
index 381f857728399024afe6a9b97c6e50935ea6d80d..d09c27fe5707f0ba576a5bfb7c7c9d2e11df1a4b 100644
--- a/doc/RTD/source/index.rst
+++ b/doc/RTD/source/index.rst
@@ -29,6 +29,7 @@ difference is the parameter file that will need to be adapted for SWIFT.
    LineOfSights/index
    EquationOfState/index
    ExternalPotentials/index
+   Neutrinos/index
    NewOption/index
    Task/index
    AnalysisTools/index
diff --git a/examples/Cosmology/NeutrinoCosmo/neutrino_cosmo.yml b/examples/Cosmology/NeutrinoCosmo/neutrino_cosmo.yml
index 9acf793e6f96f5b2f47f4814cd5f341c1c0483d7..f3d682d055427e8fdd3f2909371918a4e309f53c 100644
--- a/examples/Cosmology/NeutrinoCosmo/neutrino_cosmo.yml
+++ b/examples/Cosmology/NeutrinoCosmo/neutrino_cosmo.yml
@@ -43,6 +43,10 @@ InitialConditions:
   file_name:	particles.hdf5
   periodic:	1
 
+Neutrino:
+  use_delta_f: 1   # Use the delta-f method for shot noise reduction
+  generate_ics: 1  # Generate new Fermi-Dirac velocities for the particles
+
 Gravity:
   mesh_side_length:	32
   MAC:			geometric
diff --git a/examples/main.c b/examples/main.c
index 1f96cd49239f0e44a89a17cc5ee6062b6ad2e755..37b11d4d0379405dfe765a3e1638e365a94f88a5 100644
--- a/examples/main.c
+++ b/examples/main.c
@@ -97,6 +97,7 @@ int main(int argc, char *argv[]) {
   struct hydro_props hydro_properties;
   struct stars_props stars_properties;
   struct sink_props sink_properties;
+  struct neutrino_props neutrino_properties;
   struct feedback_props feedback_properties;
   struct rt_props rt_properties;
   struct entropy_floor_properties entropy_floor;
@@ -1268,6 +1269,12 @@ int main(int argc, char *argv[]) {
     /* Do we have neutrino DM particles? */
     const int with_neutrinos = N_total[swift_type_neutrino] > 0;
 
+    /* Initialise the neutrino properties if we have neutrino particles */
+    bzero(&neutrino_properties, sizeof(struct neutrino_props));
+    if (with_neutrinos)
+      neutrino_props_init(&neutrino_properties, &prog_const, &us, params,
+                          &cosmo);
+
     /* Initialize the space with these data. */
     if (myrank == 0) clocks_gettime(&tic);
     space_init(&s, params, &cosmo, dim, &hydro_properties, parts, gparts, sinks,
@@ -1422,9 +1429,10 @@ int main(int argc, char *argv[]) {
                 N_total[swift_type_neutrino], engine_policies, talking,
                 &reparttype, &us, &prog_const, &cosmo, &hydro_properties,
                 &entropy_floor, &gravity_properties, &stars_properties,
-                &black_holes_properties, &sink_properties, &feedback_properties,
-                &rt_properties, &mesh, &potential, &cooling_func, &starform,
-                &chemistry, &fof_properties, &los_properties);
+                &black_holes_properties, &sink_properties, &neutrino_properties,
+                &feedback_properties, &rt_properties, &mesh, &potential,
+                &cooling_func, &starform, &chemistry, &fof_properties,
+                &los_properties);
     engine_config(/*restart=*/0, /*fof=*/0, &e, params, nr_nodes, myrank,
                   nr_threads, nr_pool_threads, with_aff, talking, restart_file);
 
diff --git a/examples/main_fof.c b/examples/main_fof.c
index 9002dde186f24be76100535c189c1f019e36c8ee..c7f40980b6b7d3311b9c24b8f46eb2000645fa2f 100644
--- a/examples/main_fof.c
+++ b/examples/main_fof.c
@@ -642,7 +642,7 @@ int main(int argc, char *argv[]) {
       engine_policies, talking, &reparttype, &us, &prog_const, &cosmo,
       /*hydro_properties=*/NULL, /*entropy_floor=*/NULL, &gravity_properties,
       /*stars_properties=*/NULL, /*black_holes_properties=*/NULL,
-      /*sink_properties=*/NULL,
+      /*sink_properties=*/NULL, /*neutrino_properties=*/NULL,
       /*feedback_properties=*/NULL, /*rt_properties=*/NULL, &mesh,
       /*potential=*/NULL,
       /*cooling_func=*/NULL, /*starform=*/NULL, /*chemistry=*/NULL,
diff --git a/examples/parameter_example.yml b/examples/parameter_example.yml
index 2f50e067f9501b07f63de4559837dad267f021ff..fda3c046a92eab8982ad618d3b675f63db9acf9e 100644
--- a/examples/parameter_example.yml
+++ b/examples/parameter_example.yml
@@ -638,6 +638,12 @@ EAGLEAGN:
   merger_max_distance_ratio:          3.0        # Maximal distance over which two BHs can merge, in units of the softening length.
   minimum_timestep_Myr:               1.0        # Minimum time-step size for BHs in Mega-years. The time-step size is computed based on the accretion rate and this serves as a minimum. Defaults to FLT_MAX.
 
+# Parameters related to the neutrino particles ----------------------------------
+Neutrino:
+  use_delta_f:                        1          # Use the delta-f method for shot noise reduction
+  generate_ics:                       0          # Automatically generate neutrino velocities at the start of the run
+  neutrino_seed:                      1234       # Seed used in the delta-f weighting step for Fermi-Dirac momentum generation
+
 # Parameters related to the sink particles ---------------------------------------
 
 # Default sink particles
diff --git a/src/Makefile.am b/src/Makefile.am
index 7f127064436d052924c452447ecde44dcd80821f..f6013014caeab4bd584ec7f1f083b4f7e5071475 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -177,6 +177,8 @@ AM_SOURCES += chemistry.c cosmology.c mesh_gravity.c velociraptor_interface.c
 AM_SOURCES += output_list.c velociraptor_dummy.c logger_io.c memuse.c mpiuse.c memuse_rnodes.c
 AM_SOURCES += fof.c fof_catalogue_io.c
 AM_SOURCES += hashmap.c pressure_floor.c logger_history.c
+AM_SOURCES += runner_neutrino.c
+AM_SOURCES += neutrino/Default/fermi_dirac.c
 AM_SOURCES += $(QLA_COOLING_SOURCES) 
 AM_SOURCES += $(EAGLE_COOLING_SOURCES) $(EAGLE_FEEDBACK_SOURCES) 
 AM_SOURCES += $(GRACKLE_COOLING_SOURCES) $(GEAR_FEEDBACK_SOURCES) 
@@ -186,7 +188,7 @@ AM_SOURCES += $(STAR_FORMATION_LOGGER)
 
 # Include files for distribution, not installation.
 nobase_noinst_HEADERS = align.h approx_math.h atomic.h barrier.h cycle.h error.h inline.h kernel_hydro.h kernel_gravity.h 
-nobase_noinst_HEADERS += gravity_iact.h kernel_long_gravity.h vector.h accumulate.h cache.h exp.h 
+nobase_noinst_HEADERS += gravity_iact.h kernel_long_gravity.h vector.h accumulate.h cache.h exp.h log.h
 nobase_noinst_HEADERS += runner_doiact_nosort.h runner_doiact_hydro.h runner_doiact_stars.h runner_doiact_black_holes.h runner_doiact_grav.h 
 nobase_noinst_HEADERS += runner_doiact_functions_hydro.h runner_doiact_functions_stars.h runner_doiact_functions_black_holes.h 
 nobase_noinst_HEADERS += runner_doiact_functions_limiter.h runner_doiact_limiter.h units.h intrinsics.h minmax.h 
@@ -400,7 +402,9 @@ nobase_noinst_HEADERS += pressure_floor/GEAR/pressure_floor_struct.h pressure_fl
 nobase_noinst_HEADERS += sink/Default/sink.h sink/Default/sink_io.h sink/Default/sink_part.h sink/Default/sink_properties.h 
 nobase_noinst_HEADERS += sink/Default/sink_iact.h
 nobase_noinst_HEADERS += sink.h sink_io.h sink_properties.h
-nobase_noinst_HEADERS += neutrino/neutrino.h neutrino/relativity.h
+nobase_noinst_HEADERS += neutrino.h neutrino_properties.h neutrino_io.h
+nobase_noinst_HEADERS += neutrino/Default/neutrino.h neutrino/Default/relativity.h neutrino/Default/fermi_dirac.h
+nobase_noinst_HEADERS += neutrino/Default/neutrino_properties.h neutrino/Default/neutrino_io.h
 
 # Sources and special flags for the gravity library
 libgrav_la_SOURCES = runner_doiact_grav.c
diff --git a/src/cell_drift.c b/src/cell_drift.c
index 1a128fe4caa1441a6f992b8458f453bd14fe5dfd..f541b4b89f5d0226a98d93949d958eaaf82ab55c 100644
--- a/src/cell_drift.c
+++ b/src/cell_drift.c
@@ -31,7 +31,7 @@
 #include "feedback.h"
 #include "gravity.h"
 #include "multipole.h"
-#include "neutrino/relativity.h"
+#include "neutrino.h"
 #include "pressure_floor.h"
 #include "rt.h"
 #include "star_formation.h"
@@ -281,7 +281,6 @@ void cell_drift_gpart(struct cell *c, const struct engine *e, int force) {
   const double a = e->cosmology->a;
   const double c_vel = e->physical_constants->const_speed_light_c;
   const int with_neutrinos = e->s->with_neutrinos;
-  const int with_relat_drift = with_neutrinos && (a < e->cosmology->a_nu_nr);
 
   /* Drift irrespective of cell flags? */
   force = (force || cell_get_flag(c, cell_flag_do_grav_drift));
@@ -344,7 +343,7 @@ void cell_drift_gpart(struct cell *c, const struct engine *e, int force) {
 
       /* Relativistic drift correction for neutrinos */
       double dt_drift_k = dt_drift;
-      if (with_relat_drift && gp->type == swift_type_neutrino) {
+      if (with_neutrinos && gp->type == swift_type_neutrino) {
         dt_drift_k *= relativistic_drift_factor(gp->v_full, a, c_vel);
       }
 
diff --git a/src/cell_grav.h b/src/cell_grav.h
index ec690966efa7d3b79b0f085d52a190499eddbcd5..b0780c7d570694be0670960b2b3f26b6d62c4bfd 100644
--- a/src/cell_grav.h
+++ b/src/cell_grav.h
@@ -77,6 +77,9 @@ struct cell_grav {
   /*! The task to end the force calculation */
   struct task *end_force;
 
+  /*! Task for weighting neutrino particles */
+  struct task *neutrino_weight;
+
   /*! Minimum end of (integer) time step in this cell for gravity tasks. */
   integertime_t ti_end_min;
 
diff --git a/src/cell_unskip.c b/src/cell_unskip.c
index d4eb0525f97a74a1d184a7ca81ebb3ea32ed9361..d229fbffe550353cc3f3c0275044450299d01dcb 100644
--- a/src/cell_unskip.c
+++ b/src/cell_unskip.c
@@ -1961,6 +1961,8 @@ int cell_unskip_gravity_tasks(struct cell *c, struct scheduler *s) {
     if (c->grav.down_in != NULL) scheduler_activate(s, c->grav.down_in);
     if (c->grav.long_range != NULL) scheduler_activate(s, c->grav.long_range);
     if (c->grav.end_force != NULL) scheduler_activate(s, c->grav.end_force);
+    if (c->grav.neutrino_weight != NULL)
+      scheduler_activate(s, c->grav.neutrino_weight);
 #ifdef WITH_LOGGER
     if (c->logger != NULL) scheduler_activate(s, c->logger);
 #endif
diff --git a/src/common_io.c b/src/common_io.c
index e1ffd211a37d57c9a291f60ba70b953ae28175ef..53694875ff3c17b4e084b3a7b89647e33c8512df 100644
--- a/src/common_io.c
+++ b/src/common_io.c
@@ -42,6 +42,7 @@
 #include "fof_io.h"
 #include "gravity_io.h"
 #include "hydro_io.h"
+#include "neutrino_io.h"
 #include "particle_splitting.h"
 #include "rt_io.h"
 #include "sink_io.h"
@@ -1641,6 +1642,27 @@ void io_select_dm_fields(const struct gpart* const gparts,
   }
 }
 
+/**
+ * @brief Select the fields to write to snapshots for the neutrino particles.
+ *
+ * @param gparts The #gpart's
+ * @param with_fof Are we running FoF?
+ * @param with_stf Are we running with structure finding?
+ * @param e The #engine (to access scheme properties).
+ * @param num_fields (return) The number of fields to write.
+ * @param list (return) The list of fields to write.
+ */
+void io_select_neutrino_fields(
+    const struct gpart* const gparts,
+    const struct velociraptor_gpart_data* gpart_group_data, const int with_fof,
+    const int with_stf, const struct engine* const e, int* const num_fields,
+    struct io_props* const list) {
+
+  darkmatter_write_particles(gparts, list, num_fields);
+
+  *num_fields += neutrino_write_particles(gparts, list + *num_fields);
+}
+
 /**
  * @brief Select the fields to write to snapshots for the sink particles.
  *
diff --git a/src/common_io.h b/src/common_io.h
index 1311e77e439ee75f57ce50e1f8ac5dc454c54a91..879a927fc6f04d857a7b1b9e351bfa2c4a60f5ec 100644
--- a/src/common_io.h
+++ b/src/common_io.h
@@ -227,6 +227,12 @@ void io_select_dm_fields(const struct gpart* const gparts,
                          const struct engine* const e, int* const num_fields,
                          struct io_props* const list);
 
+void io_select_neutrino_fields(
+    const struct gpart* const gparts,
+    const struct velociraptor_gpart_data* gpart_group_data, const int with_fof,
+    const int with_stf, const struct engine* const e, int* const num_fields,
+    struct io_props* const list);
+
 void io_select_sink_fields(const struct sink* const sinks,
                            const int with_cosmology, const int with_fof,
                            const int with_stf, const struct engine* const e,
diff --git a/src/common_io_fields.c b/src/common_io_fields.c
index c7d8d4bf300c714350fcc60fe3c2520ce67056c2..00fa41aaf6c0f6fc824cbd7feea2c1a9778133e7 100644
--- a/src/common_io_fields.c
+++ b/src/common_io_fields.c
@@ -85,15 +85,16 @@ int io_get_ptype_fields(const int ptype, struct io_props* list,
 
     case swift_type_dark_matter:
     case swift_type_dark_matter_background:
-    case swift_type_neutrino:
       io_select_dm_fields(NULL, NULL, with_fof, with_stf, /*e=*/NULL,
                           &num_fields, list);
       break;
-
+    case swift_type_neutrino:
+      io_select_neutrino_fields(NULL, NULL, with_fof, with_stf, /*e=*/NULL,
+                                &num_fields, list);
+      break;
     case swift_type_stars:
       io_select_star_fields(NULL, with_cosmology, with_fof, with_stf,
-                            /*with_rt=*/1,
-                            /*e=*/NULL, &num_fields, list);
+                            /*with_rt=*/1, /*e=*/NULL, &num_fields, list);
       break;
 
     case swift_type_sink:
diff --git a/src/cosmology.c b/src/cosmology.c
index 6603d5c1e77fc8bac7553087ed83490cfbad455b..23aa2877cdae08d9fc374b7ee0de3f29f11a58f9 100644
--- a/src/cosmology.c
+++ b/src/cosmology.c
@@ -801,7 +801,6 @@ void cosmology_init(struct swift_params *params, const struct unit_system *us,
     c->Omega_nu_0 = 0.;
     c->Omega_nu = 0.;
     c->N_eff = 0.;
-    c->a_nu_nr = 0.;
 
     c->neutrino_density_early_table = NULL;
     c->neutrino_density_late_table = NULL;
@@ -862,15 +861,6 @@ void cosmology_init(struct swift_params *params, const struct unit_system *us,
     for (int i = 0; i < c->N_nu; i++) {
       M_eV_min = fmin(M_eV_min, c->M_nu_eV[i]);
     }
-
-    /* Time when the relativistic drift correction is below 1% for all nu's */
-    if (c->N_nu > 0 && M_eV_min > 0.) {
-      const double five_sigma_velocity = 20.0 * c->T_nu_0_eV / M_eV_min;
-      const double approx_treshold = 0.14249;  // 1.0 / sqrt(1 + x^2) = 0.99
-      c->a_nu_nr = five_sigma_velocity / approx_treshold;
-    } else {
-      c->a_nu_nr = 0.;
-    }
   }
 
   /* Cold dark matter density */
@@ -927,7 +917,6 @@ void cosmology_init_no_cosmo(struct cosmology *c) {
   c->N_nu = 0;
   c->N_ur = 0.;
   c->N_eff = 0.;
-  c->a_nu_nr = 0.;
 
   c->a_begin = 1.;
   c->a_end = 1.;
@@ -1359,7 +1348,6 @@ void cosmology_write_model(hid_t h_grp, const struct cosmology *c) {
   io_write_attribute_d(h_grp, "N_eff", c->N_eff);
   io_write_attribute_d(h_grp, "N_ur", c->N_ur);
   io_write_attribute_d(h_grp, "N_nu", c->N_nu);
-  io_write_attribute_d(h_grp, "a_nu_nr", c->a_nu_nr);
   if (c->N_nu > 0) {
     io_write_attribute(h_grp, "M_nu_eV", DOUBLE, c->M_nu_eV, c->N_nu);
     io_write_attribute(h_grp, "deg_nu", DOUBLE, c->deg_nu, c->N_nu);
diff --git a/src/cosmology.h b/src/cosmology.h
index 2f15a696c6781c55b022060e18e709d5c2b0c4b4..47a17e26b50e527ba8b0bea4070618096021057c 100644
--- a/src/cosmology.h
+++ b/src/cosmology.h
@@ -198,9 +198,6 @@ struct cosmology {
   /*! Degeneracy of each massive neutrino species */
   double *deg_nu;
 
-  /*! Scale-factor after which all neutrinos move non-relativistically */
-  double a_nu_nr;
-
   /*! Log of starting expansion factor for neutrino interpolation tables */
   double log_a_long_begin;
 
diff --git a/src/distributed_io.c b/src/distributed_io.c
index 8ceec30b20fb6fddcf0825d40d73dbb74266d861..15451fd85fa856675c6205c135a31772563c79a9 100644
--- a/src/distributed_io.c
+++ b/src/distributed_io.c
@@ -703,8 +703,8 @@ void write_output_distributed(struct engine* e,
             gpart_group_data_written, Ntot, Ndm_neutrino, with_stf);
 
         /* Select the fields to write */
-        io_select_dm_fields(gparts_written, gpart_group_data_written, with_fof,
-                            with_stf, e, &num_fields, list);
+        io_select_neutrino_fields(gparts_written, gpart_group_data_written,
+                                  with_fof, with_stf, e, &num_fields, list);
       } break;
 
       case swift_type_sink: {
diff --git a/src/engine.c b/src/engine.c
index f59f328e33f4d0b63156be3139ea006f8c44a653..36b8f7048650b8197d623b146c692c29609b52d5 100644
--- a/src/engine.c
+++ b/src/engine.c
@@ -77,7 +77,8 @@
 #include "minmax.h"
 #include "mpiuse.h"
 #include "multipole_struct.h"
-#include "neutrino/neutrino.h"
+#include "neutrino.h"
+#include "neutrino_properties.h"
 #include "output_list.h"
 #include "output_options.h"
 #include "partition.h"
@@ -1572,6 +1573,7 @@ void engine_skip_force_and_kick(struct engine *e) {
         t->type == task_type_bh_swallow_ghost3 || t->type == task_type_bh_in ||
         t->type == task_type_bh_out || t->type == task_type_rt_ghost1 ||
         t->type == task_type_rt_ghost2 || t->type == task_type_rt_tchem ||
+        t->type == task_type_neutrino_weight ||
         t->subtype == task_subtype_force ||
         t->subtype == task_subtype_limiter ||
         t->subtype == task_subtype_gradient ||
@@ -2762,6 +2764,7 @@ void engine_unpin(void) {
  * @param stars The #stars_props used for this run.
  * @param black_holes The #black_holes_props used for this run.
  * @param sinks The #sink_props used for this run.
+ * @param neutrinos The #neutrino_props used for this run.
  * @param feedback The #feedback_props used for this run.
  * @param mesh The #pm_mesh used for the long-range periodic forces.
  * @param potential The properties of the external potential.
@@ -2782,7 +2785,8 @@ void engine_init(
     const struct entropy_floor_properties *entropy_floor,
     struct gravity_props *gravity, const struct stars_props *stars,
     const struct black_holes_props *black_holes, const struct sink_props *sinks,
-    struct feedback_props *feedback, struct rt_props *rt, struct pm_mesh *mesh,
+    const struct neutrino_props *neutrinos, struct feedback_props *feedback,
+    struct rt_props *rt, struct pm_mesh *mesh,
     const struct external_potential *potential,
     struct cooling_function_data *cooling_func,
     const struct star_formation *starform,
@@ -2873,6 +2877,7 @@ void engine_init(
   e->stars_properties = stars;
   e->black_holes_properties = black_holes;
   e->sink_properties = sinks;
+  e->neutrino_properties = neutrinos;
   e->mesh = mesh;
   e->external_potential = potential;
   e->cooling_func = cooling_func;
@@ -3358,6 +3363,7 @@ void engine_struct_dump(struct engine *e, FILE *stream) {
   rt_struct_dump(e->rt_props, stream);
   black_holes_struct_dump(e->black_holes_properties, stream);
   sink_struct_dump(e->sink_properties, stream);
+  neutrino_struct_dump(e->neutrino_properties, stream);
   chemistry_struct_dump(e->chemistry, stream);
 #ifdef WITH_FOF
   fof_struct_dump(e->fof_properties, stream);
@@ -3487,6 +3493,11 @@ void engine_struct_restore(struct engine *e, FILE *stream) {
   sink_struct_restore(sink_properties, stream);
   e->sink_properties = sink_properties;
 
+  struct neutrino_props *neutrino_properties =
+      (struct neutrino_props *)malloc(sizeof(struct neutrino_props));
+  neutrino_struct_restore(neutrino_properties, stream);
+  e->neutrino_properties = neutrino_properties;
+
   struct chemistry_global_data *chemistry =
       (struct chemistry_global_data *)malloc(
           sizeof(struct chemistry_global_data));
diff --git a/src/engine.h b/src/engine.h
index d0afe37fb209d9926a2d8cc7575434042c4092bf..0de4031568dee18e580ab74a6e668bd9bce15ce5 100644
--- a/src/engine.h
+++ b/src/engine.h
@@ -458,6 +458,9 @@ struct engine {
   /* Properties of the sink model */
   const struct sink_props *sink_properties;
 
+  /* Properties of the neutrino model */
+  const struct neutrino_props *neutrino_properties;
+
   /* Properties of the self-gravity scheme */
   struct gravity_props *gravity_properties;
 
@@ -589,7 +592,8 @@ void engine_init(
     const struct entropy_floor_properties *entropy_floor,
     struct gravity_props *gravity, const struct stars_props *stars,
     const struct black_holes_props *black_holes, const struct sink_props *sinks,
-    struct feedback_props *feedback, struct rt_props *rt, struct pm_mesh *mesh,
+    const struct neutrino_props *neutrinos, struct feedback_props *feedback,
+    struct rt_props *rt, struct pm_mesh *mesh,
     const struct external_potential *potential,
     struct cooling_function_data *cooling_func,
     const struct star_formation *starform,
diff --git a/src/engine_maketasks.c b/src/engine_maketasks.c
index 3fd36a54f703d01a53367ca61f2c147b3ed753f1..6e3ba60a5af82f06786e8dd5dae0004695e7d4ec 100644
--- a/src/engine_maketasks.c
+++ b/src/engine_maketasks.c
@@ -50,6 +50,7 @@
 #include "debug.h"
 #include "error.h"
 #include "feedback.h"
+#include "neutrino_properties.h"
 #include "proxy.h"
 #include "rt_properties.h"
 #include "timers.h"
@@ -1004,6 +1005,13 @@ void engine_make_hierarchical_tasks_common(struct engine *e, struct cell *c) {
       c->kick2 = scheduler_addtask(s, task_type_kick2, task_subtype_none, 0, 0,
                                    c, NULL);
 
+      /* Weighting task for neutrinos after the last kick */
+      if (e->neutrino_properties->use_delta_f) {
+        c->grav.neutrino_weight = scheduler_addtask(
+            s, task_type_neutrino_weight, task_subtype_none, 0, 0, c, NULL);
+        scheduler_addunlock(s, c->kick1, c->grav.neutrino_weight);
+      }
+
 #if defined(WITH_LOGGER)
       struct task *kick2_or_logger;
       if (with_logger) {
diff --git a/src/engine_marktasks.c b/src/engine_marktasks.c
index 0ba9e7dc1b763d258cddd67e97ca014af148d264..ee192fe0f27eeac8bf2b4656476c70f9250ead9a 100644
--- a/src/engine_marktasks.c
+++ b/src/engine_marktasks.c
@@ -1243,6 +1243,13 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
       if (cell_is_active_gravity(t->ci, e)) scheduler_activate(s, t);
     }
 
+    /* Activate the weighting task for neutrinos */
+    else if (t_type == task_type_neutrino_weight) {
+      if (cell_is_active_gravity(t->ci, e)) {
+        scheduler_activate(s, t);
+      }
+    }
+
     /* Kick ? */
     else if (t_type == task_type_kick1 || t_type == task_type_kick2) {
 
diff --git a/src/log.h b/src/log.h
new file mode 100644
index 0000000000000000000000000000000000000000..6bee9643371323ec9984a11af5f7bf3df589f1e5
--- /dev/null
+++ b/src/log.h
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2021 Willem Elbers (whe@willemelbers.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_OPTIMIZED_LOG_H
+#define SWIFT_OPTIMIZED_LOG_H
+
+/* Config parameters. */
+#include "../config.h"
+
+/* Local headers. */
+#include "inline.h"
+
+/* Standard headers */
+#include <math.h>
+#include <stdint.h>
+
+/**
+ * @brief Compute the natural logarithm of a number.
+ *
+ * This function has a maximum absolute error of 5e-3 over the domain
+ * [exp(-32.), exp(32.)] and a maximum relative error of 5e-3 over the two
+ * intervals [exp(-32.), exp(-0.3)] and [exp(0.3), exp(32.)]
+ *
+ * @param x The argument of the log
+ */
+__attribute__((always_inline, const)) INLINE static float optimized_logf(
+    float val) {
+  union {
+    int32_t i;
+    float f;
+  } e;
+  e.f = val;
+
+  /* Isolate the exponent */
+  float log_2 = (float)(((e.i >> 23) & 255) - 128);
+  e.i &= ~(255 << 23);
+  e.i += 127 << 23;
+  /* Approximation based on https://stackoverflow.com/a/28730362 comment */
+  log_2 += ((-0.34484843f) * e.f + 2.02466578f) * e.f - 0.67487759f;
+  /* Return the natural log */
+  return log_2 * 0.693147181f;
+}
+
+/**
+ * @brief Compute the base-10 logarithm of a number.
+ *
+ * This function has a maximum absolute error of 5e-3 over the domain
+ * [exp(-32.), exp(32.)] and a maximum relative error of 5e-3 over the two
+ * intervals [exp(-32.), exp(-0.3)] and [exp(0.3), exp(32.)]
+ *
+ * @param x The argument of the log
+ */
+__attribute__((always_inline, const)) INLINE static float optimized_log10f(
+    float val) {
+  /* Compute the natural log */
+  float log_e = optimized_logf(val);
+  /* Return the base-10 log */
+  return log_e * 0.434294482f;
+}
+
+#endif /* SWIFT_OPTIMIZED_LOG_H */
diff --git a/src/neutrino.h b/src/neutrino.h
new file mode 100644
index 0000000000000000000000000000000000000000..a19261b3a619a62fb1aae8892aaf36e83ca1eebb
--- /dev/null
+++ b/src/neutrino.h
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Coypright (c) 2021 Willem Elbers (whe@willemelbers.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_NEUTRINO_H
+#define SWIFT_NEUTRINO_H
+
+/* Config parameters. */
+#include "../config.h"
+
+/* Select the correct neutrino model */
+#include "./neutrino/Default/neutrino.h"
+
+#endif
diff --git a/src/neutrino/Default/fermi_dirac.c b/src/neutrino/Default/fermi_dirac.c
new file mode 100644
index 0000000000000000000000000000000000000000..dfd8e34d21a4de49124b6b8e8b5785824cc2a00f
--- /dev/null
+++ b/src/neutrino/Default/fermi_dirac.c
@@ -0,0 +1,327 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2021 Willem Elbers (whe@willemelbers.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/>.
+ *
+ ******************************************************************************/
+
+/**
+ *  @file fermi_dirac.c
+ *
+ *  @brief Fast generation of pseudo-random numbers from a Fermi-Dirac
+ *  distribution using numerical inversion (Hormann & Leydold, 2003). The
+ *  spline tables were generated with AnyRNG. For more details, refer
+ *  to https://github.com/wullm/AnyRNG.
+ */
+
+/* This object's header. */
+#include "fermi_dirac.h"
+
+/* Standard headers */
+#include <math.h>
+
+/* Fast optimized logarithm */
+#include "../../log.h"
+
+/* Cubic spline coefficients */
+struct spline {
+  double a0, a1, a2, a3;
+};
+
+/**
+ * @brief Interpolation and search tables for the Fermi-Dirac distribution
+ */
+struct anyrng {
+  /*! Number of intervals on which the interpolation is defined */
+  int intervalN;
+
+  /*! Endpoints of the intervals */
+  double *endpoints;
+
+  /*! Cubic splines of the Fermi-Dirac quantile function */
+  struct spline *splines;
+
+  /*! Length of the look up tables */
+  int tablelen;
+
+  /*! Search table to look up enclosing intervals for small u */
+  int *index_table_a;
+
+  /*! Search table to look up enclosing intervals for intermediate u */
+  int *index_table_b;
+
+  /*! Search table to look up enclosing intervals for large u */
+  int *index_table_c;
+};
+
+static double endpoints[118] = {
+    0.000000e+00, 8.812220e-16, 3.490090e-15, 1.764874e-14, 1.080345e-13,
+    7.475047e-13, 5.544411e-12, 4.267185e-11, 3.346975e-10, 2.649997e-09,
+    2.107211e-08, 7.092068e-08, 1.677779e-07, 5.644860e-07, 1.334411e-06,
+    2.599613e-06, 4.480999e-06, 1.057018e-05, 2.054614e-05, 3.533463e-05,
+    5.584298e-05, 8.296016e-05, 1.175567e-04, 1.604850e-04, 2.125789e-04,
+    2.746542e-04, 4.319210e-04, 6.384483e-04, 9.001106e-04, 1.222496e-03,
+    1.610910e-03, 2.070375e-03, 2.605637e-03, 3.221165e-03, 3.921158e-03,
+    4.709548e-03, 5.590000e-03, 6.565923e-03, 8.816531e-03, 1.148360e-02,
+    1.458549e-02, 1.813688e-02, 2.214891e-02, 2.662937e-02, 3.158292e-02,
+    3.701122e-02, 4.291319e-02, 4.928517e-02, 5.612111e-02, 6.341282e-02,
+    7.115008e-02, 8.791173e-02, 1.062920e-01, 1.261570e-01, 1.473582e-01,
+    1.697373e-01, 1.931305e-01, 2.173724e-01, 2.422994e-01, 2.677516e-01,
+    2.935760e-01, 3.196273e-01, 3.457697e-01, 3.718776e-01, 3.978360e-01,
+    4.235411e-01, 4.488999e-01, 4.738305e-01, 5.221310e-01, 5.679885e-01,
+    6.110917e-01, 6.512521e-01, 6.883826e-01, 7.224780e-01, 7.535962e-01,
+    7.818432e-01, 8.073587e-01, 8.303052e-01, 8.508587e-01, 8.692016e-01,
+    8.855172e-01, 8.999850e-01, 9.127781e-01, 9.240609e-01, 9.339876e-01,
+    9.427015e-01, 9.503348e-01, 9.570084e-01, 9.628322e-01, 9.679056e-01,
+    9.723182e-01, 9.761502e-01, 9.794730e-01, 9.823505e-01, 9.848391e-01,
+    9.869886e-01, 9.888431e-01, 9.904414e-01, 9.918172e-01, 9.930005e-01,
+    9.948897e-01, 9.962793e-01, 9.972980e-01, 9.980425e-01, 9.985850e-01,
+    9.989794e-01, 9.992652e-01, 9.994720e-01, 9.996213e-01, 9.998061e-01,
+    9.999013e-01, 9.999500e-01, 9.999748e-01, 9.999937e-01, 9.999984e-01,
+    9.999996e-01, 9.999999e-01, 1.000000e+00};
+static struct spline splines[118] = {
+    {1.000000e-05, 3.177853e-05, -3.440759e-05, 1.454999e-05},
+    {2.192092e-05, 1.957877e-05, -1.160959e-05, 3.951739e-06},
+    {3.384185e-05, 4.458279e-05, -3.298529e-05, 1.224435e-05},
+    {5.768370e-05, 9.796089e-05, -8.223074e-05, 3.195355e-05},
+    {1.053674e-04, 2.077194e-04, -1.865719e-04, 7.421998e-05},
+    {2.007348e-04, 4.293444e-04, -3.993851e-04, 1.607755e-04},
+    {3.914696e-04, 8.738366e-04, -8.274556e-04, 3.350886e-04},
+    {7.729391e-04, 1.763374e-03, -1.684705e-03, 6.842694e-04},
+    {1.535878e-03, 3.542204e-03, -3.398798e-03, 1.382473e-03},
+    {3.061757e-03, 7.097573e-03, -6.822804e-03, 2.776987e-03},
+    {6.113513e-03, 4.824439e-03, -2.643433e-03, 8.707511e-04},
+    {9.165270e-03, 4.177172e-03, -1.553626e-03, 4.282101e-04},
+    {1.221703e-02, 9.643782e-03, -5.278698e-03, 1.738429e-03},
+    {1.832054e-02, 8.348620e-03, -3.098591e-03, 8.534846e-04},
+    {2.442405e-02, 7.742955e-03, -2.147031e-03, 5.075889e-04},
+    {3.052757e-02, 7.392976e-03, -1.625943e-03, 3.364794e-04},
+    {3.663108e-02, 1.666989e-02, -6.155448e-03, 1.692581e-03},
+    {4.883811e-02, 1.546030e-02, -4.257164e-03, 1.003894e-03},
+    {6.104513e-02, 1.476135e-02, -3.217990e-03, 6.636676e-04},
+    {7.325216e-02, 1.430652e-02, -2.570199e-03, 4.707042e-04},
+    {8.545918e-02, 1.398707e-02, -2.130803e-03, 3.507572e-04},
+    {9.766621e-02, 1.375043e-02, -1.814549e-03, 2.711427e-04},
+    {1.098732e-01, 1.356811e-02, -1.576708e-03, 2.156231e-04},
+    {1.220803e-01, 1.342333e-02, -1.391687e-03, 1.753831e-04},
+    {1.342873e-01, 1.330557e-02, -1.243846e-03, 1.452992e-04},
+    {1.464943e-01, 2.851127e-02, -5.005621e-03, 9.084052e-04},
+    {1.709084e-01, 2.787360e-02, -4.132288e-03, 6.727367e-04},
+    {1.953224e-01, 2.740090e-02, -3.503576e-03, 5.167249e-04},
+    {2.197365e-01, 2.703641e-02, -3.030574e-03, 4.082211e-04},
+    {2.441505e-01, 2.674669e-02, -2.662433e-03, 3.297933e-04},
+    {2.685646e-01, 2.651082e-02, -2.368087e-03, 2.713228e-04},
+    {2.929786e-01, 2.631498e-02, -2.127539e-03, 2.266089e-04},
+    {3.173927e-01, 2.614973e-02, -1.927362e-03, 1.916808e-04},
+    {3.418067e-01, 2.600838e-02, -1.758224e-03, 1.639006e-04},
+    {3.662208e-01, 2.588603e-02, -1.613441e-03, 1.414606e-04},
+    {3.906348e-01, 2.577907e-02, -1.488110e-03, 1.230891e-04},
+    {4.150489e-01, 2.568473e-02, -1.378552e-03, 1.078702e-04},
+    {4.394629e-01, 5.362050e-02, -5.511167e-03, 7.187774e-04},
+    {4.882911e-01, 5.303602e-02, -4.778531e-03, 5.706129e-04},
+    {5.371192e-01, 5.255849e-02, -4.191326e-03, 4.609411e-04},
+    {5.859473e-01, 5.216054e-02, -3.710147e-03, 3.777099e-04},
+    {6.347754e-01, 5.182344e-02, -3.308556e-03, 3.132206e-04},
+    {6.836035e-01, 5.153391e-02, -2.968181e-03, 2.623720e-04},
+    {7.324316e-01, 5.128230e-02, -2.675876e-03, 2.216775e-04},
+    {7.812597e-01, 5.106141e-02, -2.421999e-03, 1.886903e-04},
+    {8.300878e-01, 5.086577e-02, -2.199319e-03, 1.616529e-04},
+    {8.789159e-01, 5.069114e-02, -2.002315e-03, 1.392774e-04},
+    {9.277440e-01, 5.053420e-02, -1.826699e-03, 1.206032e-04},
+    {9.765721e-01, 5.039230e-02, -1.669092e-03, 1.049023e-04},
+    {1.025400e+00, 5.026329e-02, -1.526795e-03, 9.161505e-05},
+    {1.074228e+00, 1.028683e-01, -5.815751e-03, 6.036140e-04},
+    {1.171885e+00, 1.020332e-01, -4.846724e-03, 4.697008e-04},
+    {1.269541e+00, 1.013216e-01, -4.035237e-03, 3.698061e-04},
+    {1.367197e+00, 1.007075e-01, -3.345636e-03, 2.943847e-04},
+    {1.464853e+00, 1.001716e-01, -2.752379e-03, 2.369418e-04},
+    {1.562509e+00, 9.969996e-02, -2.236687e-03, 1.929401e-04},
+    {1.660166e+00, 9.928152e-02, -1.784441e-03, 1.591361e-04},
+    {1.757822e+00, 9.890786e-02, -1.384813e-03, 1.331654e-04},
+    {1.855478e+00, 9.857228e-02, -1.029346e-03, 1.132745e-04},
+    {1.953134e+00, 9.826940e-02, -7.113359e-04, 9.814323e-05},
+    {2.050790e+00, 9.799484e-02, -4.253906e-04, 8.676449e-05},
+    {2.148447e+00, 9.774497e-02, -1.671167e-04, 7.836135e-05},
+    {2.246103e+00, 9.751678e-02, 6.710498e-05, 7.232835e-05},
+    {2.343759e+00, 9.730773e-02, 2.802876e-04, 6.818952e-05},
+    {2.441415e+00, 9.711568e-02, 4.749628e-04, 6.556786e-05},
+    {2.539071e+00, 9.693877e-02, 6.532755e-04, 6.416282e-05},
+    {2.636728e+00, 9.677542e-02, 8.170569e-04, 6.373341e-05},
+    {2.734384e+00, 1.910291e-01, 3.767224e-03, 5.160521e-04},
+    {2.929696e+00, 1.899901e-01, 4.782956e-03, 5.393740e-04},
+    {3.125009e+00, 1.890915e-01, 5.646906e-03, 5.740339e-04},
+    {3.320321e+00, 1.883086e-01, 6.388338e-03, 6.155239e-04},
+    {3.515634e+00, 1.876218e-01, 7.029801e-03, 6.608298e-04},
+    {3.710946e+00, 1.870156e-01, 7.588922e-03, 7.079357e-04},
+    {3.906258e+00, 1.864773e-01, 8.079643e-03, 7.555006e-04},
+    {4.101571e+00, 1.859967e-01, 8.513103e-03, 8.026422e-04},
+    {4.296883e+00, 1.855653e-01, 8.898285e-03, 8.487922e-04},
+    {4.492196e+00, 1.851763e-01, 9.242495e-03, 8.935968e-04},
+    {4.687508e+00, 1.848239e-01, 9.551712e-03, 9.368494e-04},
+    {4.882821e+00, 1.845031e-01, 9.830867e-03, 9.784433e-04},
+    {5.078133e+00, 1.842100e-01, 1.008405e-02, 1.018340e-03},
+    {5.273445e+00, 1.839412e-01, 1.031468e-02, 1.056544e-03},
+    {5.468758e+00, 1.836937e-01, 1.052561e-02, 1.093093e-03},
+    {5.664070e+00, 1.834651e-01, 1.071926e-02, 1.128039e-03},
+    {5.859383e+00, 1.832533e-01, 1.089768e-02, 1.161448e-03},
+    {6.054695e+00, 1.830564e-01, 1.106262e-02, 1.193391e-03},
+    {6.250007e+00, 1.828729e-01, 1.121556e-02, 1.223940e-03},
+    {6.445320e+00, 1.827015e-01, 1.135780e-02, 1.253169e-03},
+    {6.640632e+00, 1.825408e-01, 1.149043e-02, 1.281149e-03},
+    {6.835945e+00, 1.823900e-01, 1.161443e-02, 1.307949e-03},
+    {7.031257e+00, 1.822482e-01, 1.173063e-02, 1.333635e-03},
+    {7.226570e+00, 1.821144e-01, 1.183975e-02, 1.358270e-03},
+    {7.421882e+00, 1.819881e-01, 1.194245e-02, 1.381911e-03},
+    {7.617194e+00, 1.818685e-01, 1.203929e-02, 1.404614e-03},
+    {7.812507e+00, 1.817552e-01, 1.213077e-02, 1.426431e-03},
+    {8.007819e+00, 1.816477e-01, 1.221734e-02, 1.447411e-03},
+    {8.203132e+00, 1.815454e-01, 1.229938e-02, 1.467598e-03},
+    {8.398444e+00, 1.814481e-01, 1.237726e-02, 1.487035e-03},
+    {8.593757e+00, 1.813554e-01, 1.245129e-02, 1.505762e-03},
+    {8.789069e+00, 1.812668e-01, 1.252176e-02, 1.523815e-03},
+    {8.984381e+00, 3.367030e-01, 4.147623e-02, 1.244560e-02},
+    {9.375006e+00, 3.361379e-01, 4.177920e-02, 1.270774e-02},
+    {9.765631e+00, 3.356191e-01, 4.205304e-02, 1.295269e-02},
+    {1.015626e+01, 3.351411e-01, 4.230171e-02, 1.318205e-02},
+    {1.054688e+01, 3.346991e-01, 4.252850e-02, 1.339724e-02},
+    {1.093751e+01, 3.342892e-01, 4.273612e-02, 1.359950e-02},
+    {1.132813e+01, 3.339080e-01, 4.292687e-02, 1.378995e-02},
+    {1.171876e+01, 3.335526e-01, 4.310271e-02, 1.396956e-02},
+    {1.210938e+01, 3.332203e-01, 4.326528e-02, 1.413922e-02},
+    {1.250000e+01, 5.722553e-01, 9.204483e-02, 1.169495e-01},
+    {1.328125e+01, 5.704529e-01, 9.145775e-02, 1.193390e-01},
+    {1.406250e+01, 5.688543e-01, 9.089620e-02, 1.214992e-01},
+    {1.484375e+01, 5.674266e-01, 9.036152e-02, 1.234615e-01},
+    {1.562500e+01, 8.513274e-01, -3.717295e-01, 1.082901e+00},
+    {1.718750e+01, 8.456524e-01, -3.938066e-01, 1.110653e+00},
+    {1.875000e+01, 8.409733e-01, -4.130061e-01, 1.134532e+00},
+    {2.031250e+01, 8.371720e-01, -4.305845e-01, 1.155912e+00},
+    {2.187500e+01, 8.346323e-01, -4.503617e-01, 1.178229e+00},
+    {2.343750e+01, 8.359434e-01, -4.895204e-01, 1.216076e+00}};
+static int index_table_a[165] = {
+    0,  1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2,
+    2,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  4,  4,
+    4,  4,  4,  4,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  6,  6,  6,
+    6,  6,  6,  6,  6,  6,  6,  6,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,
+    8,  8,  8,  8,  8,  8,  8,  8,  8,  8,  8,  8,  9,  9,  9,  9,  9,  9,  9,
+    10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13,
+    13, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19,
+    19, 20, 20, 21, 21, 22, 23, 24, 24, 24, 25, 25, 26, 26, 27, 27, 28, 29, 29,
+    30, 31, 32, 33, 34, 35, 36, 36, 37, 38, 38, 39, 40};
+static int index_table_b[165] = {
+    41, 42, 43, 44, 45, 46, 47, 48, 48, 49, 49, 50, 50, 50, 50, 51, 51, 51, 52,
+    52, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 56, 56, 56, 56,
+    57, 57, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59, 59, 60, 60, 60, 60, 61,
+    61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 63, 63, 64, 64, 64, 64, 65, 65,
+    65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67,
+    68, 68, 68, 68, 68, 68, 68, 69, 69, 69, 69, 69, 69, 69, 70, 70, 70, 70, 70,
+    70, 70, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 73, 73, 73, 73, 73, 74,
+    74, 74, 74, 75, 75, 75, 75, 76, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79,
+    80, 80, 80, 81, 81, 82, 83, 83, 84, 85, 86, 87, 88};
+static int index_table_c[165] = {
+    89,  90,  90,  91,  92,  92,  93,  93,  94,  94,  95,  96,  96,  97,  97,
+    98,  98,  98,  99,  99,  99,  99,  100, 100, 100, 101, 101, 101, 101, 102,
+    102, 102, 102, 103, 103, 103, 104, 104, 104, 104, 105, 105, 105, 105, 106,
+    106, 106, 107, 107, 107, 107, 107, 107, 107, 107, 108, 108, 108, 108, 108,
+    108, 108, 109, 109, 109, 109, 109, 109, 109, 109, 110, 110, 110, 110, 110,
+    110, 110, 110, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111,
+    111, 111, 111, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112,
+    112, 112, 112, 112, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113, 113,
+    113, 113, 113, 113, 113, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114,
+    114, 114, 114, 114, 114, 114, 115, 115, 115, 115, 115, 115, 115, 115, 115,
+    115, 115, 115, 115, 115, 115, 115, 115, 115, 115, 115, 115, 115, 115, 115};
+static struct anyrng anyrng = {118,           endpoints,     splines,      165,
+                               index_table_a, index_table_b, index_table_c};
+
+/**
+ * @brief Transform a 64-bit unsigned integer seed into a (dimensionless)
+ * Fermi-Dirac momentum (units of kb*T), using cubic spline interpolation of
+ * the quantile function.
+ *
+ * @param seed Random seed to be transformed
+ */
+double neutrino_seed_to_fermi_dirac(uint64_t seed) {
+  /* Scramble the bits with splitmix64 */
+  uint64_t A = seed;
+  A = A + 0x9E3779B97f4A7C15;
+  A = (A ^ (A >> 30)) * 0xBF58476D1CE4E5B9;
+  A = (A ^ (A >> 27)) * 0x94D049BB133111EB;
+  A = A ^ (A >> 31);
+
+  /* Map the integer to the unit open interval (0, 1) */
+  const double u = ((double)A + 0.5) / ((double)UINT64_MAX + 1);
+
+  /* Use the hash table to find an enclosing interval */
+  const int tablen = anyrng.tablelen;
+  int index, interval;
+  if (u < 0.025) {
+    index = (int)((optimized_log10f(u) + 14.5229) / 12.9208 * tablen);
+    interval = anyrng.index_table_a[index < tablen ? index : tablen - 1] + 1;
+  } else if (u < 0.975) {
+    index = (int)((u - 0.025) / 0.95 * tablen);
+    interval = anyrng.index_table_b[index < tablen ? index : tablen - 1] + 1;
+  } else {
+    index = (int)(-(optimized_log10f(1 - u) + 1.60206) / 6.39794 * tablen);
+    interval = anyrng.index_table_c[index < tablen ? index : tablen - 1] + 1;
+  }
+
+  /* Retrieve the endpoints and cubic spline coefficients of this interval */
+  const double Fl = anyrng.endpoints[interval];
+  const double Fr = anyrng.endpoints[interval + 1];
+  struct spline *iv = &anyrng.splines[interval];
+
+  /* Evaluate F^-1(u) using the Hermite approximation of F in this interval */
+  const double u_tilde = (u - Fl) / (Fr - Fl);
+  const double u_tilde2 = u_tilde * u_tilde;
+  const double u_tilde3 = u_tilde2 * u_tilde;
+  return iv->a0 + iv->a1 * u_tilde + iv->a2 * u_tilde2 + iv->a3 * u_tilde3;
+}
+
+/**
+ * @brief Transform a 64-bit unsigned integer seed into a point on the sphere.
+ *
+ * @param seed Random seed to be transformed
+ */
+void neutrino_seed_to_direction(uint64_t seed, double n[3]) {
+  /* Skip the first draw, which is used for the magnitude */
+  seed += 0x9E3779B97f4A7C15;
+
+  /* Draw three numbers for the orientation of the momentum vector */
+  double u[3];
+  for (int i = 0; i < 3; i++) {
+    /* Scramble the bits with splitmix64 */
+    uint64_t N = seed += 0x9E3779B97f4A7C15;
+    N = (N ^ (N >> 30)) * 0xBF58476D1CE4E5B9;
+    N = (N ^ (N >> 27)) * 0x94D049BB133111EB;
+    N = N ^ (N >> 31);
+    /* Map the integer to the unit open interval (0,1) */
+    u[i] = ((double)N + 0.5) / ((double)UINT64_MAX + 1);
+  }
+
+  /* Map to three independent Gaussians with Box-Mueller */
+  const double sqrt_log_u = sqrt(-2 * log(u[0]));
+  n[0] = sqrt_log_u * cos(2 * M_PI * u[1]);
+  n[1] = sqrt_log_u * sin(2 * M_PI * u[1]);
+  n[2] = sqrt_log_u * cos(2 * M_PI * u[2]);
+
+  /* Normalize the vector */
+  const double nmag = sqrtf(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
+  if (nmag > 0) {
+    const double inv_nmag = 1.0 / nmag;
+    n[0] *= inv_nmag;
+    n[1] *= inv_nmag;
+    n[2] *= inv_nmag;
+  }
+}
diff --git a/src/neutrino/Default/fermi_dirac.h b/src/neutrino/Default/fermi_dirac.h
new file mode 100644
index 0000000000000000000000000000000000000000..8c47a12568e6b5ea4e9913e9ebbf38ace299a485
--- /dev/null
+++ b/src/neutrino/Default/fermi_dirac.h
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2021 Willem Elbers (whe@willemelbers.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_DEFAULT_FERMI_DIRAC_H
+#define SWIFT_DEFAULT_FERMI_DIRAC_H
+
+/* Standard headers */
+#include <math.h>
+#include <stdint.h>
+
+/* Faster exponential */
+#include "exp.h"
+
+/**
+ * @brief Calculate the neutrino density at the particle's location in phase
+ * space, according to the 0th order background model: f_0(x,p,t).
+ *
+ * @param z Argument of the FD function: z = p/kbT
+ */
+INLINE static float fermi_dirac_density(float z) {
+  return 1.f / (optimized_expf(z) + 1.f);
+}
+
+/**
+ * @brief Return a microscopic neutrino mass in eV based on the particle seed.
+ * We simply cycle through the masses defined in the cosmology. TODO: consider
+ * multiplicities.
+ *
+ * @param N_nu Number of distinct neutrino masses
+ * @param m_eV_array Array of masses in eV
+ * @param seed The seed of the neutrino particle
+ */
+INLINE static double neutrino_seed_to_mass(const int N_nu,
+                                           const double *m_eV_array,
+                                           uint64_t seed) {
+  return m_eV_array[(int)(seed % N_nu)];
+}
+
+double neutrino_seed_to_fermi_dirac(uint64_t seed);
+void neutrino_seed_to_direction(uint64_t seed, double n[3]);
+
+#endif /* SWIFT_DEFAULT_FERMI_DIRAC_H */
diff --git a/src/neutrino/neutrino.h b/src/neutrino/Default/neutrino.h
similarity index 58%
rename from src/neutrino/neutrino.h
rename to src/neutrino/Default/neutrino.h
index 13e7bd1d1a05ab2cf88a46b679a63a0c8c28bde4..a84688d42b0115f2c8e0bf671ef63cd4561da2ff 100644
--- a/src/neutrino/neutrino.h
+++ b/src/neutrino/Default/neutrino.h
@@ -19,6 +19,15 @@
 #ifndef SWIFT_DEFAULT_NEUTRINO_H
 #define SWIFT_DEFAULT_NEUTRINO_H
 
+/* Config parameters. */
+#include "../config.h"
+
+/* Local includes */
+#include "../../engine.h"
+#include "fermi_dirac.h"
+#include "neutrino_properties.h"
+#include "relativity.h"
+
 /* Riemann function zeta(3) */
 #define M_ZETA_3 1.2020569031595942853997
 
@@ -63,4 +72,53 @@ INLINE static double neutrino_mass_factor(
   return mass_factor_eV;
 }
 
+/**
+ * @brief Initialises the neutrino g-particles for the first time. This is done
+ * in addition to gravity_first_init_gpart().
+ *
+ * This function is called only once just after the ICs have been read in
+ * and after IDs have been remapped (if used) by space_remap_ids().
+ *
+ * @param gp The particle to act upon
+ * @param engine The engine of the run
+ */
+__attribute__((always_inline)) INLINE static void gravity_first_init_neutrino(
+    struct gpart *gp, const struct engine *e) {
+
+  /* Do we need to do anything? */
+  if (!e->neutrino_properties->generate_ics) return;
+
+  /* Retrieve physical and cosmological constants */
+  const double c_vel = e->physical_constants->const_speed_light_c;
+  const double *m_eV_array = e->cosmology->M_nu_eV;
+  const int N_nu = e->cosmology->N_nu;
+  const double T_eV = e->cosmology->T_nu_0_eV;
+  const double inv_fac = c_vel * T_eV;
+  const long long neutrino_seed = e->neutrino_properties->neutrino_seed;
+
+  /* Use a particle id dependent seed */
+  const long long seed = gp->id_or_neg_offset + neutrino_seed;
+
+  /* Compute the initial dimensionless momentum from the seed */
+  const double pi = neutrino_seed_to_fermi_dirac(seed);
+
+  /* The neutrino mass (we cycle based on the neutrino seed) */
+  const double m_eV = neutrino_seed_to_mass(N_nu, m_eV_array, seed);
+
+  /* Compute the initial direction of the momentum vector from the seed */
+  double n[3];
+  neutrino_seed_to_direction(seed, n);
+
+  /* Set the initial velocity */
+  const double vi = pi * inv_fac / m_eV;
+  gp->v_full[0] = n[0] * vi;
+  gp->v_full[1] = n[1] * vi;
+  gp->v_full[2] = n[2] * vi;
+
+  /* If running with the delta-f method, set the weight to (almost) zero */
+  if (e->neutrino_properties->use_delta_f) {
+    gp->mass = FLT_MIN;
+  }
+}
+
 #endif /* SWIFT_DEFAULT_NEUTRINO_H */
diff --git a/src/neutrino/Default/neutrino_io.h b/src/neutrino/Default/neutrino_io.h
new file mode 100644
index 0000000000000000000000000000000000000000..b1ee5a7490bbde79b2db06446c9d70b09e9d320f
--- /dev/null
+++ b/src/neutrino/Default/neutrino_io.h
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Coypright (c) 2021 Willem Elbers (willem.h.elbers@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_DEFAULT_NEUTRINO_IO_H
+#define SWIFT_DEFAULT_NEUTRINO_IO_H
+
+#include "../../gravity.h"
+
+/* Local includes */
+#include "fermi_dirac.h"
+#include "neutrino.h"
+#include "neutrino_properties.h"
+
+/**
+ * @brief Recover and store the initial Fermi-Dirac speed, vi, for a neutrino
+ * particle. These are used in the weight calculation of the delta-f method.
+ *
+ * @param e The engine of the run
+ * @param gp The neutrino gpart in question
+ * @param ret Output
+ */
+INLINE static void convert_gpart_vi(const struct engine* e,
+                                    const struct gpart* gp, float* ret) {
+
+  /* When we are running with the delta-f method, resample the momentum */
+  if (e->neutrino_properties->use_delta_f) {
+    /* Retrieve physical constants, including the neutrino mass array */
+    const double a_scale = e->cosmology->a;
+    const double N_nu = e->cosmology->N_nu;
+    const double* m_eV_array = e->cosmology->M_nu_eV;
+    const double c_vel = e->physical_constants->const_speed_light_c;
+    const double T_eV = e->cosmology->T_nu_0_eV;
+    const double a_fac = c_vel * T_eV / a_scale;
+
+    /* Use a particle id dependent seed (sum of global seed and ID) */
+    const long long neutrino_seed = e->neutrino_properties->neutrino_seed;
+    const long long seed = gp->id_or_neg_offset + neutrino_seed;
+
+    /* Convert momentum in electronvolts to speed in internal units */
+    double pi_eV = neutrino_seed_to_fermi_dirac(seed);
+    double m_eV = neutrino_seed_to_mass(N_nu, m_eV_array, seed);
+    double vi = pi_eV / m_eV * a_fac;  // scales like peculiar velocity
+
+    ret[0] = vi;
+  } else {
+    /* We don't know what the initial momentum was and we don't need it */
+    ret[0] = 0.f;
+  }
+}
+
+/**
+ * @brief Recover and store the microscopic mass of a neutrino particle
+ *
+ * @param e The engine of the run
+ * @param gp The neutrino gpart in question
+ * @param ret Output
+ */
+INLINE static void convert_gpart_mnu(const struct engine* e,
+                                     const struct gpart* gp, double* ret) {
+
+  /* Physical constants */
+  const double c = e->physical_constants->const_speed_light_c;
+  const double eV = e->physical_constants->const_electron_volt;
+  const double eV_mass = eV / (c * c);
+
+  double micro_mass;
+
+  /* When we are running with the delta-f method, resample the mass */
+  if (e->neutrino_properties->use_delta_f) {
+
+    /* Use a particle id dependent seed (sum of global seed and ID) */
+    const long long neutrino_seed = e->neutrino_properties->neutrino_seed;
+    const long long seed = gp->id_or_neg_offset + neutrino_seed;
+
+    /* Fetch neutrino masses defined in the cosmology */
+    const int N_nu = e->cosmology->N_nu;
+    const double* m_eV_array = e->cosmology->M_nu_eV;
+
+    micro_mass = neutrino_seed_to_mass(N_nu, m_eV_array, seed);  // eV
+  } else {
+    /* Otherwise, simply use the mass implied by the conversion factor */
+    const double mass_factor = e->neutrino_mass_conversion_factor;
+
+    micro_mass = gp->mass * mass_factor;  // eV
+  }
+
+  /* Convert units and store the answer */
+  ret[0] = micro_mass * eV_mass;
+}
+
+/**
+ * @brief Specifies which particle fields to write to a dataset
+ *
+ * @param gparts The particle array.
+ * @param list The list of i/o properties to write.
+ *
+ * @return Returns the number of fields to write.
+ */
+__attribute__((always_inline)) INLINE static int neutrino_write_particles(
+    const struct gpart* gparts, struct io_props* list) {
+
+  list[0] = io_make_output_field_convert_gpart(
+      "SampledSpeeds", FLOAT, 1, UNIT_CONV_SPEED, 0.f, gparts, convert_gpart_vi,
+      "Initial Fermi-Dirac speed sampled at infinity. This is a * |dx/dt| "
+      "where x is the co-moving position of the particles.");
+
+  list[1] = io_make_output_field_convert_gpart(
+      "MicroscopicMasses", DOUBLE, 1, UNIT_CONV_MASS, 0.f, gparts,
+      convert_gpart_mnu, "Microscopic masses of individual neutrino particles");
+
+  return 2;
+}
+
+#endif /* SWIFT_DEFAULT_NEUTRINO_IO_H */
diff --git a/src/neutrino/Default/neutrino_properties.h b/src/neutrino/Default/neutrino_properties.h
new file mode 100644
index 0000000000000000000000000000000000000000..8dbc4cb568086fea6e8dc6210ff4a276e2fb4416
--- /dev/null
+++ b/src/neutrino/Default/neutrino_properties.h
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Coypright (c) 2021 Willem Elbers (willem.h.elbers@durham.ac.uk)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_DEFAULT_NEUTRINO_PROPERTIES_H
+#define SWIFT_DEFAULT_NEUTRINO_PROPERTIES_H
+
+#include "../../restart.h"
+
+/**
+ * @brief Properties of neutrinos
+ */
+struct neutrino_props {
+
+  /* Whether to run with the delta-f method for neutrino weighting */
+  char use_delta_f;
+
+  /* Whether to generate random neutrino velocities in the initial conditions */
+  char generate_ics;
+
+  /* Random seed for the neutrino weighting task */
+  long long neutrino_seed;
+};
+
+/**
+ * @brief Initialise the neutrino properties from the parameter file.
+ *
+ * @param np The #neutrino_props.
+ * @param phys_const The physical constants in the internal unit system.
+ * @param us The internal unit system.
+ * @param params The parsed parameters.
+ * @param cosmo The cosmological model.
+ */
+INLINE static void neutrino_props_init(struct neutrino_props *np,
+                                       const struct phys_const *phys_const,
+                                       const struct unit_system *us,
+                                       struct swift_params *params,
+                                       const struct cosmology *cosmo) {
+
+  np->use_delta_f = parser_get_param_int(params, "Neutrino:use_delta_f");
+  np->generate_ics = parser_get_param_int(params, "Neutrino:generate_ics");
+  np->neutrino_seed =
+      parser_get_opt_param_longlong(params, "Neutrino:neutrino_seed", 0);
+}
+
+/**
+ * @brief Write a neutrino_props struct to the given FILE as a stream of
+ * bytes.
+ *
+ * @param props the neutrino properties struct
+ * @param stream the file stream
+ */
+INLINE static void neutrino_struct_dump(const struct neutrino_props *props,
+                                        FILE *stream) {
+
+  restart_write_blocks((void *)props, sizeof(struct neutrino_props), 1, stream,
+                       "neutrino props", "Neutrino props");
+}
+
+/**
+ * @brief Restore a neutrino_props struct from the given FILE as a stream of
+ * bytes.
+ *
+ * @param props the neutrino properties struct
+ * @param stream the file stream
+ */
+INLINE static void neutrino_struct_restore(const struct neutrino_props *props,
+                                           FILE *stream) {
+  restart_read_blocks((void *)props, sizeof(struct neutrino_props), 1, stream,
+                      NULL, "Neutrino props");
+}
+
+#endif /* SWIFT_DEFAULT_NEUTRINO_PROPERTIES_H */
diff --git a/src/neutrino/relativity.h b/src/neutrino/Default/relativity.h
similarity index 100%
rename from src/neutrino/relativity.h
rename to src/neutrino/Default/relativity.h
diff --git a/src/neutrino_io.h b/src/neutrino_io.h
new file mode 100644
index 0000000000000000000000000000000000000000..f23d169dc5449ce8423f80f8a037ac0a4a2b1fc4
--- /dev/null
+++ b/src/neutrino_io.h
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2021 Willem Elbers (whe@willemelbers.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_NEUTRINO_IO_H
+#define SWIFT_NEUTRINO_IO_H
+
+/* Config parameters. */
+#include "../config.h"
+
+/* Select the correct neutrino model */
+#include "./neutrino/Default/neutrino_io.h"
+
+#endif /* SWIFT_NEUTRINO_PROPERTIES_H */
diff --git a/src/neutrino_properties.h b/src/neutrino_properties.h
new file mode 100644
index 0000000000000000000000000000000000000000..756d4e0c6c1a3bebb3e1849c950b5ab20aca76a1
--- /dev/null
+++ b/src/neutrino_properties.h
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2021 Willem Elbers (whe@willemelbers.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_NEUTRINO_PROPERTIES_H
+#define SWIFT_NEUTRINO_PROPERTIES_H
+
+/* Config parameters. */
+#include "../config.h"
+
+/* Select the correct neutrino model */
+#include "./neutrino/Default/neutrino_properties.h"
+
+#endif /* SWIFT_NEUTRINO_PROPERTIES_H */
diff --git a/src/parallel_io.c b/src/parallel_io.c
index 07a4926f021d797edb2a8c4bf98dfa98c59bc328..d9d8a5c7cc0aa2f7383361d78d39704783552d7c 100644
--- a/src/parallel_io.c
+++ b/src/parallel_io.c
@@ -1299,11 +1299,15 @@ void prepare_file(struct engine* e, const char* fileName,
 
       case swift_type_dark_matter:
       case swift_type_dark_matter_background:
-      case swift_type_neutrino:
         io_select_dm_fields(NULL, NULL, with_fof, with_stf, e, &num_fields,
                             list);
         break;
 
+      case swift_type_neutrino:
+        io_select_neutrino_fields(NULL, NULL, with_fof, with_stf, e,
+                                  &num_fields, list);
+        break;
+
       case swift_type_sink:
         io_select_sink_fields(NULL, with_cosmology, with_fof, with_stf, e,
                               &num_fields, list);
@@ -1773,8 +1777,8 @@ void write_output_parallel(struct engine* e,
             gpart_group_data_written, Ntot, Ndm_neutrino, with_stf);
 
         /* Select the fields to write */
-        io_select_dm_fields(gparts_written, gpart_group_data_written, with_fof,
-                            with_stf, e, &num_fields, list);
+        io_select_neutrino_fields(gparts_written, gpart_group_data_written,
+                                  with_fof, with_stf, e, &num_fields, list);
 
       } break;
 
diff --git a/src/runner.h b/src/runner.h
index 008ea75e6790fbed32567378fa020620706b6f38..8c71570d9c2e131e3cea2706b7941bab5c2207e4 100644
--- a/src/runner.h
+++ b/src/runner.h
@@ -145,6 +145,7 @@ void runner_do_recv_spart(struct runner *r, struct cell *c, int clear_sorts,
                           int timer);
 void runner_do_recv_bpart(struct runner *r, struct cell *c, int clear_sorts,
                           int timer);
+void runner_do_neutrino_weighting(struct runner *r, struct cell *c, int timer);
 void *runner_main(void *data);
 
 #endif /* SWIFT_RUNNER_H */
diff --git a/src/runner_main.c b/src/runner_main.c
index 2cef97f9b25032834bad85c3d64de4e075599563..988e42b69f29f5b09236d8d32f2b9854b524b3a0 100644
--- a/src/runner_main.c
+++ b/src/runner_main.c
@@ -610,6 +610,9 @@ void *runner_main(void *data) {
         case task_type_fof_pair:
           runner_do_fof_pair(r, t->ci, t->cj, 1);
           break;
+        case task_type_neutrino_weight:
+          runner_do_neutrino_weighting(r, ci, 1);
+          break;
         case task_type_rt_ghost1:
           runner_do_rt_ghost1(r, t->ci, 1);
           break;
diff --git a/src/runner_neutrino.c b/src/runner_neutrino.c
new file mode 100644
index 0000000000000000000000000000000000000000..ba380e71875680e87059149b9ddbc90279deb235
--- /dev/null
+++ b/src/runner_neutrino.c
@@ -0,0 +1,125 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2020 Willem Elbers (whe@willemelbers.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/>.
+ *
+ ******************************************************************************/
+
+/* Config parameters. */
+#include "../config.h"
+
+/* This object's header. */
+#include "runner.h"
+
+/* Phase space density functions needed */
+#include "neutrino.h"
+#include "neutrino_properties.h"
+
+/* Local headers. */
+#include "active.h"
+#include "cell.h"
+#include "engine.h"
+#include "timers.h"
+
+/* Compute the dimensionless neutrino momentum (units of kb*T).
+ *
+ * @param v The internal 3-velocity
+ * @param m_eV The neutrino mass in electron-volts
+ * @param fac Conversion factor = 1. / (speed_of_light * T_nu_eV)
+ */
+INLINE static double neutrino_momentum(float v[3], double m_eV, double fac) {
+
+  float v2 = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
+  float vmag = sqrtf(v2);
+  double p = vmag * fac * m_eV;
+  return p;
+}
+
+/**
+ * @brief Weight the active neutrino particles in a cell using the delta-f
+ * method.
+ *
+ * @param r The runner thread.
+ * @param c The cell.
+ * @param timer Are we timing this ?
+ */
+void runner_do_neutrino_weighting(struct runner *r, struct cell *c, int timer) {
+
+  const struct engine *e = r->e;
+  const int with_cosmology = (e->policy & engine_policy_cosmology);
+  struct gpart *restrict gparts = c->grav.parts;
+  const int gcount = c->grav.count;
+
+  TIMER_TIC;
+
+  /* Anything to do here? */
+  if (c->grav.count == 0) return;
+  if (!cell_is_starting_gravity(c, e) && !cell_is_active_gravity(c, e)) return;
+  if (!with_cosmology)
+    error("Phase space weighting without cosmology not implemented.");
+
+  /* Retrieve physical and cosmological constants */
+  const double c_vel = e->physical_constants->const_speed_light_c;
+  const double *m_eV_array = e->cosmology->M_nu_eV;
+  const int N_nu = e->cosmology->N_nu;
+  const double T_eV = e->cosmology->T_nu_0_eV;
+  const double fac = 1.0 / (c_vel * T_eV);
+  const double inv_mass_factor = 1. / e->neutrino_mass_conversion_factor;
+  const long long neutrino_seed = e->neutrino_properties->neutrino_seed;
+
+  /* Recurse? */
+  if (c->split) {
+    for (int k = 0; k < 8; k++)
+      if (c->progeny[k] != NULL)
+        runner_do_neutrino_weighting(r, c->progeny[k], 0);
+  } else {
+    /* Loop over the gparts in this cell. */
+    for (int k = 0; k < gcount; k++) {
+      /* Get a handle on the part. */
+      struct gpart *restrict gp = &gparts[k];
+
+      /* Only act on neutrinos */
+      if (gp->type != swift_type_neutrino) continue;
+
+      /* Use a particle id dependent seed */
+      const long long seed = gp->id_or_neg_offset + neutrino_seed;
+
+      /* Compute the initial dimensionless momentum from the seed */
+      const double pi = neutrino_seed_to_fermi_dirac(seed);
+
+      /* The neutrino mass (we cycle based on the neutrino seed) */
+      const double m_eV = neutrino_seed_to_mass(N_nu, m_eV_array, seed);
+      const double mass = m_eV * inv_mass_factor;
+
+      /* Compute the current dimensionless momentum */
+      double p = neutrino_momentum(gp->v_full, m_eV, fac);
+
+      /* Compute the initial and current background phase-space density */
+      double fi = fermi_dirac_density(pi);
+      double f = fermi_dirac_density(p);
+      double weight = 1.0 - f / fi;
+
+      /* Set the statistically weighted mass */
+      gp->mass = mass * weight;
+
+      /* Prevent degeneracies */
+      if (gp->mass == 0.) {
+        gp->mass = FLT_MIN;
+      }
+    }
+  }
+
+  if (timer) TIMER_TOC(timer_weight);
+}
diff --git a/src/serial_io.c b/src/serial_io.c
index 015d0e41558b25a5e1079fc2e01acaba9aa0d344..0e792f2a012a60303035e1190e7d01ff8467f47b 100644
--- a/src/serial_io.c
+++ b/src/serial_io.c
@@ -1410,8 +1410,8 @@ void write_output_serial(struct engine* e,
                 gpart_group_data_written, Ntot, Ndm_neutrino, with_stf);
 
             /* Select the fields to write */
-            io_select_dm_fields(gparts_written, gpart_group_data_written,
-                                with_fof, with_stf, e, &num_fields, list);
+            io_select_neutrino_fields(gparts_written, gpart_group_data_written,
+                                      with_fof, with_stf, e, &num_fields, list);
 
           } break;
 
diff --git a/src/single_io.c b/src/single_io.c
index 5deae3e179908ae239ca19a408f39710add8fd54..39377033a1ff24ec67fb5ac3180a241a5cff8e03 100644
--- a/src/single_io.c
+++ b/src/single_io.c
@@ -1189,8 +1189,8 @@ void write_output_single(struct engine* e,
             gpart_group_data_written, Ntot, Ndm_neutrino, with_stf);
 
         /* Select the fields to write */
-        io_select_dm_fields(gparts_written, gpart_group_data_written, with_fof,
-                            with_stf, e, &num_fields, list);
+        io_select_neutrino_fields(gparts_written, gpart_group_data_written,
+                                  with_fof, with_stf, e, &num_fields, list);
 
       } break;
 
diff --git a/src/space_first_init.c b/src/space_first_init.c
index 915b3646244a31b838588beb4f155ebfee5b3792..2af63445d47217acdf4b744dc6c23a77150b9351 100644
--- a/src/space_first_init.c
+++ b/src/space_first_init.c
@@ -30,6 +30,7 @@
 #include "chemistry.h"
 #include "engine.h"
 #include "gravity.h"
+#include "neutrino.h"
 #include "particle_splitting.h"
 #include "pressure_floor.h"
 #include "rt.h"
@@ -206,6 +207,9 @@ void space_first_init_gparts_mapper(void *restrict map_data, int count,
 
     gravity_first_init_gpart(&gp[k], grav_props);
 
+    if (gp[k].type == swift_type_neutrino)
+      gravity_first_init_neutrino(&gp[k], s->e);
+
 #ifdef WITH_LOGGER
     logger_part_data_init(&gp[k].logger_data);
 #endif
diff --git a/src/swift.h b/src/swift.h
index 4018c9d22a9e6b9e3de971b9cf54ed3391dadd20..06741a5a96fba8191643b9cd39bc620934bc1821 100644
--- a/src/swift.h
+++ b/src/swift.h
@@ -60,6 +60,7 @@
 #include "minmax.h"
 #include "mpiuse.h"
 #include "multipole.h"
+#include "neutrino_properties.h"
 #include "output_list.h"
 #include "output_options.h"
 #include "parallel_io.h"
diff --git a/src/task.c b/src/task.c
index edf398ff2d9556f910539da7c6a477f333ab612d..44854e5ff2ad47e05839b36a08ed25975f095cc3 100644
--- a/src/task.c
+++ b/src/task.c
@@ -107,6 +107,7 @@ const char *taskID_names[task_type_count] = {
     "bh_swallow_ghost3",
     "fof_self",
     "fof_pair",
+    "neutrino_weight",
     "sink_in",
     "sink_ghost",
     "sink_out",
@@ -169,7 +170,7 @@ const char *task_category_names[task_category_count] = {
     "black holes", "cooling", "star formation",
     "limiter",     "sync",    "time integration",
     "mpi",         "fof",     "others",
-    "sink",        "RT"};
+    "neutrino",    "sink",    "RT"};
 
 #ifdef WITH_MPI
 /* MPI communicators for the subtypes. */
@@ -1764,6 +1765,9 @@ enum task_categories task_get_category(const struct task *t) {
     case task_type_rt_out:
       return task_category_rt;
 
+    case task_type_neutrino_weight:
+      return task_category_neutrino;
+
     case task_type_self:
     case task_type_pair:
     case task_type_sub_self:
diff --git a/src/task.h b/src/task.h
index 5c6f15f67c7a154128575fcb16a4717bd0234557..96a218a87d34f928c2e58eb71ad85e6cf6ea4b10 100644
--- a/src/task.h
+++ b/src/task.h
@@ -100,6 +100,7 @@ enum task_types {
   task_type_bh_swallow_ghost3, /* Implicit */
   task_type_fof_self,
   task_type_fof_pair,
+  task_type_neutrino_weight,
   task_type_sink_in,    /* Implicit */
   task_type_sink_ghost, /* Implicit */
   task_type_sink_out,   /* Implicit */
@@ -194,6 +195,7 @@ enum task_categories {
   task_category_mpi,
   task_category_fof,
   task_category_others,
+  task_category_neutrino,
   task_category_sink,
   task_category_rt,
   task_category_count
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 559ea106868673dcc3b6d51bb8e0a1a829407c57..8f72493890ffd7d0eda54dc9ebc0144b1aa23526 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -30,7 +30,8 @@ TESTS = testGreetings testMaths testReading.sh testKernel testKernelLongGrav \
 	testPotentialPair testEOS testUtilities testSelectOutput.sh \
 	testCbrt testCosmology testOutputList testFormat.sh \
 	test27cellsStars.sh test27cellsStarsPerturbed.sh testHydroMPIrules \
-        testAtomic testGravitySpeed testNeutrinoCosmology
+        testAtomic testGravitySpeed testNeutrinoCosmology testNeutrinoFermiDirac \
+	testLog
 
 # List of test programs to compile
 check_PROGRAMS = testGreetings testReading testTimeIntegration testKernelLongGrav \
@@ -43,7 +44,8 @@ check_PROGRAMS = testGreetings testReading testTimeIntegration testKernelLongGra
 		 testGravityDerivatives testPotentialSelf testPotentialPair testEOS testUtilities \
 		 testSelectOutput testCbrt testCosmology testOutputList test27cellsStars \
 		 test27cellsStars_subset testCooling testComovingCooling testFeedback testHashmap \
-                 testAtomic testHydroMPIrules testGravitySpeed testNeutrinoCosmology
+                 testAtomic testHydroMPIrules testGravitySpeed testNeutrinoCosmology \
+		 testNeutrinoFermiDirac testLog
 
 # Rebuild tests when SWIFT is updated.
 $(check_PROGRAMS): ../src/.libs/libswiftsim.a
@@ -72,6 +74,10 @@ testSymmetry_SOURCES = testSymmetry.c
 # Added because of issues using memcmp on clang 4.x
 testSymmetry_CFLAGS = $(AM_CFLAGS) -fno-builtin-memcmp
 
+testNeutrinoCosmology_SOURCES = testNeutrinoCosmology.c
+
+testNeutrinoFermiDirac_SOURCES = testNeutrinoFermiDirac.c
+
 testTimeIntegration_SOURCES = testTimeIntegration.c
 
 testActivePair_SOURCES = testActivePair.c
@@ -148,6 +154,8 @@ testFeedback_SOURCES = testFeedback.c
 
 testHashmap_SOURCES = testHashmap.c
 
+testLog_SOURCES = testLog.c
+
 testHydroMPIrules = testHydroMPIrules.c
 
 # Files necessary for distribution
diff --git a/tests/testLog.c b/tests/testLog.c
new file mode 100644
index 0000000000000000000000000000000000000000..61544e93b76c8383d5c07df6df9ba8a8921c73fb
--- /dev/null
+++ b/tests/testLog.c
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (C) 2021 Willem Elbers (whe@willemelbers.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#include "../config.h"
+#include "log.h"
+#include "swift.h"
+
+/* Standard includes */
+#include <fenv.h>
+#include <math.h>
+
+/**
+ * @brief Check that a and b are consistent (up to some relative error)
+ *
+ * @param a First value
+ * @param b Second value
+ * @param tol Relative tolerance
+ * @param x Value for which we tested the function (for error messages)
+ */
+void check_value(double a, double b, const double tol, const double x) {
+  if (fabs(a - b) / fabs(a + b) > tol)
+    error("Values are inconsistent: %12.15e %12.15e rel=%e (for x=%e).", a, b,
+          fabs(a - b) / fabs(a + b), x);
+}
+
+/**
+ * @brief Check that a and b are consistent (up to some absolute error)
+ *
+ * @param a First value
+ * @param b Second value
+ * @param tol Relative tolerance
+ * @param x Value for which we tested the function (for error messages)
+ */
+void check_value_abs(double a, double b, const double tol, const double x) {
+  if (fabs(a - b) > tol)
+    error("Values are inconsistent: %12.15e %12.15e abs=%e (for x=%e).", a, b,
+          fabs(a - b), x);
+}
+
+int main(int argc, char* argv[]) {
+
+  /* Initialize CPU frequency, this also starts time. */
+  unsigned long long cpufreq = 0;
+  clocks_set_cpufreq(cpufreq);
+
+/* Choke on FPEs */
+#ifdef HAVE_FE_ENABLE_EXCEPT
+  feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW);
+#endif
+
+  /* Loop over some values */
+  for (float x = 0.; x < 32; x += 0.000001) {
+
+    const double exact_p = x;
+    const double exact_n = -x;
+    const double swift_log_p = optimized_logf(exp(x));
+    const double swift_log_n = optimized_logf(exp(-x));
+    const double swift_log10_p = optimized_log10f(exp10(x));
+    const double swift_log10_n = optimized_log10f(exp10(-x));
+
+    /* Check the absolute error */
+    check_value_abs(exact_p, swift_log_p, 5e-3, x);
+    check_value_abs(exact_n, swift_log_n, 5e-3, x);
+    check_value_abs(exact_p, swift_log10_p, 5e-3, x);
+    check_value_abs(exact_n, swift_log10_n, 5e-3, x);
+
+    /* Check the relative error in the domain where it is small */
+    if (x >= 0.3) {
+      check_value(exact_p, swift_log_p, 5e-3, x);
+      check_value(exact_n, swift_log_n, 5e-3, x);
+      check_value(exact_p, swift_log10_p, 5e-3, x);
+      check_value(exact_n, swift_log10_n, 5e-3, x);
+    }
+  }
+
+  message("Success.");
+
+  return 0;
+}
diff --git a/tests/testNeutrinoCosmology.c b/tests/testNeutrinoCosmology.c
index fc2cff575250e21be05e9fbd9acef020445e9ca7..a271c6bc0e50ffe0acd15bf8220533f93d2dc935 100644
--- a/tests/testNeutrinoCosmology.c
+++ b/tests/testNeutrinoCosmology.c
@@ -18,13 +18,11 @@
  ******************************************************************************/
 
 /* Some standard headers. */
-#include "../config.h"
-
-/* Some standard headers */
 #include <fenv.h>
 #include <math.h>
 
 /* Includes. */
+#include "../config.h"
 #include "swift.h"
 
 #define N_CHECK 20
diff --git a/tests/testNeutrinoFermiDirac.c b/tests/testNeutrinoFermiDirac.c
new file mode 100644
index 0000000000000000000000000000000000000000..5d010795511aa69097eb9fdcbc830e37b1b0f3ef
--- /dev/null
+++ b/tests/testNeutrinoFermiDirac.c
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2021 Willem Elbers (whe@willemelbers.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/>.
+ *
+ ******************************************************************************/
+
+/* Some standard headers */
+#include <fenv.h>
+#include <math.h>
+
+/* Includes. */
+#include "../config.h"
+#include "neutrino/Default/fermi_dirac.h"
+#include "swift.h"
+
+/* Riemann function zeta(3) and zeta(5) */
+#define M_ZETA_3 1.2020569031595942853997
+#define M_ZETA_5 1.0369277551433699263314
+
+int main(int argc, char *argv[]) {
+  /* Exact integrals of x^n / (exp(x) + 1) on (0, infinity) */
+  double integral2 = M_ZETA_3 * 1.5;
+  double integral3 = M_PI * M_PI * M_PI * M_PI * 7.0 / 120.0;
+  double integral4 = M_ZETA_5 * 22.5;
+  double integral5 = M_PI * M_PI * M_PI * M_PI * M_PI * M_PI * 31. / 252.0;
+  /* Exact moments */
+  double mu = integral3 / integral2;
+  double mu2 = mu * mu;
+  double sigma2 = integral4 / integral2 - mu2;
+  double sigma = sqrt(sigma2);
+  double sigma3 = sigma2 * sigma;
+  double skewns = (integral5 / integral2 - 3 * mu * sigma2 - mu2 * mu) / sigma3;
+
+  /* Print FermiDirac(n) for n = 1, 2, 3 for external testing */
+  message("fermi_dirac(1) = %e", neutrino_seed_to_fermi_dirac(1));
+  message("fermi_dirac(2) = %e", neutrino_seed_to_fermi_dirac(2));
+  message("fermi_dirac(3) = %e\n", neutrino_seed_to_fermi_dirac(3));
+
+  /* Generate Fermi-Dirac numbers and compute the sum, min, and max */
+  int N = 1e7;
+  double sum = 0;
+  double min = FLT_MAX, max = 0;
+  long long seed = 290009001901;
+  for (int i = 0; i < N; i++) {
+    double x = neutrino_seed_to_fermi_dirac(seed + i);
+    sum += x;
+    if (x < min) min = x;
+    if (x > max) max = x;
+  }
+
+  /* Do a second pass for the sample variance and skewness */
+  double mean = sum / N;
+  double ss_sum = 0;
+  double sss_sum = 0;
+
+  /* We also construct a histogram */
+  int bins = 1000;
+  int *histogram1 = calloc(bins, sizeof(int));
+
+  /* Generate the same numbers again and compute statistics and histogram */
+  for (int i = 0; i < N; i++) {
+    double x = neutrino_seed_to_fermi_dirac(seed + i);
+    ss_sum += (x - mean) * (x - mean);
+    sss_sum += (x - mean) * (x - mean) * (x - mean);
+
+    int bin = (int)((x - min) / (max - min) * bins);
+    histogram1[bin]++;
+  }
+
+  /* Sample statistics */
+  double var = ss_sum / (N - 1);
+  double sdev = sqrt(var);
+  double mu3 = sss_sum / (N - 2) / (var * sdev);
+
+  /* Relative errors with the exact moments */
+  double err_mu = mean / mu - 1.;
+  double err_sig = var / sigma2 - 1.;
+  double err_skew = mu3 / skewns - 1.;
+
+  message("Sample mean is %f, exact: %f (rel. %e).", mean, mu, err_mu);
+  message("Sample variance is %f, exact: %f (rel. %e).", var, sigma2, err_sig);
+  message("Sample skewness is %f, exact: %f (rel. %e).", mu3, skewns, err_skew);
+
+  assert(fabs(err_mu) < 1e-3);
+  assert(fabs(err_sig) < 1e-2);
+  assert(fabs(err_skew) < 1e-2);
+
+  /* Construct Kolmogorov-Smirnov statistic by integrating the histogram */
+  double sum_empirical = 0.;
+  double sum_analytical = 0.;
+  double KS_statistic = 0.;
+  double dx = (max - min) / bins;
+  for (int bin = 0; bin < bins; bin++) {
+    double x = min + (bin + 0.5) * dx;
+    sum_empirical += histogram1[bin] * 1. / N;
+    sum_analytical += fermi_dirac_density(x) * x * x * dx / integral2;
+
+    double delta = fabs(sum_empirical - sum_analytical);
+    if (delta > KS_statistic) {
+      KS_statistic = delta;
+    }
+  }
+
+  /* Can we reject the hypothesis that these numbers are FD distributed? */
+  double crit_val = 5.146991e-05 * sqrt(1e9 / N);  // 99% confidence level
+  message("KS statistic = %e (99%% that KS < %e)\n", KS_statistic, crit_val);
+
+  /* We should not reject this */
+  assert(KS_statistic < crit_val);
+
+  free(histogram1);
+
+  message("Success.");
+
+  return 0;
+}
diff --git a/tools/analyse_runtime.py b/tools/analyse_runtime.py
index 47f1d492a9790bff3f02d03c6da6a446654e8d03..6c40e51f0300f84b02b3d6be29eb1d6a018964ad 100755
--- a/tools/analyse_runtime.py
+++ b/tools/analyse_runtime.py
@@ -91,7 +91,8 @@ tasks = [
     "fof",
     "others",
     "sink",
-    "RT", 
+    "neutrino",
+    "RT",
     "total",
 ]