diff --git a/doc/RTD/source/ParameterFiles/parameter_description.rst b/doc/RTD/source/ParameterFiles/parameter_description.rst
index cb6248cb66a2e56b1c9d744d5ee78bf5092f6456..b41872aabfef702beaf18308cb4be917ab6553b6 100644
--- a/doc/RTD/source/ParameterFiles/parameter_description.rst
+++ b/doc/RTD/source/ParameterFiles/parameter_description.rst
@@ -171,6 +171,31 @@ w_0 + w_a (1 - a)`. The two parameters in the YAML file are:
 If unspecified these parameters default to the default
 :math:`\Lambda\rm{CDM}` values of :math:`w_0 = -1` and :math:`w_a = 0`.
 
+The radiation density :math:`\Omega_r` can also be specified by setting
+an alternative optional parameter:
+
+* The number of ultra-relativistic degrees of freedom :math:`N_\rm{ur}`:
+  ``N_ur``.
+
+The radiation density :math:`\Omega_r` is then automatically inferred from
+:math:`N_\rm{ur}` and the present-day CMB temperature
+:math:`T_{\rm{CMB},0}=2.7255` Kelvin. This parametrization cannot
+be used together with :math:`\Omega_r`. If neither parameter is used, SWIFT
+defaults to :math:`\Omega_r = 0`. Note that :math:`N_\rm{ur}` differs from
+:math:`N_\rm{eff}`, the latter of which also includes massive neutrinos.
+
+Massive neutrinos can be included by specifying the optional parameters:
+
+* The number of massive neutrino species :math:`N_{\nu}`: ``N_nu``,
+* A comma-separated list of neutrino masses in eV: ``M_nu_eV``,
+* A comma-separated list of neutrino degeneracies: ``deg_nu``,
+* The present-day neutrino temperature :math:`T_{\nu,0}`: ``T_nu_0``.
+
+When including massive neutrinos, only ``N_nu`` and ``M_nu_eV`` are necessary.
+By default, SWIFT will assume non-degenerate species and
+:math:`T_{\nu,0}=(4/11)^{1/3}T_{\rm{CMB},0}`. Neutrinos do not contribute to
+:math:`\Omega_m = \Omega_\rm{cdm} + \Omega_b` in our conventions.
+
 For a Planck+13 cosmological model (ignoring radiation density as is
 commonly done) and running from :math:`z=127` to :math:`z=0`, one would hence
 use the following parameters:
diff --git a/examples/main.c b/examples/main.c
index 1b9c21bdb1e9f9effccd29ece728d7bfb60df3bf..620e018132edf809858f31f79e8b9c148d81ec2e 100644
--- a/examples/main.c
+++ b/examples/main.c
@@ -1189,13 +1189,17 @@ int main(int argc, char *argv[]) {
     const int with_DM_background_particles =
         N_total[swift_type_dark_matter_background] > 0;
 
+    /* Do we have neutrino particles? */
+    const int with_neutrinos = 0;  // no for now
+
     /* Initialize the space with these data. */
     if (myrank == 0) clocks_gettime(&tic);
     space_init(&s, params, &cosmo, dim, &hydro_properties, parts, gparts, sinks,
                sparts, bparts, Ngas, Ngpart, Nsink, Nspart, Nbpart, periodic,
                replicate, remap_ids, generate_gas_in_ics, with_hydro,
                with_self_gravity, with_star_formation,
-               with_DM_background_particles, talking, dry_run, nr_nodes);
+               with_DM_background_particles, with_neutrinos, talking, dry_run,
+               nr_nodes);
 
     /* Initialise the line of sight properties. */
     if (with_line_of_sight) los_init(s.dim, &los_properties, params);
diff --git a/examples/main_fof.c b/examples/main_fof.c
index bed38114d7df9cd46492b1e547295a00c95fcbd3..b40a61b531aa3b17bb281fcc5be2ad52ac06500b 100644
--- a/examples/main_fof.c
+++ b/examples/main_fof.c
@@ -542,14 +542,17 @@ int main(int argc, char *argv[]) {
   const int with_DM_background_particles =
       N_total[swift_type_dark_matter_background] > 0;
 
+  /* Do we have neutrino particles? */
+  const int with_neutrinos = 0;  // no for now
+
   /* Initialize the space with these data. */
   if (myrank == 0) clocks_gettime(&tic);
   space_init(&s, params, &cosmo, dim, /*hydro_props=*/NULL, parts, gparts,
              sinks, sparts, bparts, Ngas, Ngpart, Nsink, Nspart, Nbpart,
              periodic, replicate, /*remap_ids=*/0,
              /*generate_gas_in_ics=*/0, /*hydro=*/N_total[0] > 0, /*gravity=*/1,
-             /*with_star_formation=*/0, with_DM_background_particles, talking,
-             /*dry_run=*/0, nr_nodes);
+             /*with_star_formation=*/0, with_DM_background_particles,
+             with_neutrinos, talking, /*dry_run=*/0, nr_nodes);
 
   if (myrank == 0) {
     clocks_gettime(&toc);
diff --git a/examples/parameter_example.yml b/examples/parameter_example.yml
index e201f4f05b28226930abd1378e69928c739ae424..075f7a02ec40081fc0738b17aedfb4166686f8f2 100644
--- a/examples/parameter_example.yml
+++ b/examples/parameter_example.yml
@@ -25,6 +25,11 @@ Cosmology:
   Omega_r:        0.            # (Optional) Radiation density parameter
   w_0:            -1.0          # (Optional) Dark-energy equation-of-state parameter at z=0.
   w_a:            0.            # (Optional) Dark-energy equation-of-state time evolution parameter.
+  T_nu_0:         1.9514        # (Optional) Present-day neutrino temperature in internal units, used for massive neutrinos
+  N_ur:           1.0196        # (Optional) Number of ultra-relativistic species (e.g. massless neutrinos). Assumes instantaneous decoupling T_ur/T_g=(4/11)^(1/3). Cannot be used together with Omega_r.
+  N_nu:           2             # (Optional) Integer number of massive neutrinos. Note that neutrinos do NOT contribute to Omega_m = Omega_cdm + Omega_b in our conventions.
+  M_nu_eV:        0.05, 0.01    # (Optional) Comma-separated list of N_nu nonzero neutrino masses in electron-volts
+  deg_nu:         1.0, 1.0      # (Optional) Comma-separated list of N_nu neutrino degeneracies (default values of 1.0)
 
 # Parameters for the hydrodynamics scheme
 SPH:
@@ -128,7 +133,6 @@ Scheduler:
   engine_max_parts_per_ghost:    1000  # (Optional) Maximum number of parts per ghost.
   engine_max_sparts_per_ghost:   1000  # (Optional) Maximum number of sparts per ghost.
   engine_max_parts_per_cooling: 10000  # (Optional) Maximum number of parts per cooling task.
-  dependency_graph_frequency:       0  # (Optional) Dumping frequency of the dependency graph. By default, writes only at the first step.
 
 # Parameters governing the time integration (Set dt_min and dt_max to the same value for a fixed time-step run.)
 TimeIntegration:
@@ -169,7 +173,7 @@ Logger:
   basename:             index  # Common part of the filenames
   initial_buffer_size:  1      # (Optional) Buffer size in GB
   buffer_scale:	        10     # (Optional) When buffer size is too small, update it with required memory times buffer_scale
-  
+
 # Parameters governing the conserved quantities statistics
 Statistics:
   delta_time:           1e-2        # Time between statistics output
@@ -234,7 +238,7 @@ DomainDecomposition:
 StructureFinding:
   config_file_name:     stf_input.cfg # Name of the STF config file.
   basename:             haloes        # Common part of the name of output files.
-  subdir_per_output:    stf           # (Optional) Sub-directory (within Snapshots:subdir) to use for each invocation of STF. Defaults to "" (i.e. no sub-directory) 
+  subdir_per_output:    stf           # (Optional) Sub-directory (within Snapshots:subdir) to use for each invocation of STF. Defaults to "" (i.e. no sub-directory)
   scale_factor_first:   0.92          # (Optional) Scale-factor of the first structure finding (cosmological run)
   time_first:           0.01          # (Optional) Time of the first structure finding output (in internal units).
   delta_time:           1.10          # (Optional) Time difference between consecutive structure finding outputs (in internal units) in simulation time intervals.
@@ -283,7 +287,7 @@ EoS:
 
 # Point mass external potentials
 PointMassPotential:
-  useabspos:       0                   # 0 -> positions based on centre, 1 -> absolute positions 
+  useabspos:       0                   # 0 -> positions based on centre, 1 -> absolute positions
   position:        [50.,50.,50.]       # location of external point mass (internal units)
   mass:            1e10                # mass of external point mass (internal units)
   timestep_mult:   0.03                # Dimensionless pre-factor for the time-step condition
@@ -291,15 +295,15 @@ PointMassPotential:
 
 # Isothermal potential parameters
 IsothermalPotential:
-  useabspos:       0                   # 0 -> positions based on centre, 1 -> absolute positions 
+  useabspos:       0                   # 0 -> positions based on centre, 1 -> absolute positions
   position:        [100.,100.,100.]    # Location of centre of isothermal potential with respect to centre of the box (internal units)
   vrot:            200.                # Rotation speed of isothermal potential (internal units)
   timestep_mult:   0.03                # Dimensionless pre-factor for the time-step condition
   epsilon:         0.1                 # Softening size (internal units)
-  
+
 # Hernquist potential parameters
 HernquistPotential:
-  useabspos:       0        # 0 -> positions based on centre, 1 -> absolute positions 
+  useabspos:       0        # 0 -> positions based on centre, 1 -> absolute positions
   position:        [100.,100.,100.]    # Location of centre of Henrquist potential with respect to centre of the box (if 0) otherwise absolute (if 1) (internal units)
   idealizeddisk:   0        # (Optional) Whether to run with idealizeddisk or without, 0 used the mass and scalelength as mandatory parameters, while 1 uses more advanced disk dependent paramters
   mass:            1e10     # (Optional 0) default parameter, Mass of the Hernquist potential
@@ -314,10 +318,10 @@ HernquistPotential:
   bulgefraction:              0.00705852860979  # (Optional 1) Bulge mass fraction (equal to MB in MakeNewDisk and GalIC)
   timestep_mult:   0.01     # Dimensionless pre-factor for the time-step condition, basically determines the fraction of the orbital time we use to do the time integration
   epsilon:         0.1      # Softening size (internal units)
- 
+
 # NFW potential parameters
 NFWPotential:
-  useabspos:          0             # 0 -> positions based on centre, 1 -> absolute positions 
+  useabspos:          0             # 0 -> positions based on centre, 1 -> absolute positions
   position:           [0.0,0.0,0.0] # Location of centre of the NFW potential with respect to centre of the box (internal units) if useabspos=0 otherwise with respect to the 0,0,0, coordinates.
   concentration:      8.            # Concentration of the halo
   M_200:              2.0e+12       # Mass of the halo (M_200 in internal units)
@@ -326,7 +330,7 @@ NFWPotential:
 
 # NFW + Miyamoto-Nagai disk potential
 NFW_MNPotential:
-  useabspos:         0         # 0 -> positions based on centre, 1 -> absolute positions 
+  useabspos:         0         # 0 -> positions based on centre, 1 -> absolute positions
   position:         [0.,0.,0.] # Location of centre of isothermal potential with respect to centre of the box (if 0) otherwise absolute (if 1) (internal units)
   timestep_mult:    0.01       # Dimensionless pre-factor for the time-step condition, basically determines the fraction of the orbital time we use to do the time integration
   epsilon:          0.01       # Softening size (internal units)
@@ -336,7 +340,7 @@ NFW_MNPotential:
   Mdisk:            3.0        # Mass of the disk (internal units)
   Rdisk:            4.0        # Effective radius of the disk (internal units)
   Zdisk:            0.4704911  # Scale-height of the disk (internal units)
-  
+
 # Disk-patch potential parameters. The potential is along the x-axis.
 DiscPatchPotential:
   surface_density: 10.      # Surface density of the disc (internal units)
@@ -423,7 +427,7 @@ COLIBRECooling:
   He_reion_eV_p_H:         2.0               # Energy inject by Helium re-ionization in electron-volt per Hydrogen atom
   rapid_cooling_threshold: 0.333333          # Switch to rapid cooling regime for dt / t_cool above this threshold.
   delta_logTEOS_subgrid_properties: 0.3      # delta log T above the EOS below which the subgrid properties use Teq assumption
-  
+
 # Cooling with Grackle 3.0
 GrackleCooling:
   cloudy_table: CloudyData_UVB=HM2012.h5       # Name of the Cloudy Table (available on the grackle bitbucket repository)
@@ -458,7 +462,6 @@ EAGLEChemistry:
 GEARChemistry:
   initial_metallicity: 1         # Initial metallicity of the gas (mass fraction)
   scale_initial_metallicity: 1   # Should we scale the initial metallicity with the solar one?
-  diffusion_coefficient: 1e-3    # Coefficient for the diffusion (see Shen et al. 2010; differs by gamma^2 due to h^2).
 
 
 # Parameters related to star formation models  -----------------------------------------------
@@ -552,7 +555,7 @@ GEARFeedback:
 
 
 # Parameters related to AGN models  -----------------------------------------------
-  
+
 # EAGLE AGN model
 EAGLEAGN:
   subgrid_seed_mass_Msun:             1.5e5      # Black hole subgrid mass at creation time in solar masses.
@@ -569,7 +572,7 @@ EAGLEAGN:
   radiative_efficiency:               0.1        # Fraction of the accreted mass that gets radiated.
   max_eddington_fraction:             1.         # Maximal allowed accretion rate in units of the Eddington rate.
   eddington_fraction_for_recording:   0.1        # Record the last time BHs reached an Eddington ratio above this threshold.
-  use_nibbling:                       0          # Continuously transfer small amounts of mass from all gas neighbours to a black hole [1] or stochastically swallow whole gas particles [0]? 
+  use_nibbling:                       0          # Continuously transfer small amounts of mass from all gas neighbours to a black hole [1] or stochastically swallow whole gas particles [0]?
   min_gas_mass_for_nibbling:          9e5        # Minimum mass for a gas particle to be nibbled from [M_Sun]. Only used if use_nibbling is 1.
   coupling_efficiency:                0.15       # Fraction of the radiated energy that couples to the gas in feedback events.
   AGN_feedback_model:                 Random     # Feedback modes: Random, Isotropic, MinimumDistance, MinimumDensity
diff --git a/src/cosmology.c b/src/cosmology.c
index 038d87fb6b11d428cebd73cda89cd25efc8f9b61..bc0efcc5116e69ff0d8b891c601f84fb503c3bb8 100644
--- a/src/cosmology.c
+++ b/src/cosmology.c
@@ -43,7 +43,7 @@
 #endif
 
 /*! Number of values stored in the cosmological interpolation tables */
-const int cosmology_table_length = 10000;
+const int cosmology_table_length = 30000;
 
 #ifdef HAVE_LIBGSL
 /*! Size of the GSL workspace */
@@ -195,8 +195,11 @@ void cosmology_update(struct cosmology *c, const struct phys_const *phys_const,
   /* Dark-energy equation of state */
   c->w = cosmology_dark_energy_EoS(a, c->w_0, c->w_a);
 
+  /* Update the neutrino density */
+  c->Omega_nu = cosmology_get_neutrino_density(c, a);
+
   /* E(z) */
-  const double Omega_r = c->Omega_r;
+  const double Omega_r = c->Omega_r + c->Omega_nu;
   const double Omega_m = c->Omega_m;
   const double Omega_k = c->Omega_k;
   const double Omega_l = c->Omega_lambda;
@@ -237,7 +240,8 @@ void cosmology_update(struct cosmology *c, const struct phys_const *phys_const,
 double drift_integrand(double a, void *param) {
 
   const struct cosmology *c = (const struct cosmology *)param;
-  const double Omega_r = c->Omega_r;
+  const double Omega_nu = cosmology_get_neutrino_density(c, a);
+  const double Omega_r = c->Omega_r + Omega_nu;
   const double Omega_m = c->Omega_m;
   const double Omega_k = c->Omega_k;
   const double Omega_l = c->Omega_lambda;
@@ -261,7 +265,8 @@ double drift_integrand(double a, void *param) {
 double gravity_kick_integrand(double a, void *param) {
 
   const struct cosmology *c = (const struct cosmology *)param;
-  const double Omega_r = c->Omega_r;
+  const double Omega_nu = cosmology_get_neutrino_density(c, a);
+  const double Omega_r = c->Omega_r + Omega_nu;
   const double Omega_m = c->Omega_m;
   const double Omega_k = c->Omega_k;
   const double Omega_l = c->Omega_lambda;
@@ -285,7 +290,8 @@ double gravity_kick_integrand(double a, void *param) {
 double hydro_kick_integrand(double a, void *param) {
 
   const struct cosmology *c = (const struct cosmology *)param;
-  const double Omega_r = c->Omega_r;
+  const double Omega_nu = cosmology_get_neutrino_density(c, a);
+  const double Omega_r = c->Omega_r + Omega_nu;
   const double Omega_m = c->Omega_m;
   const double Omega_k = c->Omega_k;
   const double Omega_l = c->Omega_lambda;
@@ -311,7 +317,8 @@ double hydro_kick_integrand(double a, void *param) {
 double hydro_kick_corr_integrand(double a, void *param) {
 
   const struct cosmology *c = (const struct cosmology *)param;
-  const double Omega_r = c->Omega_r;
+  const double Omega_nu = cosmology_get_neutrino_density(c, a);
+  const double Omega_r = c->Omega_r + Omega_nu;
   const double Omega_m = c->Omega_m;
   const double Omega_k = c->Omega_k;
   const double Omega_l = c->Omega_lambda;
@@ -334,7 +341,8 @@ double hydro_kick_corr_integrand(double a, void *param) {
 double time_integrand(double a, void *param) {
 
   const struct cosmology *c = (const struct cosmology *)param;
-  const double Omega_r = c->Omega_r;
+  const double Omega_nu = cosmology_get_neutrino_density(c, a);
+  const double Omega_r = c->Omega_r + Omega_nu;
   const double Omega_m = c->Omega_m;
   const double Omega_k = c->Omega_k;
   const double Omega_l = c->Omega_lambda;
@@ -349,6 +357,184 @@ double time_integrand(double a, void *param) {
   return (1. / H) * a_inv;
 }
 
+/**
+ * @brief Evaluates the neutrino density momentum integrand
+ * \f$ x^2 \sqrt{x^2 + y^2} / (1+e^x) \f$, where
+ * \f$ y = a*M_nu/(kb*T) \f$ is the neutrino mass scaled with redshift.
+ *
+ * This is used to evaluate the integral on (0, 1).
+ *
+ * @param x The momentum integration variable
+ * @param param Neutrino mass y scaled by temperature at redshift of interest.
+ * @return The integrand evaluated at x
+ */
+double neutrino_density_integrand(double x, void *param) {
+  double y = *(double *)param;
+  double numerator = x * x * hypot(x, y);
+
+  /* Handle overflows */
+  if (x > 20 + log(numerator)) {
+    return numerator * exp(-x);
+  }
+
+  return numerator / (1.0 + exp(x));
+}
+
+/**
+ * @brief Evaluates the transformed neutrino density momentum integrand
+ * \f$ w^{-4} \sqrt{w^{-2} + y^2} / (1+e^{-w}) \f$, where
+ * \f$ y = a*M_nu/(kb*T) \f$ is the neutrino mass scaled with redshift.
+ *
+ * This is used to evaluate the integral on (1, infinity).
+ *
+ * @param w The transformed momentum integration variable w=1/x
+ * @param param Neutrino mass y scaled by temperature at redshift of interest.
+ * @return The integrand evaluated at w
+ */
+double neutrino_density_integrand_transformed(double w, void *param) {
+  return neutrino_density_integrand(1. / w, param) / (w * w);
+}
+
+/**
+ * @brief Performs the neutrino density momentum integral
+ * \f$ \int_0^\infty x^2 \sqrt{x^2 + y^2} / (1+e^x) dx \f$, where
+ * \f$ y = a*M_nu/(kb*T) \f$ is the neutrino mass scaled with redshift,
+ * without pre-factors.
+ *
+ * @param space The GSL working space
+ * @param y Neutrino mass y scaled by temperature at redshift of interest.
+ * @return The integral evaluated at y
+ */
+double neutrino_density_integrate(gsl_integration_workspace *space, double y) {
+  double intermediate, abserr;
+
+  double result = 0;
+
+  gsl_function F1 = {&neutrino_density_integrand, &y};
+  gsl_function F2 = {&neutrino_density_integrand_transformed, &y};
+  /* Integrate between 0 and 1 */
+  gsl_integration_qag(&F1, 0.0, 1.0, 0, 1.0e-13, GSL_workspace_size,
+                      GSL_INTEG_GAUSS61, space, &intermediate, &abserr);
+
+  result += intermediate;
+  /* Integrate between 1 and infinity */
+  gsl_integration_qag(&F2, 0.0, 1.0, 0, 1.0e-13, GSL_workspace_size,
+                      GSL_INTEG_GAUSS61, space, &intermediate, &abserr);
+  result += intermediate;
+
+  return result;
+}
+
+/**
+ * @brief Find a time when all neutrinos are still relativistic. Store the
+ * starting, mid, and end points of the neutrino interpolation tables in c.
+ *
+ * @param c The cosmology structure
+ * @param tol Tolerance in density integral
+ * @param space The GSL working space
+ */
+void neutrino_find_relativistic_redshift(struct cosmology *c, double tol,
+                                         gsl_integration_workspace *space) {
+
+  /* Find the largest neutrino mass */
+  double M_max_eV = c->M_nu_eV[0];
+  for (int i = 1; i < c->N_nu; i++) {
+    M_max_eV = fmax(M_max_eV, c->M_nu_eV[i]);
+  }
+
+  /* A safe starting time when neutrinos are relativistic */
+  double a_safe = 0.5 * tol / M_max_eV;
+
+  /* Dont start the early table later than the simulation */
+  a_safe = fmin(a_safe, 0.9 * c->a_begin);
+
+  /* Start the late table just before the start of the simulation */
+  double a_midpoint = 0.99 * c->a_begin;
+
+  /* End the late table today (a=1) or at a_end, whichever is later */
+  double a_final = fmax(1.0, c->a_end);
+
+  /* Integrate early table on (a_start, a_mid) and late table on (a_mid, a_f) */
+  c->log_a_long_begin = log(a_safe);
+  c->log_a_long_mid = log(a_midpoint);
+  c->log_a_long_end = log(a_final);
+}
+
+/**
+ * @brief Initialise the neutrino density interpolation tables (early and late).
+ */
+void cosmology_init_neutrino_tables(struct cosmology *c) {
+
+  /* Skip if we have no massive neutrinos */
+  if (c->N_nu == 0) return;
+
+#ifdef HAVE_LIBGSL
+
+  /* Allocate memory for longer interpolation tables */
+  if (swift_memalign("cosmo.table", (void **)&c->neutrino_density_early_table,
+                     SWIFT_STRUCT_ALIGNMENT,
+                     cosmology_table_length * sizeof(double)) != 0)
+    error("Failed to allocate cosmology interpolation table");
+  if (swift_memalign("cosmo.table", (void **)&c->neutrino_density_late_table,
+                     SWIFT_STRUCT_ALIGNMENT,
+                     cosmology_table_length * sizeof(double)) != 0)
+    error("Failed to allocate cosmology interpolation table");
+
+  /* Initalise the GSL workspace */
+  gsl_integration_workspace *space =
+      gsl_integration_workspace_alloc(GSL_workspace_size);
+
+  /* Find a safe redshift to start the neutrino density interpolation table */
+  neutrino_find_relativistic_redshift(c, 1e-7, space);
+
+  const double pre_factor = 15. * pow(c->T_nu_0 * M_1_PI / c->T_CMB_0, 4);
+  const double early_delta_a =
+      (c->log_a_long_mid - c->log_a_long_begin) / cosmology_table_length;
+  const double late_delta_a =
+      (c->log_a_long_end - c->log_a_long_mid) / cosmology_table_length;
+
+  double result;
+
+  /* Fill the early neutrino density table between (a_long_begin, a_long_mid) */
+  for (int i = 0; i < cosmology_table_length; i++) {
+    double O_nu = 0.;
+    double a = exp(c->log_a_long_begin + early_delta_a * (i + 1));
+
+    /* Integrate the FD distribtution for each species */
+    for (int j = 0; j < c->N_nu; j++) {
+      double y = a * c->M_nu_eV[j] / c->T_nu_0_eV;
+      result = neutrino_density_integrate(space, y);
+      O_nu += c->deg_nu[j] * result * pre_factor * c->Omega_g;
+    }
+
+    c->neutrino_density_early_table[i] = O_nu;
+  }
+
+  /* Fill the late neutrino density table between (a_long_mid, a_long_end) */
+  for (int i = 0; i < cosmology_table_length; i++) {
+    double O_nu = 0.;
+    double a = exp(c->log_a_long_mid + late_delta_a * (i + 1));
+
+    /* Integrate the FD distribtution for each species */
+    for (int j = 0; j < c->N_nu; j++) {
+      double y = a * c->M_nu_eV[j] / c->T_nu_0_eV;
+      result = neutrino_density_integrate(space, y);
+      O_nu += c->deg_nu[j] * result * pre_factor * c->Omega_g;
+    }
+
+    c->neutrino_density_late_table[i] = O_nu;
+  }
+
+  /* Free the workspace */
+  gsl_integration_workspace_free(space);
+
+#else
+
+  error("Code not compiled with GSL. Can't compute cosmology integrals.");
+
+#endif
+}
+
 /**
  * @brief Initialise the interpolation tables for the integrals.
  */
@@ -525,11 +711,39 @@ void cosmology_init(struct swift_params *params, const struct unit_system *us,
   c->Omega_r = parser_get_opt_param_double(params, "Cosmology:Omega_r", 0.);
   c->Omega_lambda = parser_get_param_double(params, "Cosmology:Omega_lambda");
   c->Omega_b = parser_get_param_double(params, "Cosmology:Omega_b");
-  c->Omega_nu = 0.;
   c->w_0 = parser_get_opt_param_double(params, "Cosmology:w_0", -1.);
   c->w_a = parser_get_opt_param_double(params, "Cosmology:w_a", 0.);
   c->h = parser_get_param_double(params, "Cosmology:h");
 
+  /* Neutrino temperature (inferred from T_CMB_0 if not specified) */
+  c->T_nu_0 = parser_get_opt_param_double(params, "Cosmology:T_nu_0", 0.);
+
+  /* Number of ultra-relativistic (massless) and massive neutrino species */
+  c->N_ur = parser_get_opt_param_double(params, "Cosmology:N_ur", 0.);
+  c->N_nu = parser_get_opt_param_int(params, "Cosmology:N_nu", 0);
+
+  /* Make sure that the cosmological parameters are not overdetermined */
+  if (c->Omega_r != 0. && c->N_ur != 0.) {
+    error("Cannot use both Cosmology:Omega_r and Cosmology:N_ur.");
+  }
+
+  /* If there are massive neutrinos, read the masses and degeneracies */
+  if (c->N_nu > 0) {
+    c->M_nu_eV = (double *)swift_malloc("Mnu", c->N_nu * sizeof(double));
+    c->deg_nu = (double *)swift_malloc("degnu", c->N_nu * sizeof(double));
+
+    /* Set default values */
+    for (int i = 0; i < c->N_nu; i++) {
+      c->M_nu_eV[i] = 0.0;
+      c->deg_nu[i] = 1.0;
+    }
+
+    parser_get_opt_param_double_array(params, "Cosmology:M_nu_eV", c->N_nu,
+                                      c->M_nu_eV);
+    parser_get_opt_param_double_array(params, "Cosmology:deg_nu", c->N_nu,
+                                      c->deg_nu);
+  }
+
   /* Read the start and end of the simulation */
   c->a_begin = parser_get_param_double(params, "Cosmology:a_begin");
   c->a_end = parser_get_param_double(params, "Cosmology:a_end");
@@ -546,9 +760,6 @@ void cosmology_init(struct swift_params *params, const struct unit_system *us,
 
   /* Construct derived quantities */
 
-  /* Curvature density (for closure) */
-  c->Omega_k = 1. - (c->Omega_m + c->Omega_r + c->Omega_lambda);
-
   /* Dark-energy equation of state */
   c->w = cosmology_dark_energy_EoS(c->a_begin, c->w_0, c->w_a);
 
@@ -563,14 +774,86 @@ void cosmology_init(struct swift_params *params, const struct unit_system *us,
   c->critical_density_0 =
       3. * c->H0 * c->H0 / (8. * M_PI * phys_const->const_newton_G);
 
-  /* CMB temperature in internal units */
+  /* Some constants */
   const double cc = phys_const->const_speed_light_c;
-  const double T_CMB_0_4 = c->Omega_r * c->critical_density_0 * cc * cc * cc /
-                           (4. * phys_const->const_stefan_boltzmann);
-  c->T_CMB_0 = pow(T_CMB_0_4, 1. / 4.);
+  const double rho_c3_on_4sigma = c->critical_density_0 * cc * cc * cc /
+                                  (4. * phys_const->const_stefan_boltzmann);
+
+  /* Handle neutrinos only if present */
+  if (c->N_ur == 0. && c->N_nu == 0) {
+    /* Infer T_CMB_0 from Omega_r */
+    c->T_CMB_0 = pow(c->Omega_r * rho_c3_on_4sigma, 1. / 4.);
+    c->T_CMB_0_K =
+        c->T_CMB_0 / units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+
+    c->Omega_g = c->Omega_r;
+    c->T_nu_0 = 0.;
+    c->T_nu_0_eV = 0.;
+    c->Omega_ur = 0.;
+    c->Omega_nu_0 = 0.;
+    c->Omega_nu = 0.;
+    c->N_eff = 0.;
+
+    c->neutrino_density_early_table = NULL;
+    c->neutrino_density_late_table = NULL;
+  } else {
+    /* Infer T_CMB_0 from Omega_r if the latter is specified */
+    if (c->Omega_r != 0) {
+      c->T_CMB_0 = pow(c->Omega_r * rho_c3_on_4sigma, 1. / 4.);
+    } else {
+      c->T_CMB_0 = phys_const->const_T_CMB_0;
+    }
 
-  c->T_CMB_0_K =
-      c->T_CMB_0 / units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+    /* Approximate the neutrino temperature if unspecified */
+    const double decoupling_factor = cbrt(4. / 11);
+    const double dec_4 = pow(decoupling_factor, 4);
+    if (c->T_nu_0 == 0.) {
+      c->T_nu_0 = c->T_CMB_0 * decoupling_factor;
+    }
+
+    /* Unit conversions */
+    c->T_CMB_0_K =
+        c->T_CMB_0 / units_cgs_conversion_factor(us, UNIT_CONV_TEMPERATURE);
+    c->T_nu_0_eV = c->T_nu_0 * phys_const->const_boltzmann_k /
+                   phys_const->const_electron_volt;
+
+    /* Set photon density and compute radiation density if necessary */
+    if (c->Omega_r != 0.) {
+      c->Omega_g = c->Omega_r;
+      c->Omega_ur = 0.;
+    } else {
+      /* Infer CMB density from the temperature */
+      c->Omega_g = pow(c->T_CMB_0, 4) / rho_c3_on_4sigma;
+
+      /* Compute the density of ultra-relativistic fermionic species */
+      c->Omega_ur = c->N_ur * (7. / 8.) * dec_4 * c->Omega_g;
+
+      /* Compute the total radiation density */
+      c->Omega_r = c->Omega_g + c->Omega_ur;
+    }
+
+    /* Compute effective number of relativistic species at early times */
+    double N_nu_tot_deg = 0.;
+    for (int i = 0; i < c->N_nu; i++) {
+      N_nu_tot_deg += c->deg_nu[i];
+    }
+    c->N_eff = c->N_ur + N_nu_tot_deg * pow(c->T_nu_0 / c->T_CMB_0, 4) / dec_4;
+
+    /* Initialise the neutrino density interpolation tables if necessary */
+    c->neutrino_density_early_table = NULL;
+    c->neutrino_density_late_table = NULL;
+    cosmology_init_neutrino_tables(c);
+
+    /* Retrieve the present-day total density due to massive neutrinos */
+    c->Omega_nu_0 = cosmology_get_neutrino_density(c, 1);
+    c->Omega_nu = c->Omega_nu_0;  // will be updated
+  }
+
+  /* Cold dark matter density */
+  c->Omega_cdm = c->Omega_m - c->Omega_b;
+
+  /* Curvature density (for closure) */
+  c->Omega_k = 1. - (c->Omega_m + c->Omega_r + c->Omega_lambda + c->Omega_nu_0);
 
   /* Initialise the interpolation tables */
   c->drift_fac_interp_table = NULL;
@@ -580,7 +863,7 @@ void cosmology_init(struct swift_params *params, const struct unit_system *us,
   c->time_interp_table_offset = 0.;
   cosmology_init_tables(c);
 
-  /* Set remaining variables to alid values */
+  /* Set remaining variables to valid values */
   cosmology_update(c, phys_const, 0);
 
   /* Update the times */
@@ -612,10 +895,22 @@ void cosmology_init_no_cosmo(struct cosmology *c) {
   c->h = 1.;
   c->w = -1.;
 
+  c->Omega_cdm = 0.;
+  c->Omega_ur = 0.;
+  c->Omega_g = 0.;
+  c->T_nu_0 = 0.;
+  c->T_nu_0_eV = 0.;
+  c->N_nu = 0;
+  c->N_ur = 0.;
+  c->N_eff = 0.;
+
   c->a_begin = 1.;
   c->a_end = 1.;
   c->log_a_begin = 0.;
   c->log_a_end = 0.;
+  c->log_a_long_begin = 0.;
+  c->log_a_long_mid = 0.;
+  c->log_a_long_end = 0.;
 
   c->H = 0.;
   c->H0 = 0.;
@@ -656,6 +951,8 @@ void cosmology_init_no_cosmo(struct cosmology *c) {
   c->hydro_kick_fac_interp_table = NULL;
   c->hydro_kick_corr_interp_table = NULL;
   c->time_interp_table = NULL;
+  c->neutrino_density_early_table = NULL;
+  c->neutrino_density_late_table = NULL;
   c->time_interp_table_offset = 0.;
   c->scale_factor_interp_table = NULL;
 
@@ -836,6 +1133,30 @@ double cosmology_get_delta_time(const struct cosmology *c,
   return t2 - t1;
 }
 
+/**
+ * @brief Compute neutrino density parameter Omega_nu at the given scale-factor
+ * This is the effective present day value, i.e. must be multiplied by (1+z)^4
+ *
+ * @param c The current #cosmology.
+ * @param a The scale factor
+ * @return The density parameter
+ */
+double cosmology_get_neutrino_density(const struct cosmology *c, double a) {
+
+  if (c->N_nu == 0) return 0.;
+
+  const double log_a = log(a);
+
+  if (log_a < c->log_a_long_begin)
+    return c->neutrino_density_early_table[0];
+  else if (log_a < c->log_a_long_mid)
+    return interp_table(c->neutrino_density_early_table, log_a,
+                        c->log_a_long_begin, c->log_a_long_mid);
+  else
+    return interp_table(c->neutrino_density_late_table, log_a,
+                        c->log_a_long_mid, c->log_a_long_end);
+}
+
 /**
  * @brief Compute the cosmic time (in internal units) between two scale factors
  *
@@ -935,15 +1256,31 @@ double cosmology_get_scale_factor(const struct cosmology *c, double t) {
 void cosmology_print(const struct cosmology *c) {
 
   message(
-      "Density parameters: [O_m, O_l, O_b, O_k, O_r, O_nu] = [%f, %f, %f, %f, "
-      "%f, %f]",
-      c->Omega_m, c->Omega_lambda, c->Omega_b, c->Omega_k, c->Omega_r,
-      c->Omega_nu);
+      "Density parameters: [O_m, O_l, O_b, O_k, O_r] = [%f, %f, %f, %f, %f]",
+      c->Omega_m, c->Omega_lambda, c->Omega_b, c->Omega_k, c->Omega_r);
+  message(
+      "Additional density parameters: [O_nu_0, O_cdm, O_ur, O_g] = [%f, "
+      "%f, %f, %f]",
+      c->Omega_nu_0, c->Omega_cdm, c->Omega_ur, c->Omega_g);
   message("Dark energy equation of state: w_0=%f w_a=%f", c->w_0, c->w_a);
   message("Hubble constant: h = %f, H_0 = %e U_t^(-1)", c->h, c->H0);
   message("Hubble time: 1/H0 = %e U_t", c->Hubble_time);
-  message("CMB temperature at z=0 derived from O_r: T_CMB = %e U_T",
+  message("CMB temperature at z=0 implied by cosmology: T_CMB = %e U_T",
           c->T_CMB_0);
+  message("Neutrino temperature at z=0: T_nu = %e U_T", c->T_nu_0);
+  message("Numbers of relatistic species: [N_nu, N_ur, N_eff] = [%d, %f, %f]",
+          c->N_nu, c->N_ur, c->N_eff);
+  /* Print neutrino masses and degeneracies */
+  if (c->N_nu > 0) {
+    char neutrino_mass_string[10 * c->N_nu];
+    char neutrino_deg_string[10 * c->N_nu];
+    for (int i = 0; i < c->N_nu; i++) {
+      sprintf(neutrino_mass_string + i * 10, "%.2e  ", c->M_nu_eV[i]);
+      sprintf(neutrino_deg_string + i * 10, "%.2e  ", c->deg_nu[i]);
+    }
+    message("Neutrino masses: %seV", neutrino_mass_string);
+    message("Neutrino degeneracies: %s", neutrino_deg_string);
+  }
   message("Universe age at present day: %e U_t",
           c->universe_age_at_present_day);
 }
@@ -956,6 +1293,12 @@ void cosmology_clean(struct cosmology *c) {
   swift_free("cosmo.table", c->hydro_kick_corr_interp_table);
   swift_free("cosmo.table", c->time_interp_table);
   swift_free("cosmo.table", c->scale_factor_interp_table);
+  if (c->N_nu > 0) {
+    swift_free("cosmo.table", c->neutrino_density_early_table);
+    swift_free("cosmo.table", c->neutrino_density_late_table);
+    swift_free("Mnu", c->M_nu_eV);
+    swift_free("degnu", c->deg_nu);
+  }
 }
 
 #ifdef HAVE_HDF5
@@ -978,6 +1321,19 @@ void cosmology_write_model(hid_t h_grp, const struct cosmology *c) {
   io_write_attribute_d(h_grp, "Omega_k", c->Omega_k);
   io_write_attribute_d(h_grp, "Omega_lambda", c->Omega_lambda);
   io_write_attribute_d(h_grp, "Omega_nu", c->Omega_nu);
+  io_write_attribute_d(h_grp, "Omega_nu_0", c->Omega_nu_0);
+  io_write_attribute_d(h_grp, "Omega_ur", c->Omega_ur);
+  io_write_attribute_d(h_grp, "Omega_cdm", c->Omega_cdm);
+  io_write_attribute_d(h_grp, "Omega_g", c->Omega_g);
+  io_write_attribute_d(h_grp, "T_nu_0 [internal units]", c->T_nu_0);
+  io_write_attribute_d(h_grp, "T_nu_0 [eV]", c->T_nu_0_eV);
+  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);
+  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);
+  }
   io_write_attribute_d(h_grp, "T_CMB_0 [internal units]", c->T_CMB_0);
   io_write_attribute_d(h_grp, "T_CMB_0 [K]", c->T_CMB_0_K);
   io_write_attribute_d(h_grp, "w_0", c->w_0);
@@ -999,6 +1355,16 @@ void cosmology_write_model(hid_t h_grp, const struct cosmology *c) {
 void cosmology_struct_dump(const struct cosmology *cosmology, FILE *stream) {
   restart_write_blocks((void *)cosmology, sizeof(struct cosmology), 1, stream,
                        "cosmology", "cosmology function");
+
+  /* Also store the neutrino mass and degeneracy arrays if necessary */
+  if (cosmology->N_nu > 0) {
+    restart_write_blocks((double *)cosmology->M_nu_eV, sizeof(double),
+                         cosmology->N_nu, stream, "cosmology->M_nu_eV",
+                         "neutrino masses eV");
+    restart_write_blocks((double *)cosmology->deg_nu, sizeof(double),
+                         cosmology->N_nu, stream, "cosmology->deg_nu",
+                         "neutrino degeneracies");
+  }
 }
 
 /**
@@ -1014,6 +1380,21 @@ void cosmology_struct_restore(int enabled, struct cosmology *cosmology,
   restart_read_blocks((void *)cosmology, sizeof(struct cosmology), 1, stream,
                       NULL, "cosmology function");
 
+  /* Restore the neutrino mass and degeneracy arrays if necessary */
+  if (cosmology->N_nu > 0) {
+    cosmology->M_nu_eV =
+        (double *)swift_malloc("Mnu", cosmology->N_nu * sizeof(double));
+    restart_read_blocks((double *)cosmology->M_nu_eV, sizeof(double),
+                        cosmology->N_nu, stream, NULL, "neutrino masses eV");
+    cosmology->deg_nu =
+        (double *)swift_malloc("degnu", cosmology->N_nu * sizeof(double));
+    restart_read_blocks((double *)cosmology->deg_nu, sizeof(double),
+                        cosmology->N_nu, stream, NULL, "neutrino degeneracies");
+  }
+
   /* Re-initialise the tables if using a cosmology. */
-  if (enabled) cosmology_init_tables(cosmology);
+  if (enabled) {
+    cosmology_init_neutrino_tables(cosmology);
+    cosmology_init_tables(cosmology);
+  }
 }
diff --git a/src/cosmology.h b/src/cosmology.h
index 5a33229a9d7255e47eb79772c7b59202e59e20c8..a72474d16c37403bcc5c25aa127027c63e853a3f 100644
--- a/src/cosmology.h
+++ b/src/cosmology.h
@@ -144,12 +144,24 @@ struct cosmology {
   /*! Cosmological constant density parameter */
   double Omega_lambda;
 
-  /*! Radiation constant density parameter */
+  /*! Total radiation density parameter (photons and other relics) */
   double Omega_r;
 
-  /*! Neutrino density parameter */
+  /*! CMB radiation density parameter (Omega_gamma) */
+  double Omega_g;
+
+  /*! Massive neutrino density parameter */
   double Omega_nu;
 
+  /*! Massive neutrino density parameter at z=0 */
+  double Omega_nu_0;
+
+  /*! Ultra-relativistic species (e.g. massless neutrinos) density parameter */
+  double Omega_ur;
+
+  /*! Cold dark matter density parameter */
+  double Omega_cdm;
+
   /*! Curvature density parameter */
   double Omega_k;
 
@@ -159,12 +171,42 @@ struct cosmology {
   /*! Dark-energy evolution parameter */
   double w_a;
 
-  /*! CMB temperature at z = 0 derived from Omega_r (internal units) */
+  /*! CMB temperature at z = 0 implied by cosmology (internal units) */
   double T_CMB_0;
 
-  /*! CMB temperature at z = 0 derived from Omega_r (Kelvins) */
+  /*! CMB temperature at z = 0 implied by cosmology (Kelvins) */
   double T_CMB_0_K;
 
+  /*! Neutrino temperature at z = 0 (internal units) */
+  double T_nu_0;
+
+  /* Neutrino temperature at z = 0 (electron-volts) */
+  double T_nu_0_eV;
+
+  /*! Number of massive neutrino species */
+  int N_nu;
+
+  /*! Number of ultra-relativistic species (excluding massive neutrinos) */
+  double N_ur;
+
+  /*! Effective number of relativistic species (including massive neutrinos) */
+  double N_eff;
+
+  /*! Mass of each massive neutrino species in electron-volts */
+  double *M_nu_eV;
+
+  /*! Degeneracy of each massive neutrino species */
+  double *deg_nu;
+
+  /*! Log of starting expansion factor for neutrino interpolation tables */
+  double log_a_long_begin;
+
+  /*! Log of midpoint expansion factor for neutrino interpolation tables */
+  double log_a_long_mid;
+
+  /*! Log of ending expansion factor for neutrino interpolation tables */
+  double log_a_long_end;
+
   /*! Log of starting expansion factor */
   double log_a_begin;
 
@@ -189,6 +231,12 @@ struct cosmology {
   /*! Scale factor interpolation table */
   double *scale_factor_interp_table;
 
+  /*! Massive neutrino density interpolation table at early times */
+  double *neutrino_density_early_table;
+
+  /*! Massive neutrino density interpolation table at late times */
+  double *neutrino_density_late_table;
+
   /*! Time between Big Bang and first entry in the table */
   double time_interp_table_offset;
 
@@ -217,6 +265,7 @@ double cosmology_get_corr_kick_factor(const struct cosmology *cosmo,
 double cosmology_get_delta_time(const struct cosmology *c,
                                 const integertime_t ti_start,
                                 const integertime_t ti_end);
+double cosmology_get_neutrino_density(const struct cosmology *c, double a);
 
 double cosmology_get_delta_time_from_scale_factors(const struct cosmology *c,
                                                    const double a_start,
diff --git a/src/engine.c b/src/engine.c
index f2e5c3d4c6629a662488012dcbfa742f38c7b104..2a0492ee2c55abd0d572b9cce100dd3a84c20ac7 100644
--- a/src/engine.c
+++ b/src/engine.c
@@ -2932,7 +2932,7 @@ void engine_recompute_displacement_constraint(struct engine *e) {
   /* Get the cosmological information */
   const int with_cosmology = e->policy & engine_policy_cosmology;
   const struct cosmology *cosmo = e->cosmology;
-  const float Om = cosmo->Omega_m;
+  const float Ocdm = cosmo->Omega_cdm;
   const float Ob = cosmo->Omega_b;
   const float H0 = cosmo->H0;
   const float a = cosmo->a;
@@ -3015,7 +3015,7 @@ void engine_recompute_displacement_constraint(struct engine *e) {
       const float min_mass_dm = min_mass[1];
 
       /* Inter-particle sepration for the DM */
-      const float d_dm = cbrtf(min_mass_dm / ((Om - Ob) * rho_crit0));
+      const float d_dm = cbrtf(min_mass_dm / (Ocdm * rho_crit0));
 
       /* RMS peculiar motion for the DM */
       const float rms_vel_dm = vel_norm_dm / N_dm;
diff --git a/src/space.c b/src/space.c
index 52fbdeb96b9b861c1de23431397258bdddfa95c9..1b60d924742df427f57a9df8350deae953b41e54 100644
--- a/src/space.c
+++ b/src/space.c
@@ -1019,7 +1019,7 @@ void space_init(struct space *s, struct swift_params *params,
                 size_t Nspart, size_t Nbpart, int periodic, int replicate,
                 int remap_ids, int generate_gas_in_ics, int hydro,
                 int self_gravity, int star_formation, int DM_background,
-                int verbose, int dry_run, int nr_nodes) {
+                int neutrinos, int verbose, int dry_run, int nr_nodes) {
 
   /* Clean-up everything */
   bzero(s, sizeof(struct space));
@@ -1033,6 +1033,7 @@ void space_init(struct space *s, struct swift_params *params,
   s->with_hydro = hydro;
   s->with_star_formation = star_formation;
   s->with_DM_background = DM_background;
+  s->with_neutrinos = neutrinos;
   s->nr_parts = Npart;
   s->nr_gparts = Ngpart;
   s->nr_sparts = Nspart;
@@ -1855,13 +1856,21 @@ void space_check_cosmology(struct space *s, const struct cosmology *cosmo,
     const double rho_crit0 = cosmo->critical_density * H0 * H0 / (H * H);
 
     /* Compute the mass density */
-    const double Omega_m = (total_mass / volume) / rho_crit0;
+    const double Omega_sim = (total_mass / volume) / rho_crit0;
 
-    if (fabs(Omega_m - cosmo->Omega_m) > 1e-3)
+    /* The density required to match the cosmology */
+    double Omega_cosmo;
+    if (s->with_neutrinos)
+      Omega_cosmo = cosmo->Omega_m + cosmo->Omega_nu_0;
+    else
+      Omega_cosmo = cosmo->Omega_m;
+
+    if (fabs(Omega_sim - Omega_cosmo) > 1e-3)
       error(
           "The matter content of the simulation does not match the cosmology "
-          "in the parameter file cosmo.Omega_m=%e Omega_m=%e",
-          cosmo->Omega_m, Omega_m);
+          "in the parameter file cosmo.Omega_m=%e Omega_particles=%e. Are you"
+          "running with neutrinos? Then account for cosmo.Omega_nu_0=%e too.",
+          cosmo->Omega_m, Omega_sim, cosmo->Omega_nu_0);
   }
 }
 
diff --git a/src/space.h b/src/space.h
index c726a68d9873f7473c191f649fb3d7fa53b10400..47a8ffe153c3bc4da9101c71a9aa4fbde32bd6fd 100644
--- a/src/space.h
+++ b/src/space.h
@@ -111,6 +111,9 @@ struct space {
   /*! Are we running with some DM background particles? */
   int with_DM_background;
 
+  /*! Are we running with neutrino particles? */
+  int with_neutrinos;
+
   /*! Width of the top-level cells. */
   double width[3];
 
@@ -346,8 +349,8 @@ void space_init(struct space *s, struct swift_params *params,
                 struct bpart *bparts, size_t Npart, size_t Ngpart, size_t Nsink,
                 size_t Nspart, size_t Nbpart, int periodic, int replicate,
                 int remap_ids, int generate_gas_in_ics, int hydro, int gravity,
-                int star_formation, int DM_background, int verbose, int dry_run,
-                int nr_nodes);
+                int star_formation, int DM_background, int neutrinos,
+                int verbose, int dry_run, int nr_nodes);
 void space_sanitize(struct space *s);
 void space_map_cells_pre(struct space *s, int full,
                          void (*fun)(struct cell *c, void *data), void *data);
diff --git a/src/velociraptor_interface.c b/src/velociraptor_interface.c
index 231349e0d7a10d8920926e876fc09337e8633d63..aabb58906e626f03f4ede727b7462e261d18ea19 100644
--- a/src/velociraptor_interface.c
+++ b/src/velociraptor_interface.c
@@ -66,7 +66,7 @@ struct cosmoinfo {
   /*! Radiation density parameter (cosmology.Omega_r) */
   double Omega_r;
 
-  /*! Neutrino density parameter (0 in SWIFT) */
+  /*! Neutrino density parameter at z = 0 (cosmology.Omega_nu_0) */
   double Omega_nu;
 
   /*! Neutrino density parameter (cosmology.Omega_k) */
@@ -78,7 +78,7 @@ struct cosmoinfo {
   /*! Radiation constant density parameter (cosmology.Omega_lambda) */
   double Omega_Lambda;
 
-  /*! Dark matter density parameter (cosmology.Omega_m - cosmology.Omega_b) */
+  /*! Dark matter density parameter (cosmology.Omega_cdm) */
   double Omega_cdm;
 
   /*! Dark-energy equation of state at the current time (cosmology.w)*/
@@ -774,9 +774,9 @@ void velociraptor_invoke(struct engine *e, const int linked_with_snap) {
   cosmo_info.Omega_b = e->cosmology->Omega_b;
   cosmo_info.Omega_r = e->cosmology->Omega_r;
   cosmo_info.Omega_k = e->cosmology->Omega_k;
-  cosmo_info.Omega_nu = 0.;
+  cosmo_info.Omega_nu = e->cosmology->Omega_nu_0;
   cosmo_info.Omega_Lambda = e->cosmology->Omega_lambda;
-  cosmo_info.Omega_cdm = e->cosmology->Omega_m - e->cosmology->Omega_b;
+  cosmo_info.Omega_cdm = e->cosmology->Omega_cdm;
   cosmo_info.w_de = e->cosmology->w;
 
   /* Report the cosmo info we use */
@@ -787,6 +787,7 @@ void velociraptor_invoke(struct engine *e, const int linked_with_snap) {
     message("VELOCIraptor conf: Omega_b: %e", cosmo_info.Omega_b);
     message("VELOCIraptor conf: Omega_Lambda: %e", cosmo_info.Omega_Lambda);
     message("VELOCIraptor conf: Omega_cdm: %e", cosmo_info.Omega_cdm);
+    message("VELOCIraptor conf: Omega_nu: %e", cosmo_info.Omega_nu);
     message("VELOCIraptor conf: w_de: %e", cosmo_info.w_de);
   }
 
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 347de263b05836f2579739f3666833736b64968f..c1b3e975705d65a1de4f4a58e0f726e3df2b7522 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -30,7 +30,7 @@ TESTS = testGreetings testMaths testReading.sh testKernel testKernelLongGrav \
 	testPotentialPair testEOS testUtilities testSelectOutput.sh \
 	testCbrt testCosmology testOutputList testFormat.sh \
 	test27cellsStars.sh test27cellsStarsPerturbed.sh testHydroMPIrules \
-        testAtomic testGravitySpeed
+        testAtomic testGravitySpeed testNeutrinoCosmology
 
 # List of test programs to compile
 check_PROGRAMS = testGreetings testReading testTimeIntegration testKernelLongGrav \
@@ -43,7 +43,7 @@ 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
+                 testAtomic testHydroMPIrules testGravitySpeed testNeutrinoCosmology
 
 # Rebuild tests when SWIFT is updated.
 $(check_PROGRAMS): ../src/.libs/libswiftsim.a
diff --git a/tests/testFFT.c b/tests/testFFT.c
index 1654120b6ca6f059d2be07834ef336be877a86a3..415802d0da276bf7a5b1a7adf219d2d54702796e 100644
--- a/tests/testFFT.c
+++ b/tests/testFFT.c
@@ -84,7 +84,7 @@ int main(int argc, char *argv[]) {
   struct space space;
   double dim[3] = {1., 1., 1.};
   space_init(&space, params, &cosmo, dim, NULL, gparts, NULL, 0, nr_gparts, 0,
-             1, 1, 0, 1, 1, 0);
+             1, 1, 0, 0, 1, 1, 0);
 
   struct engine engine;
   engine.s = &space;
diff --git a/tests/testNeutrinoCosmology.c b/tests/testNeutrinoCosmology.c
new file mode 100644
index 0000000000000000000000000000000000000000..38b2db49bad9a60919630e94c8c0f4d5ef72a7f4
--- /dev/null
+++ b/tests/testNeutrinoCosmology.c
@@ -0,0 +1,264 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2019 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 "../config.h"
+
+/* Some standard headers */
+#include <fenv.h>
+#include <math.h>
+
+/* Includes. */
+#include "swift.h"
+
+#define N_CHECK 20
+#define TOLERANCE 1e-6
+#define EXTERNAL_TOLERANCE 1e-5  // comparing against CLASS
+
+void test_params_init(struct swift_params *params, int testnr) {
+  switch (testnr) {
+    /* One doubly degenerate light neutrino (total 0.10 eV) and one massless */
+    case 0:
+      parser_init("", params);
+      parser_set_param(params, "Cosmology:Omega_m:0.3122688651165");
+      parser_set_param(params, "Cosmology:Omega_lambda:6.853646e-01");
+      parser_set_param(params, "Cosmology:Omega_b:0.049198926683");
+      parser_set_param(params, "Cosmology:h:0.6737");
+      parser_set_param(params, "Cosmology:a_begin:1e-2");
+      parser_set_param(params, "Cosmology:a_end:1.0");
+      parser_set_param(params, "Cosmology:T_nu_0:1.95176");
+      parser_set_param(params, "Cosmology:N_nu:1");
+      parser_set_param(params, "Cosmology:N_ur:1.0196");
+      parser_set_param(params, "Cosmology:M_nu_eV:0.0486");
+      parser_set_param(params, "Cosmology:deg_nu:2");
+      break;
+    /* Three very massive neutrinos */
+    case 1:
+      parser_init("", params);
+      parser_set_param(params, "Cosmology:Omega_m:0.291188231748");
+      parser_set_param(params, "Cosmology:Omega_lambda:0.685365");
+      parser_set_param(params, "Cosmology:Omega_b:0.049199");
+      parser_set_param(params, "Cosmology:h:0.6737");
+      parser_set_param(params, "Cosmology:a_begin:1e-2");
+      parser_set_param(params, "Cosmology:a_end:1.0");
+      parser_set_param(params, "Cosmology:N_nu:3");
+      parser_set_param(params, "Cosmology:M_nu_eV:0.2,0.3,0.4");
+      break;
+    /* One light massive neutrino (0.05 eV) + two massless */
+    case 2:
+      parser_init("", params);
+      parser_set_param(params, "Cosmology:Omega_m:0.3110861814103");
+      parser_set_param(params, "Cosmology:Omega_lambda:0.685365");
+      parser_set_param(params, "Cosmology:Omega_b:0.049199");
+      parser_set_param(params, "Cosmology:h:0.6737");
+      parser_set_param(params, "Cosmology:a_begin:1e-2");
+      parser_set_param(params, "Cosmology:a_end:1.0");
+      parser_set_param(params, "Cosmology:T_nu_0:1.95176");
+      parser_set_param(params, "Cosmology:N_ur:2");
+      parser_set_param(params, "Cosmology:N_nu:1");
+      parser_set_param(params, "Cosmology:M_nu_eV:0.05");
+      break;
+    /* Three very massive neutrinos (same as 1), but starting earlier */
+    case 3:
+      parser_init("", params);
+      parser_set_param(params, "Cosmology:Omega_m:0.291188231748");
+      parser_set_param(params, "Cosmology:Omega_lambda:0.685365");
+      parser_set_param(params, "Cosmology:Omega_b:0.049199");
+      parser_set_param(params, "Cosmology:h:0.6737");
+      parser_set_param(params, "Cosmology:a_begin:1e-3");
+      parser_set_param(params, "Cosmology:a_end:1.0");
+      parser_set_param(params, "Cosmology:N_nu:3");
+      parser_set_param(params, "Cosmology:M_nu_eV:0.2,0.3,0.4");
+      break;
+  }
+
+  parser_set_param(params, "InternalUnitSystem:UnitMass_in_cgs:1.988435e+43");
+  parser_set_param(params,
+                   "InternalUnitSystem:UnitLength_in_cgs:3.0856775815e+24");
+  parser_set_param(params,
+                   "InternalUnitSystem:UnitVelocity_in_cgs:9.7846194238e+07");
+  parser_set_param(params, "InternalUnitSystem:UnitCurrent_in_cgs:1");
+  parser_set_param(params, "InternalUnitSystem:UnitTemp_in_cgs:1");
+}
+
+int main(int argc, char *argv[]) {
+
+  /* Load a CLASS data table of background quantities for cosmology 0 */
+  const int rows = 51;
+  const int cols = 3;
+  double CLASS_table[rows * cols];
+  FILE *stream = fopen("testNeutrinoCosmology.dat", "r");
+  char line[1024];
+  int row = 0;
+  while (fgets(line, 1024, stream)) {
+    if (line[0] == '#') continue;
+    char *tmp = strdup(line);
+    char *ptr = NULL;
+    CLASS_table[0 + row * 3] = strtod(tmp, &ptr);
+    CLASS_table[1 + row * 3] = strtod(ptr + 1, &ptr);
+    CLASS_table[2 + row * 3] = strtod(ptr + 1, &ptr);
+    row++;
+    free(tmp);
+  }
+  fclose(stream);
+
+  /* Initialize CPU frequency, this also starts time. */
+  unsigned long long cpufreq = 0;
+  clocks_set_cpufreq(cpufreq);
+
+  /* Test a number of different cosmologies */
+  int N_cosmos = 4;
+  double times1[N_CHECK];  // compare t(a) for cosmology 1
+  double times2[N_CHECK];  // with t(a) for cosmology 3
+  for (int testnr = 0; testnr < N_cosmos; testnr++) {
+    message("Initialization of cosmology %i", testnr);
+
+    /* pseudo initialization of params */
+    struct swift_params params;
+    test_params_init(&params, testnr);
+
+    /* initialization of unit system */
+    struct unit_system us;
+    units_init_from_params(&us, &params, "InternalUnitSystem");
+
+    /* initialization of phys_const */
+    struct phys_const phys_const;
+    phys_const_init(&us, &params, &phys_const);
+
+    /* initialization of cosmo */
+    struct cosmology cosmo;
+    cosmology_init(&params, &us, &phys_const, &cosmo);
+
+    message("Start checking computation...");
+
+    for (int i = 0; i < N_CHECK; i++) {
+      double a = 0.1 + 0.9 * i / (N_CHECK - 1.);
+      /* Compute a(t(a)) and check if same results */
+      double time = cosmology_get_time_since_big_bang(&cosmo, a);
+
+      /* Store the value for cosmologies 1 and 3 to compare later */
+      if (testnr == 1) {
+        times1[i] = time;
+      } else if (testnr == 3) {
+        times2[i] = time;
+      }
+
+      double my_a = cosmology_get_scale_factor(&cosmo, time);
+
+      /* check accuracy */
+      double rel_err = (my_a - a) / a;
+      message("Accuracy of %g at a=%g", rel_err, a);
+      assert(fabs(rel_err) < TOLERANCE);
+    }
+
+    if (testnr == 0) {
+      /* For cosmology 0, compare against independently computed array */
+      message("Relative to CLASS values (a, t, Omega_nu(a))");
+      const double delta_a = (cosmo.log_a_end - cosmo.log_a_begin) / 10000;
+      for (int j = 0; j < 50; j++) {
+        double a1 = exp(cosmo.log_a_begin + delta_a * (j * 200 + 1));
+        double time1 = cosmology_get_time_since_big_bang(&cosmo, a1);
+        // double time1 = cosmo.time_interp_table[j*200] +
+        // cosmo.time_interp_table_offset;
+        double Onu1 = cosmology_get_neutrino_density(&cosmo, a1);
+
+        double a2 = CLASS_table[0 + 3 * j];
+        double time2 = CLASS_table[1 + 3 * j];
+        double Onu2 = CLASS_table[2 + 3 * j];
+
+        /* Class defines years as 365.25 days, we use 365 days. */
+        time2 *= 365.25 / 365.0;
+
+        assert(fabs(a1 - a2) / (a1 + a2) < EXTERNAL_TOLERANCE);
+        assert(fabs(time1 - time2) / (time1 + time2) < EXTERNAL_TOLERANCE);
+        assert(fabs(Onu1 - Onu2) / (Onu1 + Onu2) < EXTERNAL_TOLERANCE);
+
+        message("At a = %e: (%e, %e, %e)", a1, a1 / a2, time1 / time2,
+                Onu1 / Onu2);
+      }
+    }
+
+    message("Everything seems fine with this cosmology.");
+
+    cosmology_clean(&cosmo);
+  }
+
+  /* Compare cosmologies 0 and 3 */
+  for (int j = 0; j < N_CHECK; j++) {
+    /* check accuracy */
+    double err = (times1[j] - times2[j]) / times1[j];
+    message("Agreement of %g at step %i", err, j);
+    assert(fabs(err) < TOLERANCE);
+  }
+
+  /* For the massive neutrino cosmology 3, check that it reproduces
+   *  the relativistic (early) and non-relativistic (late) limits */
+
+  message("Initialization...");
+
+  /* pseudo initialization of params */
+  struct swift_params params;
+  test_params_init(&params, 3);
+
+  /* initialization of unit system */
+  struct unit_system us;
+  units_init_cgs(&us);
+
+  /* initialization of phys_const */
+  struct phys_const phys_const;
+  phys_const_init(&us, &params, &phys_const);
+
+  /* initialization of cosmo */
+  struct cosmology cosmo;
+  cosmology_init(&params, &us, &phys_const, &cosmo);
+
+  message("Start checking the fermion integration...");
+
+  /* Relativistic limit */
+  double Omega_nu_early = cosmology_get_neutrino_density(&cosmo, 1e-10);
+  double Omega_nu_r =
+      cosmo.Omega_g * cosmo.N_eff * (7. / 8.) * pow(4. / 11., 4. / 3.);
+  double err = (Omega_nu_early - Omega_nu_r) / Omega_nu_early;
+  message("Accuracy of %g of the relativistic limit", err);
+  assert(fabs(err) < TOLERANCE);
+
+  /* Zeta function and other constants */
+  const double zeta3 = 1.202056903159594;
+  const double kb = phys_const.const_boltzmann_k;
+  const double hbar = phys_const.const_planck_h / (2 * M_PI);
+  const double cvel = phys_const.const_speed_light_c;
+  const double eV = phys_const.const_electron_volt;
+
+  /* Non-relativistic limit (limit not reached, so lower tolerance is fine)*/
+  double Omega_nu_late = cosmology_get_neutrino_density(&cosmo, 1.0);
+  double Omega_nu_nr = 0.;
+  for (int i = 0; i < cosmo.N_nu; i++) {
+    Omega_nu_nr += 6 * zeta3 / (11 * M_PI * M_PI) * pow(kb * cosmo.T_CMB_0, 3) /
+                   pow(cvel * hbar, 3) * cosmo.M_nu_eV[i] * eV /
+                   (cosmo.critical_density_0 * cvel * cvel);
+  }
+  double err2 = (Omega_nu_late - Omega_nu_nr) / Omega_nu_late;
+  message("Accuracy of %g of the non-relativistic limit", err2);
+  assert(fabs(err2) < 1e-5);
+
+  message("Neutrino cosmology test successful.");
+
+  cosmology_clean(&cosmo);
+  return 0;
+}
diff --git a/tests/testNeutrinoCosmology.dat b/tests/testNeutrinoCosmology.dat
new file mode 100644
index 0000000000000000000000000000000000000000..351cd2756a246da2ffcd44ccd68917b1eaeef39a
--- /dev/null
+++ b/tests/testNeutrinoCosmology.dat
@@ -0,0 +1,53 @@
+#Computed with CLASS v. 2.9
+#a,proper_time_Gpc,O_nu
+0.01000461,0.016701397553317,3.5163787160972E-05
+0.01096983,0.019228996526818,3.67516040528256E-05
+0.01202818,0.02213429919907,3.85569090708168E-05
+0.01318864,0.025473378968706,4.06036826343241E-05
+0.01446106,0.0293106271508,4.29178039616624E-05
+0.01585623,0.033720161738937,4.55272171628141E-05
+0.01738601,0.038786778537091,4.84619440279236E-05
+0.01906338,0.044607929499909,5.17543596186455E-05
+0.02090259,0.051295577208859,5.54394196800272E-05
+0.02291923,0.058978141111643,5.95548023375683E-05
+0.02513043,0.067803393711092,6.41413614902226E-05
+0.02755497,0.077940926147672,6.92433337546202E-05
+0.03021343,0.08958501664697,7.49086656511501E-05
+0.03312836,0.102958902483026,8.11895180833707E-05
+0.03632453,0.118319082448388,8.8142821279131E-05
+0.03982905,0.135959789513129,9.58306007331672E-05
+0.04367169,0.156219964076042,0.000104320794266
+0.04788506,0.17948690789494,0.000113687563152
+0.05250492,0.206205809197034,0.000124012192172
+0.0575705,0.236887879351767,0.00013538382664
+0.0631248,0.272119744284891,0.000147900195733
+0.06921496,0.312575279195446,0.000161668510521
+0.0758927,0.359027968989762,0.000176806479732
+0.08321469,0.412362655983059,0.000193443160009
+0.09124309,0.473595514035587,0.000211720276868
+0.1000461,0.543891750068211,0.00023179347611
+0.1096983,0.62458472301863,0.000253833120516
+0.1202818,0.717207770565306,0.000278026953631
+0.1318864,0.823506701592335,0.00030458041981
+0.1446106,0.945478283370435,0.000333719041739
+0.1585623,1.0854002866677,0.000365690020889
+0.1738601,1.24586901225677,0.000400765218598
+0.1906338,1.42983264427013,0.000439242312846
+0.2090259,1.64063166511213,0.000481448238577
+0.2291923,1.88201267212233,0.00052774077823
+0.2513043,2.15817979552822,0.000578513184864
+0.2755497,2.47380033173388,0.000634196697354
+0.3021343,2.83399860301076,0.000695263986152
+0.3312836,3.24433010205929,0.000762233064932
+0.3632453,3.71068357665048,0.000835673017592
+0.3982905,4.23914128398719,0.000916206768316
+0.4367169,4.83578207787208,0.00100451843668
+0.4788506,5.50636437013827,0.001101357471308
+0.5250492,6.25594667098269,0.001207545774545
+0.575705,7.08845062088615,0.001323985061712
+0.631248,8.00618086446026,0.001451663744548
+0.6921496,9.00947265502665,0.001591665447889
+0.758927,10.0964700596076,0.001745179121787
+0.8321469,11.2631241396981,0.001913507546324
+0.9124309,12.503470676376,0.002098079745641
+1,13.80342381474,0.002299403771466
diff --git a/theory/Cosmology/bibliography.bib b/theory/Cosmology/bibliography.bib
index 84cec263d2e8195bc831e672184b41d61479fcc2..7467e64a99e1a497f3c550f67b47e0f4a41a37e3 100644
--- a/theory/Cosmology/bibliography.bib
+++ b/theory/Cosmology/bibliography.bib
@@ -176,3 +176,61 @@ archivePrefix = "arXiv",
    adsurl = {http://adsabs.harvard.edu/abs/2010MNRAS.401..791S},
   adsnote = {Provided by the SAO/NASA Astrophysics Data System}
 }
+
+@article{Zennaro2016,
+  title={Initial conditions for accurate N-body simulations of massive neutrino cosmologies},
+  author={Zennaro, Matteo and Bel, Julien and Villaescusa-Navarro, Francisco and Carbone, Carmelita and Sefusatti, Emiliano and Guzzo, Luigi},
+  journal={Monthly Notices of the Royal Astronomical Society},
+  volume={466},
+  number={3},
+  pages={3244--3258},
+  year={2016},
+  publisher={Oxford University Press}
+}
+
+@article{Elbers2020,
+    author = "Elbers, Willem and Frenk, Carlos S. and Jenkins, Adrian and Li, Baojiu and Pascoli, Silvia",
+    title = "{An optimal nonlinear method for simulating relic neutrinos}",
+    eprint = "2010.07321",
+    archivePrefix = "arXiv",
+    primaryClass = "astro-ph.CO",
+    month = "10",
+    year = "2020"
+}
+
+@ARTICLE{Mangano2005,
+       author = {{Mangano}, Gianpiero and {Miele}, Gennaro and {Pastor}, Sergio and {Pinto}, Teguayco and {Pisanti}, Ofelia and {Serpico}, Pasquale D.},
+        title = "{Relic neutrino decoupling including flavour oscillations}",
+      journal = {Nuclear Physics B},
+     keywords = {High Energy Physics - Phenomenology, Astrophysics},
+         year = 2005,
+        month = nov,
+       volume = {729},
+       number = {1-2},
+        pages = {221-234},
+          doi = {10.1016/j.nuclphysb.2005.09.041},
+archivePrefix = {arXiv},
+       eprint = {hep-ph/0506164},
+ primaryClass = {hep-ph},
+       adsurl = {https://ui.adsabs.harvard.edu/abs/2005NuPhB.729..221M},
+      adsnote = {Provided by the SAO/NASA Astrophysics Data System}
+}
+
+@ARTICLE{Lesgourgues2011,
+       author = {{Lesgourgues}, Julien and {Tram}, Thomas},
+        title = "{The Cosmic Linear Anisotropy Solving System (CLASS) IV: efficient implementation of non-cold relics}",
+      journal = {\jcap},
+     keywords = {Astrophysics - Cosmology and Nongalactic Astrophysics},
+         year = 2011,
+        month = sep,
+       volume = {2011},
+       number = {9},
+          eid = {032},
+        pages = {032},
+          doi = {10.1088/1475-7516/2011/09/032},
+archivePrefix = {arXiv},
+       eprint = {1104.2935},
+ primaryClass = {astro-ph.CO},
+       adsurl = {https://ui.adsabs.harvard.edu/abs/2011JCAP...09..032L},
+      adsnote = {Provided by the SAO/NASA Astrophysics Data System}
+}
diff --git a/theory/Cosmology/cosmology_standalone.tex b/theory/Cosmology/cosmology_standalone.tex
index 5b5fa228fe4cd1c5cfbd64a5ddb7a7ec466fa7a7..72b802761fad0c87fc12f5895a4f6c887af294bd 100644
--- a/theory/Cosmology/cosmology_standalone.tex
+++ b/theory/Cosmology/cosmology_standalone.tex
@@ -37,6 +37,8 @@ Making cosmology great again.
 
 \input{flrw}
 
+\input{neutrinos}
+
 \input{coordinates}
 
 \input{operators}
diff --git a/theory/Cosmology/flrw.tex b/theory/Cosmology/flrw.tex
index 875d4a4cc3eb8057f62003f90d30432ad879d4dc..d6e8cfcda6ee5eebf53d623afdae00118c47fa2a 100644
--- a/theory/Cosmology/flrw.tex
+++ b/theory/Cosmology/flrw.tex
@@ -19,7 +19,7 @@ and write
 \begin{align}
 H(a) &\equiv H_0 E(a) \\ E(a) &\equiv\sqrt{\Omega_m a^{-3} + \Omega_r
   a^{-4} + \Omega_k a^{-2} + \Omega_\Lambda \exp\left(3\tilde{w}(a)\right)},
-\\
+  \label{eq:Ea} \\
 \tilde{w}(a) &= (a-1)w_a - (1+w_0 + w_a)\log\left(a\right),
 \label{eq:friedmann}
 \end{align}
diff --git a/theory/Cosmology/neutrinos.tex b/theory/Cosmology/neutrinos.tex
new file mode 100644
index 0000000000000000000000000000000000000000..b9a5330f45e4763b115f4b7574ecaf598ee98830
--- /dev/null
+++ b/theory/Cosmology/neutrinos.tex
@@ -0,0 +1,36 @@
+% Willem Elbers, 2019
+
+\subsection{Massive neutrinos}
+
+The non-relativistic transition of massive neutrinos occurs at $a^{-1}\approx 1890 (m_\nu/1\text{ eV})$, which changes the background evolution, affecting the Hubble rate $E(a)$ and therefore most integrated quantities described in this document. Users can include this effect by specifying the number of massive neutrino species $N_\nu$ and their nonzero neutrino masses $m_{\nu,i}$ in eV $(m_{\nu,i}\neq 0, i=1,\dots,N_\nu)$ and (optional) degeneracies $g_i$. In addition, users have the option to specify the present-day neutrino temperature $T_{\nu,0}$\footnote{To match the neutrino density from an accurate calculation of decoupling \citep{Mangano2005}, one can use the value $T_{\nu,0}/T_{\mathrm{CMB},0}=0.71599$ \citep{Lesgourgues2011}.} and and an effective number of ultra-relativistic (massless) species $N_\mathrm{ur}$. Together with the present-day CMB temperature $T_{\mathrm{CMB},0}$, these parameters are used to compute the photon density $\Omega_\gamma$, the ultra-relativistic species density $\Omega_\mathrm{ur}$, and the massive neutrino density $\Omega_\nu(a)$, replacing the total radiation density parameter $\Omega_r$.
+
+In our conventions, the massive neutrino contribution at $a=1$ is \emph{not} included in the present-day matter density $\Omega_m=\Omega_c+\Omega_b$. The radiation term appearing in (\ref{eq:Ea}) is simply replaced by
+\begin{align}
+    \Omega_r a^{-4} &= \left[\Omega_\gamma + \Omega_\mathrm{ur} + \Omega_\nu(a)\right] a^{-4}.
+\end{align}
+The CMB density is constant and given by
+%
+\begin{align}
+    \Omega_\gamma &= \frac{\pi^2}{15}\frac{(k_b T_\text{CMB,0})^4}{(\hbar c)^3}\frac{1}{\rho_{\rm crit}c^2},
+\end{align}
+The ultra-relativistic density is given by
+\begin{align}
+    \Omega_\mathrm{ur} = \frac{7}{8}\left(\frac{4}{11}\right)^{4/3} N_\mathrm{ur}\,\Omega_\gamma.
+\end{align}
+Note that we assume instantaneous decoupling for the ultra-relativistic species. The time-dependent massive neutrino density parameter is \citep{Zennaro2016}
+\begin{align}
+    \Omega_\nu(a) = \Omega_\gamma \sum_{i=1}^{N_\nu}\frac{15}{\pi^4}g_i\left(\frac{T_{\nu,0}}{T_\text{CMB}}\right)^4 \mathcal{F}\left(\frac{a m_{\nu,i}}{k_b T_{\nu,0}}\right), \label{eq:nudensity}
+\end{align}
+where the function $\mathcal{F}$ is given by the momentum integral
+%
+\begin{align}
+    \mathcal{F}(y) = \int_0^{\infty} \frac{x^2\sqrt{x^2+y^2}}{1+e^{x}}\mathrm{d}x.
+\end{align}
+As $\Omega_\nu(a)$ is needed to compute other cosmological integrals, this function should be calculated with greater accuracy. At the start of the simulation, values of (\ref{eq:nudensity}) are tabulated on a piecewise linear grid of $2\times3\times10^4$ values of $a$ spaced between $\log(a_{\nu,\text{begin}})$, $\log(a_{\nu,\text{mid}})$, and $\log(a_{\nu,\text{end}}) = \log(1)=0$. The value of $a_{\nu,\text{begin}}$ is automatically chosen such that the neutrinos are still relativistic at the start of the table. The value of $\log(a_{\nu,\text{mid}})$ is chosen just before the start of the simulation. The integrals $\mathcal{F}(y)$ are evaluated using the 61-points Gauss-Konrod rule implemented in the {\sc gsl} library with a relative error limit of $\epsilon=10^{-13}$. Tabulated values are then linearly interpolated whenever $E(a)$ is computed.
+
+For completeness, we also compute the effective number of relativistic degrees of freedom at high redshift:
+\begin{align}
+    N_\text{eff} = N_\mathrm{ur} + N_\nu\left(\frac{11}{4}\right)^{4/3}\left(\frac{T_{\nu,0}}{T_\text{CMB,0}}\right)^4.
+\end{align}
+
+Neutrino effects also play a role at the perturbation level. These can be included in SWIFT using the fully non-linear $\delta f$ method \citep{Elbers2020}.