/*******************************************************************************
* 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 .
*
******************************************************************************/
/* This object's header. */
#include "neutrino.h"
/* Standard headers */
#include
/* Local includes */
#include "lightcone/lightcone.h"
#include "lightcone/lightcone_map_types.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(const float v[3], const double m_eV,
const 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 Gather neutrino constants
*
* @param s The #space for this run.
* @param nm Struct with neutrino constants
*/
void gather_neutrino_consts(const struct space *s, struct neutrino_model *nm) {
nm->use_delta_f_mesh_only = s->e->neutrino_properties->use_delta_f_mesh_only;
nm->M_nu_eV = s->e->cosmology->M_nu_eV;
nm->deg_nu = s->e->cosmology->deg_nu;
nm->N_nu = s->e->cosmology->N_nu;
nm->fac = 1.0 / (s->e->physical_constants->const_speed_light_c *
s->e->cosmology->T_nu_0_eV);
nm->inv_mass_factor = 1. / s->e->neutrino_mass_conversion_factor;
nm->neutrino_seed = s->e->neutrino_properties->neutrino_seed;
}
/**
* @brief Compute delta-f weight of a neutrino particle, but *only* when using
* the delta-f method exclusively on the mesh (otherwise the mass is already
* weighted).
*
* @param gp The #gpart.
* @param nm Properties of the neutrino model
* @param weight The resulting weight (output)
*/
void gpart_neutrino_weight_mesh_only(const struct gpart *gp,
const struct neutrino_model *nm,
double *weight) {
/* Anything to do? */
if (!nm->use_delta_f_mesh_only) return;
/* Use a particle id dependent seed */
const long long seed = gp->id_or_neg_offset + nm->neutrino_seed;
/* Compute the initial dimensionless momentum from the seed */
const double pi = neutrino_seed_to_fermi_dirac(seed);
/* The neutrino mass and degeneracy (we cycle based on the seed) */
const double m_eV = neutrino_seed_to_mass(nm->N_nu, nm->M_nu_eV, seed);
/* Compute the current dimensionless momentum */
double p = neutrino_momentum(gp->v_full, m_eV, nm->fac);
/* Compute the initial and current background phase-space density */
double fi = fermi_dirac_density(pi);
double f = fermi_dirac_density(p);
*weight = 1.0 - f / fi;
}
/**
* @brief Compute the mass and delta-f weight of a neutrino particle
*
* @param gp The #gpart.
* @param nm Properties of the neutrino model
* @param mass The mass (output)
* @param weight The resulting weight (output)
*/
void gpart_neutrino_mass_weight(const struct gpart *gp,
const struct neutrino_model *nm, double *mass,
double *weight) {
/* Use a particle id dependent seed */
const long long seed = gp->id_or_neg_offset + nm->neutrino_seed;
/* Compute the initial dimensionless momentum from the seed */
const double pi = neutrino_seed_to_fermi_dirac(seed);
/* The neutrino mass and degeneracy (we cycle based on the seed) */
const double m_eV = neutrino_seed_to_mass(nm->N_nu, nm->M_nu_eV, seed);
const double deg = neutrino_seed_to_degeneracy(nm->N_nu, nm->deg_nu, seed);
*mass = deg * m_eV * nm->inv_mass_factor;
/* Compute the current dimensionless momentum */
const double p = neutrino_momentum(gp->v_full, m_eV, nm->fac);
/* Compute the initial and current background phase-space density */
const double fi = fermi_dirac_density(pi);
const double f = fermi_dirac_density(p);
*weight = 1.0 - f / fi;
}
/**
* @brief Compute diagnostics for the neutrino delta-f method, including
* the mean squared weight.
*
* @param s The #space.
* @param cosmo The current cosmology model.
* @param physical_constants The #phys_const used for this run.
* @param neutrino_props The #neutrino_props used for this run.
* @param rank The MPI rank of this #space.
* @param r Output: correlation coefficient between current and sampled momenta
* @param I_df Output: half the mean squared weight
* @param mass_tot Output: the total mass in neutrino particles
*/
void compute_neutrino_diagnostics(
const struct space *s, const struct cosmology *cosmo,
const struct phys_const *physical_constants,
const struct neutrino_props *neutrino_properties, const int rank, double *r,
double *I_df, double *mass_tot) {
int use_df = neutrino_properties->use_delta_f;
int use_df_mesh = neutrino_properties->use_delta_f_mesh_only;
if (!use_df && !use_df_mesh) {
error("Neutrino diagnostics only defined when using the delta-f method.");
}
struct gpart *gparts = s->gparts;
const size_t nr_gparts = s->nr_gparts;
/* Retrieve physical and cosmological constants */
const double c_vel = physical_constants->const_speed_light_c;
const double *m_eV_array = cosmo->M_nu_eV;
const double *deg_array = cosmo->deg_nu;
const int N_nu = cosmo->N_nu;
const double T_eV = cosmo->T_nu_0_eV;
const double fac = 1.0 / (c_vel * T_eV);
const double inv_mass_factor = 1. / s->e->neutrino_mass_conversion_factor;
const long long total_nr_neutrinos = s->e->total_nr_neutrino_gparts;
const long long neutrino_seed = neutrino_properties->neutrino_seed;
/* Sum up the masses, weights, and momenta for the neutrinos in this space */
double mass_sum = 0;
double weight2_sum = 0;
double p_sum = 0; // current momenta
double p2_sum = 0;
double pi_sum = 0; // sampled momenta
double pi2_sum = 0;
double ppi_sum = 0;
for (size_t i = 0; i < nr_gparts; ++i) {
/* Skip extra and non-neutrino particles */
if (gparts[i].time_bin == time_bin_not_created) continue;
if (gparts[i].type != swift_type_neutrino) continue;
/* Use a particle id dependent seed */
const long long seed = gparts[i].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 and degeneracy (we cycle based on the seed) */
const double m_eV = neutrino_seed_to_mass(N_nu, m_eV_array, seed);
const double deg = neutrino_seed_to_degeneracy(N_nu, deg_array, seed);
const double mass = deg * m_eV * inv_mass_factor;
/* Compute the current dimensionless momentum */
double v2 = gparts[i].v_full[0] * gparts[i].v_full[0] +
gparts[i].v_full[1] * gparts[i].v_full[1] +
gparts[i].v_full[2] * gparts[i].v_full[2];
double p = sqrt(v2) * 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;
p_sum += p;
p2_sum += p * p;
pi_sum += pi;
pi2_sum += pi * pi;
ppi_sum += p * pi;
mass_sum += mass;
weight2_sum += weight * weight;
}
/* Reduce the total mass, weights, and momenta */
#ifdef WITH_MPI
double sums[7] = {p_sum, p2_sum, pi_sum, pi2_sum,
ppi_sum, mass_sum, weight2_sum};
double total_sums[7];
MPI_Reduce(sums, total_sums, 7, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
double total_p = total_sums[0];
double total_p2 = total_sums[1];
double total_pi = total_sums[2];
double total_pi2 = total_sums[3];
double total_ppi = total_sums[4];
double total_mass = total_sums[5];
double total_weight2 = total_sums[6];
#else
double total_p = p_sum;
double total_p2 = p2_sum;
double total_pi = pi_sum;
double total_pi2 = pi2_sum;
double total_ppi = ppi_sum;
double total_mass = mass_sum;
double total_weight2 = weight2_sum;
#endif
if (rank == 0) {
/* Compute the correlation coefficient */
double Sp = total_nr_neutrinos * total_p2 - total_p * total_p;
double Spi = total_nr_neutrinos * total_pi2 - total_pi * total_pi;
double Sppi = sqrt(Sp * Spi);
*r = (total_nr_neutrinos * total_ppi - total_p * total_pi) / Sppi;
/* Half the mean squared weight */
*I_df = 0.5 * total_weight2 / total_nr_neutrinos;
/* The total mass in neutrino particles */
*mass_tot = total_mass;
}
}
/**
* @brief Verify that the neutrino content matches the cosmological model
* and has been correctly loaded, when using the delta-f method.
*
* @param s The #space.
* @param cosmo The current cosmology model.
* @param physical_constants The #phys_const used for this run.
* @param params The parsed parameters.
* @param neutrino_props The #neutrino_props used for this run.
* @param rank The MPI rank of this #space.
* @param with_neutrinos Are we running with neutrino particles?
* @param verbose Are we verbose?
*/
void neutrino_check_cosmology(const struct space *s,
const struct cosmology *cosmo,
const struct phys_const *physical_constants,
struct swift_params *params,
const struct neutrino_props *neutrino_props,
const int rank, const int verbose) {
/* Check that we have neutrino particles if and only if we need them */
int use_df = neutrino_props->use_delta_f;
int use_df_mesh = neutrino_props->use_delta_f_mesh_only;
int use_linres = neutrino_props->use_linear_response;
int use_none = neutrino_props->use_model_none;
int genics = neutrino_props->generate_ics;
int with_neutrinos = s->with_neutrinos;
if ((use_df || use_df_mesh || genics) && !with_neutrinos) {
error(
"Running without neutrino particles, but specified a neutrino "
"model that requires them.");
} else if ((use_linres || use_none) && with_neutrinos) {
error(
"Running with neutrino particles, but specified a neutrino "
"model that is incompatible with particles.");
} else if (cosmo->Omega_nu_0 > 0. && !(use_linres || use_none) &&
!with_neutrinos) {
error(
"Running without neutrino particles, but specified neutrinos "
"in the background cosmology and not using a neutrino model that does "
"not use particles.");
}
/* We are done if the delta-f method is not used, since the total mass
* has otherwise already been checked in space_check_cosmology. */
if (!use_df && !use_df_mesh) return;
/* Compute neutrino diagnostics, including the total mass */
double r, I_df, total_mass;
compute_neutrino_diagnostics(s, cosmo, physical_constants, neutrino_props,
rank, &r, &I_df, &total_mass);
if (rank == 0) {
/* Check the correlation coefficient */
if (r < 0.1)
error(
"There is no correlation between current and sampled neutrino "
"momenta (r = %e, I = %e). Most likely, the neutrino seed is "
"incorrect or the particle IDs have been scrambled.",
r, I_df);
/* Check the mean squared weight */
else if (I_df > 0.1)
error(
"The neutrino particle weights are very large (r = %e, I = %e). "
"Most likely, the particle velocities are incorrectly normalised.",
r, I_df);
if (verbose) message("Neutrino delta-f diagnostic: I = %e", I_df);
/* Check the neutrino mass */
const double volume = s->dim[0] * s->dim[1] * s->dim[2];
/* Current Hubble constant */
const double H = cosmo->H;
/* z=0 Hubble parameter */
const double H0 = cosmo->H0;
/* Critical density at z=0 */
const double rho_crit0 = cosmo->critical_density * H0 * H0 / (H * H);
/* Density in neutrino particles */
const double Omega_particles_nu = (total_mass / volume) / rho_crit0;
if (fabs(Omega_particles_nu - cosmo->Omega_nu_0) > 1e-4)
error(
"The massive neutrino content of the simulation does not match the "
"cosmology in the parameter file: cosmo.Omega_nu = %e particles "
"Omega_nu = %e",
cosmo->Omega_nu_0, Omega_particles_nu);
}
}
/*
Lightcone map of neutrino mass perturbation
*/
/**
* @brief Determine if a particle type contributes to this map type
*
* @param part_type the particle type
*/
int lightcone_map_neutrino_mass_type_contributes(int ptype) {
switch (ptype) {
case swift_type_neutrino:
return 1;
default:
return 0;
}
}
/**
* @brief Make a healpix map of the neutrino mass perturbation
*
* When a neutrino particle crosses the lightcone this function
* should return the value to accumulate to the corresponding
* pixel in the healpix map.
*
* @param e the #engine structure
* @param lightcone_props properties of the lightcone to update
* @param gp the #gpart to add to the map
* @param a_cross expansion factor at which the particle crosses the lightcone
* @param x_cross comoving coordinates at which the particle crosses the
* lightcone
*/
double lightcone_map_neutrino_mass_get_value(
const struct engine *e, const struct lightcone_props *lightcone_props,
const struct gpart *gp, const double a_cross, const double x_cross[3]) {
switch (gp->type) {
case swift_type_neutrino: {
struct neutrino_model nu_model;
bzero(&nu_model, sizeof(struct neutrino_model));
if (e->neutrino_properties->use_delta_f_mesh_only)
gather_neutrino_consts(e->s, &nu_model);
double weight = 1.0;
gpart_neutrino_weight_mesh_only(gp, &nu_model, &weight);
return gp->mass * weight;
} break;
default:
error("lightcone map function called on wrong particle type");
return -1.0; /* Prevent 'missing return' error */
}
}
/**
* @brief Return baseline value for neutrino mass lightcone maps.
*
* This is the mean neutrino density integrated over the volume of the pixel.
*
* @param e the #engine structure
* @param lightcone_props properties of the lightcone to update
* @param map The lightcone map
*/
double lightcone_map_neutrino_baseline_value(
const struct cosmology *c, const struct lightcone_props *lightcone_props,
const struct lightcone_map *map) {
/* Fetch the area of healpix pixels */
const double area = lightcone_props->pixel_area_steradians;
/* Fetch the inner and outer radii */
const double r_inner = map->r_min;
const double r_outer = map->r_max;
const double r_inner_3 = r_inner * r_inner * r_inner;
const double r_outer_3 = r_outer * r_outer * r_outer;
/* The volume mapped into a healpix pixel */
const double volume = area * (r_outer_3 - r_inner_3) / 3.0;
/* The mean comoving neutrino density at z = 0 */
const double rho_nu_0 = c->critical_density_0 * c->Omega_nu_0;
return rho_nu_0 * volume;
}