/******************************************************************************* * This file is part of SWIFT. * Copyright (c) 2016 Matthieu Schaller (schaller@strw.leidenuniv.nl) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * ******************************************************************************/ #ifndef SWIFT_EAGLE_BLACK_HOLES_H #define SWIFT_EAGLE_BLACK_HOLES_H /* Local includes */ #include "black_holes_properties.h" #include "black_holes_struct.h" #include "cooling.h" #include "cooling/PS2020/cooling_tables.h" #include "cosmology.h" #include "dimension.h" #include "exp10.h" #include "gravity.h" #include "kernel_hydro.h" #include "minmax.h" #include "physical_constants.h" #include "random.h" #include "rays.h" /* Standard includes */ #include #include /** * @brief Computes the time-step of a given black hole particle. * * @param bp Pointer to the s-particle data. * @param props The properties of the black hole scheme. * @param constants The physical constants (in internal units). */ __attribute__((always_inline)) INLINE static float black_holes_compute_timestep( const struct bpart* const bp, const struct black_holes_props* props, const struct phys_const* constants, const struct cosmology* cosmo) { /* Gather some physical constants (in internal units) */ const double c = constants->const_speed_light_c; /* Compute instantaneous energy supply rate to the BH energy reservoir * which is proportional to the BH mass accretion rate */ const double Energy_rate = props->epsilon_r * props->epsilon_f * bp->accretion_rate * c * c; /* BHs not accreting can't estimate their time-step length based on the * the time-scale for heating */ if (Energy_rate == 0.) { return FLT_MAX; } /* Average particle mass in BH's kernel */ const double mean_ngb_mass = bp->ngb_mass / ((double)bp->num_ngbs); /* Get the AGN heating temperature that is tored in this BH */ const double AGN_delta_T = bp->AGN_delta_T; /* Without multiplying by mean_ngb_mass we'd get energy per unit mass */ const double E_heat = AGN_delta_T * props->temp_to_u_factor * mean_ngb_mass; /* Compute average time between heating events for the given accretion * rate. The time is multiplied by the number of Ngbs to heat because * if more particles are heated at once then the time between different * AGN feedback events increases proportionally. */ const double dt_heat = E_heat * bp->num_ngbs_to_heat / Energy_rate; /* The new timestep of the BH cannot be smaller than the miminum allowed * time-step */ const double bh_timestep = max(dt_heat, props->time_step_min); return bh_timestep; } /** * @brief Initialises the b-particles for the first time * * This function is called only once just after the ICs have been * read in to do some conversions. * * @param bp The particle to act upon * @param props The properties of the black holes model. */ __attribute__((always_inline)) INLINE static void black_holes_first_init_bpart( struct bpart* bp, const struct black_holes_props* props) { bp->time_bin = 0; if (props->use_subgrid_mass_from_ics == 0) { bp->subgrid_mass = bp->mass; } else if (props->with_subgrid_mass_check && bp->subgrid_mass <= 0) { error( "Black hole %lld has a subgrid mass of %f (internal units).\n" "If this is because the ICs do not contain a 'SubgridMass' data " "set, you should set the parameter " "'EAGLEAGN:use_subgrid_mass_from_ics' to 0 to initialize the " "black hole subgrid masses to the corresponding dynamical masses.\n" "If the subgrid mass is intentionally set to this value, you can " "disable this error by setting 'EAGLEAGN:with_subgrid_mass_check' " "to 0.", bp->id, bp->subgrid_mass); } bp->total_accreted_mass = 0.f; bp->accretion_rate = 0.f; bp->formation_time = -1.f; bp->energy_reservoir = 0.f; bp->cumulative_number_seeds = 1; bp->number_of_mergers = 0; bp->number_of_gas_swallows = 0; bp->number_of_direct_gas_swallows = 0; bp->number_of_repositions = 0; bp->number_of_reposition_attempts = 0; bp->number_of_time_steps = 0; bp->last_high_Eddington_fraction_scale_factor = -1.f; bp->last_minor_merger_time = -1.; bp->last_major_merger_time = -1.; bp->swallowed_angular_momentum[0] = 0.f; bp->swallowed_angular_momentum[1] = 0.f; bp->swallowed_angular_momentum[2] = 0.f; bp->accreted_angular_momentum[0] = 0.f; bp->accreted_angular_momentum[1] = 0.f; bp->accreted_angular_momentum[2] = 0.f; bp->last_repos_vel = 0.f; bp->num_ngbs_to_heat = props->num_ngbs_to_heat; /* Filler value */ bp->dt_heat = 0.f; bp->AGN_number_of_AGN_events = 0; bp->AGN_number_of_energy_injections = 0; bp->AGN_cumulative_energy = 0.f; /* Set the initial targetted heating temperature, used for the * BH time step determination */ bp->AGN_delta_T = props->AGN_delta_T_desired; } /** * @brief Prepares a b-particle for its interactions * * @param bp The particle to act upon */ __attribute__((always_inline)) INLINE static void black_holes_init_bpart( struct bpart* bp) { #ifdef DEBUG_INTERACTIONS_BLACK_HOLES for (int i = 0; i < MAX_NUM_OF_NEIGHBOURS_STARS; ++i) bp->ids_ngbs_density[i] = -1; bp->num_ngb_density = 0; #endif bp->density.wcount = 0.f; bp->density.wcount_dh = 0.f; bp->rho_gas = 0.f; bp->sound_speed_gas = 0.f; bp->internal_energy_gas = 0.f; bp->rho_subgrid_gas = -1.f; bp->sound_speed_subgrid_gas = -1.f; bp->velocity_gas[0] = 0.f; bp->velocity_gas[1] = 0.f; bp->velocity_gas[2] = 0.f; bp->circular_velocity_gas[0] = 0.f; bp->circular_velocity_gas[1] = 0.f; bp->circular_velocity_gas[2] = 0.f; bp->ngb_mass = 0.f; bp->num_ngbs = 0; bp->reposition.delta_x[0] = -FLT_MAX; bp->reposition.delta_x[1] = -FLT_MAX; bp->reposition.delta_x[2] = -FLT_MAX; bp->reposition.min_potential = FLT_MAX; bp->reposition.potential = FLT_MAX; bp->accretion_rate = 0.f; /* Optionally accumulated ngb-by-ngb */ bp->f_visc = FLT_MAX; bp->accretion_boost_factor = -FLT_MAX; bp->mass_at_start_of_step = bp->mass; /* bp->mass may grow in nibbling mode */ /* Reset the rays carried by this BH */ ray_init(bp->rays, eagle_blackhole_number_of_rays); } /** * @brief Predict additional particle fields forward in time when drifting * * The fields do not get predicted but we move the BH to its new position * if a new one was calculated in the repositioning loop. * * @param bp The particle * @param dt_drift The drift time-step for positions. */ __attribute__((always_inline)) INLINE static void black_holes_predict_extra( struct bpart* restrict bp, float dt_drift) { /* Are we doing some repositioning? */ if (bp->reposition.min_potential != FLT_MAX) { #ifdef SWIFT_DEBUG_CHECKS if (bp->reposition.delta_x[0] == -FLT_MAX || bp->reposition.delta_x[1] == -FLT_MAX || bp->reposition.delta_x[2] == -FLT_MAX) { error("Something went wrong with the new repositioning position"); } const double dx = bp->reposition.delta_x[0]; const double dy = bp->reposition.delta_x[1]; const double dz = bp->reposition.delta_x[2]; const double d = sqrt(dx * dx + dy * dy + dz * dz); if (d > 1.01 * kernel_gamma * bp->h) error("Repositioning BH beyond the kernel support!"); #endif /* Move the black hole */ bp->x[0] += bp->reposition.delta_x[0]; bp->x[1] += bp->reposition.delta_x[1]; bp->x[2] += bp->reposition.delta_x[2]; /* Move its gravity properties as well */ bp->gpart->x[0] += bp->reposition.delta_x[0]; bp->gpart->x[1] += bp->reposition.delta_x[1]; bp->gpart->x[2] += bp->reposition.delta_x[2]; /* Store the delta position */ bp->x_diff[0] -= bp->reposition.delta_x[0]; bp->x_diff[1] -= bp->reposition.delta_x[1]; bp->x_diff[2] -= bp->reposition.delta_x[2]; /* Reset the reposition variables */ bp->reposition.delta_x[0] = -FLT_MAX; bp->reposition.delta_x[1] = -FLT_MAX; bp->reposition.delta_x[2] = -FLT_MAX; bp->reposition.min_potential = FLT_MAX; /* Count the jump */ bp->number_of_repositions++; } } /** * @brief Sets the values to be predicted in the drifts to their values at a * kick time * * @param bp The particle. */ __attribute__((always_inline)) INLINE static void black_holes_reset_predicted_values(struct bpart* bp) {} /** * @brief Kick the additional variables * * @param bp The particle to act upon * @param dt The time-step for this kick */ __attribute__((always_inline)) INLINE static void black_holes_kick_extra( struct bpart* bp, float dt) {} /** * @brief Finishes the calculation of density on black holes * * @param bp The particle to act upon * @param cosmo The current cosmological model. */ __attribute__((always_inline)) INLINE static void black_holes_end_density( struct bpart* bp, const struct cosmology* cosmo) { /* Some smoothing length multiples. */ const float h = bp->h; const float h_inv = 1.0f / h; /* 1/h */ const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */ const float h_inv_dim_plus_one = h_inv_dim * h_inv; /* 1/h^(d+1) */ /* --- Finish the calculation by inserting the missing h factors --- */ bp->density.wcount *= h_inv_dim; bp->density.wcount_dh *= h_inv_dim_plus_one; bp->rho_gas *= h_inv_dim; const float rho_inv = 1.f / bp->rho_gas; /* For the following, we also have to undo the mass smoothing * (N.B.: bp->velocity_gas is in BH frame, in internal units). */ bp->sound_speed_gas *= h_inv_dim * rho_inv; bp->internal_energy_gas *= h_inv_dim * rho_inv; bp->velocity_gas[0] *= h_inv_dim * rho_inv; bp->velocity_gas[1] *= h_inv_dim * rho_inv; bp->velocity_gas[2] *= h_inv_dim * rho_inv; bp->circular_velocity_gas[0] *= h_inv_dim * rho_inv; bp->circular_velocity_gas[1] *= h_inv_dim * rho_inv; bp->circular_velocity_gas[2] *= h_inv_dim * rho_inv; /* Calculate circular velocity at the smoothing radius from specific * angular momentum (extra h_inv) */ bp->circular_velocity_gas[0] *= h_inv; bp->circular_velocity_gas[1] *= h_inv; bp->circular_velocity_gas[2] *= h_inv; } /** * @brief Sets all particle fields to sensible values when the #spart has 0 * ngbs. * * @param bp The particle to act upon * @param cosmo The current cosmological model. */ __attribute__((always_inline)) INLINE static void black_holes_bpart_has_no_neighbours(struct bpart* bp, const struct cosmology* cosmo) { warning( "BH particle with ID %lld treated as having no neighbours (h: %g, " "wcount: %g).", bp->id, bp->h, bp->density.wcount); /* Some smoothing length multiples. */ const float h = bp->h; const float h_inv = 1.0f / h; /* 1/h */ const float h_inv_dim = pow_dimension(h_inv); /* 1/h^d */ /* Re-set problematic values */ bp->density.wcount = kernel_root * h_inv_dim; bp->density.wcount_dh = 0.f; bp->velocity_gas[0] = FLT_MAX; bp->velocity_gas[1] = FLT_MAX; bp->velocity_gas[2] = FLT_MAX; bp->internal_energy_gas = -FLT_MAX; } /** * @brief Return the current instantaneous accretion rate of the BH. * * @param bp the #bpart. */ __attribute__((always_inline)) INLINE static double black_holes_get_accretion_rate(const struct bpart* bp) { return bp->accretion_rate; } /** * @brief Return the total accreted gas mass of this BH. * * @param bp the #bpart. */ __attribute__((always_inline)) INLINE static double black_holes_get_accreted_mass(const struct bpart* bp) { return bp->total_accreted_mass; } /** * @brief Return the subgrid mass of this BH. * * @param bp the #bpart. */ __attribute__((always_inline)) INLINE static double black_holes_get_subgrid_mass(const struct bpart* bp) { return bp->subgrid_mass; } /** * @brief Return the current bolometric luminosity of the BH. * * @param bp the #bpart. */ __attribute__((always_inline)) INLINE static double black_holes_get_bolometric_luminosity(const struct bpart* bp, const struct phys_const* constants) { return 0.; } /** * @brief Return the current kinetic jet power of the BH. * * @param bp the #bpart. */ __attribute__((always_inline)) INLINE static double black_holes_get_jet_power( const struct bpart* bp, const struct phys_const* constants) { return 0.; } /** * @brief Update the properties of a black hole particles by swallowing * a gas particle. * * @param bp The #bpart to update. * @param p The #part that is swallowed. * @param xp The #xpart that is swallowed. * @param cosmo The current cosmological model. */ __attribute__((always_inline)) INLINE static void black_holes_swallow_part( struct bpart* bp, const struct part* p, const struct xpart* xp, const struct cosmology* cosmo) { /* Get the current dynamical masses */ const float gas_mass = hydro_get_mass(p); const float BH_mass = bp->mass; /* Increase the dynamical mass of the BH. */ bp->mass += gas_mass; bp->gpart->mass += gas_mass; /* Physical velocity difference between the particles */ const float dv[3] = {(bp->v[0] - p->v[0]) * cosmo->a_inv, (bp->v[1] - p->v[1]) * cosmo->a_inv, (bp->v[2] - p->v[2]) * cosmo->a_inv}; /* Physical distance between the particles */ const float dx[3] = {(bp->x[0] - p->x[0]) * cosmo->a, (bp->x[1] - p->x[1]) * cosmo->a, (bp->x[2] - p->x[2]) * cosmo->a}; /* Collect the swallowed angular momentum */ bp->swallowed_angular_momentum[0] += gas_mass * (dx[1] * dv[2] - dx[2] * dv[1]); bp->swallowed_angular_momentum[1] += gas_mass * (dx[2] * dv[0] - dx[0] * dv[2]); bp->swallowed_angular_momentum[2] += gas_mass * (dx[0] * dv[1] - dx[1] * dv[0]); /* Update the BH momentum */ const float BH_mom[3] = {BH_mass * bp->v[0] + gas_mass * p->v[0], BH_mass * bp->v[1] + gas_mass * p->v[1], BH_mass * bp->v[2] + gas_mass * p->v[2]}; bp->v[0] = BH_mom[0] / bp->mass; bp->v[1] = BH_mom[1] / bp->mass; bp->v[2] = BH_mom[2] / bp->mass; bp->gpart->v_full[0] = bp->v[0]; bp->gpart->v_full[1] = bp->v[1]; bp->gpart->v_full[2] = bp->v[2]; const float dr = sqrt(dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]); message( "BH %lld swallowing gas particle %lld " "(Delta_v = [%f, %f, %f] U_V, " "Delta_x = [%f, %f, %f] U_L, " "Delta_v_rad = %f)", bp->id, p->id, -dv[0], -dv[1], -dv[2], -dx[0], -dx[1], -dx[2], (dv[0] * dx[0] + dv[1] * dx[1] + dv[2] * dx[2]) / dr); /* Update the BH metal masses */ struct chemistry_bpart_data* bp_chem = &bp->chemistry_data; const struct chemistry_part_data* p_chem = &p->chemistry_data; chemistry_add_part_to_bpart(bp_chem, p_chem, gas_mass); /* This BH swallowed a gas particle */ bp->number_of_gas_swallows++; bp->number_of_direct_gas_swallows++; /* This BH lost a neighbour */ bp->num_ngbs--; bp->ngb_mass -= gas_mass; /* The ray(s) should not point to the no-longer existing particle */ ray_reset_part_id(bp->rays, eagle_blackhole_number_of_rays, p->id); } /** * @brief Update the properties of a black hole particles by swallowing * a BH particle. * * @param bpi The #bpart to update. * @param bpj The #bpart that is swallowed. * @param cosmo The current cosmological model. * @param time Time since the start of the simulation (non-cosmo mode). * @param with_cosmology Are we running with cosmology? * @param props The properties of the black hole scheme. * @param constants The physical constants in internal units. */ __attribute__((always_inline)) INLINE static void black_holes_swallow_bpart( struct bpart* bpi, const struct bpart* bpj, const struct cosmology* cosmo, const double time, const int with_cosmology, const struct black_holes_props* props, const struct phys_const* constants) { /* Get the current dynamical masses */ const float bpi_dyn_mass = bpi->mass; const float bpj_dyn_mass = bpj->mass; /* Is this merger ratio above the threshold for recording? */ const double merger_ratio = bpj->subgrid_mass / bpi->subgrid_mass; if (merger_ratio > props->major_merger_threshold) { if (with_cosmology) { bpi->last_major_merger_scale_factor = cosmo->a; } else { bpi->last_major_merger_time = time; } } else if (merger_ratio > props->minor_merger_threshold) { if (with_cosmology) { bpi->last_minor_merger_scale_factor = cosmo->a; } else { bpi->last_minor_merger_time = time; } } /* Increase the masses of the BH. */ bpi->mass += bpj->mass; bpi->gpart->mass += bpj->mass; bpi->subgrid_mass += bpj->subgrid_mass; /* Collect the swallowed angular momentum */ bpi->swallowed_angular_momentum[0] += bpj->swallowed_angular_momentum[0]; bpi->swallowed_angular_momentum[1] += bpj->swallowed_angular_momentum[1]; bpi->swallowed_angular_momentum[2] += bpj->swallowed_angular_momentum[2]; /* Update the BH momentum */ const float BH_mom[3] = {bpi_dyn_mass * bpi->v[0] + bpj_dyn_mass * bpj->v[0], bpi_dyn_mass * bpi->v[1] + bpj_dyn_mass * bpj->v[1], bpi_dyn_mass * bpi->v[2] + bpj_dyn_mass * bpj->v[2]}; bpi->v[0] = BH_mom[0] / bpi->mass; bpi->v[1] = BH_mom[1] / bpi->mass; bpi->v[2] = BH_mom[2] / bpi->mass; bpi->gpart->v_full[0] = bpi->v[0]; bpi->gpart->v_full[1] = bpi->v[1]; bpi->gpart->v_full[2] = bpi->v[2]; /* Update the BH metal masses */ struct chemistry_bpart_data* bpi_chem = &bpi->chemistry_data; const struct chemistry_bpart_data* bpj_chem = &bpj->chemistry_data; chemistry_add_bpart_to_bpart(bpi_chem, bpj_chem); /* Update the energy reservoir */ bpi->energy_reservoir += bpj->energy_reservoir; /* Add up all the BH seeds */ bpi->cumulative_number_seeds += bpj->cumulative_number_seeds; /* Add up all the gas particles we swallowed */ bpi->number_of_gas_swallows += bpj->number_of_gas_swallows; /* Add the subgrid angular momentum that we swallowed */ bpi->accreted_angular_momentum[0] += bpj->accreted_angular_momentum[0]; bpi->accreted_angular_momentum[1] += bpj->accreted_angular_momentum[1]; bpi->accreted_angular_momentum[2] += bpj->accreted_angular_momentum[2]; /* We had another merger */ bpi->number_of_mergers++; } /** * @brief Computes the temperature increase delta_T for black hole feedback. * * This is calculated as delta_T = min(max(dT_crit, T_gas), dT_num, dT_max): * dT_crit = f_crit * critical temperature for suppressing numerical losses * T_gas = f_gas * temperature of ambient gas * dT_num = temperature increase affordable if N particles should be heated * dT_max = maximum allowed energy increase. * * @param bp The #bpart. * @param props The properties of the black hole model. * @param cosmo The current cosmological model. */ __attribute__((always_inline)) INLINE static double black_hole_feedback_delta_T( const struct bpart* bp, const struct black_holes_props* props, const struct cosmology* cosmo) { /* If we do not want a variable delta T, we can stop right here. */ if (!props->use_variable_delta_T) return props->AGN_delta_T_desired; if (bp->internal_energy_gas < 0) error("Attempting to compute feedback energy for BH without neighbours."); /* Black hole properties */ const double n_gas_phys = bp->rho_gas * cosmo->a3_inv * props->rho_to_n_cgs; const double mean_ngb_mass = bp->ngb_mass / ((double)bp->num_ngbs); const double T_gas = bp->internal_energy_gas * cosmo->a_factor_internal_energy / props->temp_to_u_factor; /* Calculate base line delta T from BH subgrid mass. The assumption is that * the BH mass scales (via halo mass) with the virial temperature, so that * this aims for delta T > T_vir. */ double delta_T = props->AGN_delta_T_mass_norm * pow((bp->subgrid_mass / props->AGN_delta_T_mass_reference), props->AGN_delta_T_mass_exponent); /* If desired, also make sure that delta T is not below the numerically * critical temperature or that of the ambient gas */ if (props->AGN_with_locally_adaptive_delta_T) { /* Critical temperature for numerical efficiency, based on equation 18 * of Dalla Vecchia & Schaye (2012) */ const double T_crit = 3.162e7 * pow(n_gas_phys * 0.1, 0.6666667) * pow(mean_ngb_mass * props->mass_to_solar_mass * 1e-6, 0.33333333); delta_T = max(delta_T, T_crit * props->AGN_delta_T_crit_factor); /* Delta_T should be (at least) some multiple of the local gas T */ delta_T = max(delta_T, T_gas * props->AGN_delta_T_background_factor); } /* Respect the limits */ delta_T = max(delta_T, props->AGN_delta_T_min); return min(delta_T, props->AGN_delta_T_max); } /** * @brief Computes the energy reservoir threshold for AGN feedback. * * If adaptive, this is proportional to the accretion rate, with an * asymptotic upper limit. * * @param bp The #bpart. * @param props The properties of the black hole model. */ __attribute__((always_inline)) INLINE static double black_hole_energy_reservoir_threshold(struct bpart* bp, const struct black_holes_props* props) { /* If we want a constant threshold, this is short and sweet. */ if (!props->use_adaptive_energy_reservoir_threshold) return props->num_ngbs_to_heat; double num_to_heat = props->nheat_alpha * (bp->accretion_rate / props->nheat_maccr_normalisation); /* Impose smooth truncation of num_to_heat towards props->nheat_limit */ if (num_to_heat > props->nheat_alpha) { const double coeff_b = 1. / (props->nheat_limit - props->nheat_alpha); const double coeff_a = exp(coeff_b * props->nheat_alpha) / coeff_b; num_to_heat = props->nheat_limit - coeff_a * exp(-coeff_b * num_to_heat); } bp->num_ngbs_to_heat = num_to_heat; return num_to_heat; } /** * @brief Compute the accretion rate of the black hole and all the quantities * required for the feedback loop. * * @param bp The black hole particle. * @param props The properties of the black hole scheme. * @param constants The physical constants (in internal units). * @param cosmo The cosmological model. * @param cooling Properties of the cooling model. * @param floor_props Properties of the entropy fllor. * @param time Time since the start of the simulation (non-cosmo mode). * @param with_cosmology Are we running with cosmology? * @param dt The time-step size (in physical internal units). * @param ti_begin The time at which the step begun (ti_current). */ __attribute__((always_inline)) INLINE static void black_holes_prepare_feedback( struct bpart* restrict bp, const struct black_holes_props* props, const struct phys_const* constants, const struct cosmology* cosmo, const struct cooling_function_data* cooling, const struct entropy_floor_properties* floor_props, const double time, const int with_cosmology, const double dt, const integertime_t ti_begin) { /* Record that the black hole has another active time step */ bp->number_of_time_steps++; if (dt == 0. || bp->rho_gas == 0.) return; /* Gather some physical constants (all in internal units) */ const double G = constants->const_newton_G; const double c = constants->const_speed_light_c; const double proton_mass = constants->const_proton_mass; const double sigma_Thomson = constants->const_thomson_cross_section; /* Gather the parameters of the model */ const double f_Edd = props->f_Edd; const double f_Edd_recording = props->f_Edd_recording; const double epsilon_r = props->epsilon_r; const double epsilon_f = props->epsilon_f; const double num_ngbs_to_heat = props->num_ngbs_to_heat; const int with_angmom_limiter = props->with_angmom_limiter; /* (Subgrid) mass of the BH (internal units) */ const double BH_mass = bp->subgrid_mass; /* Convert the quantities we gathered to physical frame (all internal units). * Note: for the velocities this means peculiar velocities */ double gas_c_phys = bp->sound_speed_gas * cosmo->a_factor_sound_speed; double gas_c_phys2 = gas_c_phys * gas_c_phys; const double gas_v_circular[3] = { bp->circular_velocity_gas[0] * cosmo->a_inv, bp->circular_velocity_gas[1] * cosmo->a_inv, bp->circular_velocity_gas[2] * cosmo->a_inv}; /* Norm of the circular velocity of the gas around the BH */ const double tangential_velocity2 = gas_v_circular[0] * gas_v_circular[0] + gas_v_circular[1] * gas_v_circular[1] + gas_v_circular[2] * gas_v_circular[2]; const double tangential_velocity = sqrt(tangential_velocity2); /* We can now compute the Bondi accretion rate (internal units) */ double Bondi_rate; if (props->use_multi_phase_bondi) { /* In this case, we are in 'multi-phase-Bondi' mode -- otherwise, * the accretion_rate is still zero (was initialised to this) */ const float hi_inv = 1.f / bp->h; const float hi_inv_dim = pow_dimension(hi_inv); /* 1/h^d */ Bondi_rate = bp->accretion_rate * (4. * M_PI * G * G * BH_mass * BH_mass * hi_inv_dim); } else { /* Standard approach: compute accretion rate for all gas simultaneously */ /* Convert velocities to physical frame * Note: velocities are already in black hole frame. */ const double gas_v_phys[3] = {bp->velocity_gas[0] * cosmo->a_inv, bp->velocity_gas[1] * cosmo->a_inv, bp->velocity_gas[2] * cosmo->a_inv}; const double gas_v_norm2 = gas_v_phys[0] * gas_v_phys[0] + gas_v_phys[1] * gas_v_phys[1] + gas_v_phys[2] * gas_v_phys[2]; if (props->use_subgrid_bondi) { /* Use subgrid rho and c for Bondi model */ /* Construct basic properties of the gas around the BH in physical coordinates */ const double gas_rho_phys = bp->rho_gas * cosmo->a3_inv; const double gas_u_phys = bp->internal_energy_gas * cosmo->a_factor_internal_energy; const double gas_P_phys = gas_pressure_from_internal_energy(gas_rho_phys, gas_u_phys); /* Assume primordial abundance and solar metallicity pattern * (Yes, that is inconsitent but makes no difference) */ const double logZZsol = 0.; const double XH = 0.75; float abundance_ratio[colibre_cooling_N_elementtypes]; for (int i = 0; i < colibre_cooling_N_elementtypes; ++i) abundance_ratio[i] = 1.f; /* Get the gas temperature */ const float gas_T = cooling_get_temperature_from_gas( constants, cosmo, cooling, gas_rho_phys, logZZsol, XH, gas_u_phys, /*HII_region=*/0); const float log10_gas_T = log10f(gas_T); /* Internal energy on the entropy floor */ const double P_EOS = entropy_floor_gas_pressure(gas_rho_phys, bp->rho_gas, cosmo, floor_props); const double u_EOS = gas_internal_energy_from_pressure(gas_rho_phys, P_EOS); const double u_EOS_max = u_EOS * exp10(cooling->dlogT_EOS); const float log10_u_EOS_max_cgs = log10f(u_EOS_max * cooling->internal_energy_to_cgs + FLT_MIN); /* Compute the subgrid density assuming pressure * equilibirum if on the entropy floor */ const double rho_sub = compute_subgrid_property( cooling, constants, floor_props, cosmo, gas_rho_phys, logZZsol, XH, gas_P_phys, log10_gas_T, log10_u_EOS_max_cgs, /*HII_region=*/0, abundance_ratio, 0.f, cooling_compute_subgrid_density); /* Record what we used */ bp->rho_subgrid_gas = rho_sub; /* And the subgrid sound-speed */ const float c_sub = gas_soundspeed_from_pressure(rho_sub, gas_P_phys); /* Also update the sound-speed to use in the angular momentum limiter */ gas_c_phys = c_sub; gas_c_phys2 = c_sub * c_sub; /* Record what we used */ bp->sound_speed_subgrid_gas = c_sub; /* Now, compute the Bondi rate based on the normal velocities and * the subgrid density and sound-speed */ const double denominator2 = gas_v_norm2 + gas_c_phys2; #ifdef SWIFT_DEBUG_CHECKS /* Make sure that the denominator is strictly positive */ if (denominator2 <= 0) error( "Invalid denominator for black hole particle %lld in Bondi rate " "calculation.", bp->id); #endif const double denominator_inv = 1. / sqrt(denominator2); Bondi_rate = 4. * M_PI * G * G * BH_mass * BH_mass * rho_sub * denominator_inv * denominator_inv * denominator_inv; } else { /* Use dynamical rho and c for Bondi model */ const double gas_rho_phys = bp->rho_gas * cosmo->a3_inv; const double denominator2 = gas_v_norm2 + gas_c_phys2; #ifdef SWIFT_DEBUG_CHECKS /* Make sure that the denominator is strictly positive */ if (denominator2 <= 0) error( "Invalid denominator for black hole particle %lld in Bondi rate " "calculation.", bp->id); #endif const double denominator_inv = 1. / sqrt(denominator2); Bondi_rate = 4. * M_PI * G * G * BH_mass * BH_mass * gas_rho_phys * denominator_inv * denominator_inv * denominator_inv; } } /* Compute the boost factor from Booth & Schaye (2009) */ if (props->with_boost_factor) { const double XH = 0.75; const double gas_rho_phys = bp->rho_gas * cosmo->a3_inv; const double n_H = gas_rho_phys * XH / proton_mass; const double boost_ratio = n_H / props->boost_n_h_star; double boost_factor = 1.; if (props->boost_alpha_only) { boost_factor = props->boost_alpha; } else { /* Booth & Schaye (2009), eq. 4 */ if (n_H > props->boost_n_h_star) { boost_factor = pow(boost_ratio, props->boost_beta); } } Bondi_rate *= boost_factor; bp->accretion_boost_factor = boost_factor; } else { bp->accretion_boost_factor = 1.; } /* Compute the reduction factor from Rosas-Guevara et al. (2015) */ if (with_angmom_limiter) { const double Bondi_radius = G * BH_mass / gas_c_phys2; const double Bondi_time = Bondi_radius / gas_c_phys; const double r_times_v_tang = Bondi_radius * tangential_velocity; const double r_times_v_tang_3 = r_times_v_tang * r_times_v_tang * r_times_v_tang; const double viscous_time = 2. * M_PI * r_times_v_tang_3 / (1e-6 * props->alpha_visc * G * G * BH_mass * BH_mass); const double f_visc = min(Bondi_time / viscous_time, 1.); bp->f_visc = f_visc; /* Limit the accretion rate by the Bondi-to-viscous time ratio */ Bondi_rate *= f_visc; } else { bp->f_visc = 1.0; } /* Compute the Eddington rate (internal units) */ const double Eddington_rate = 4. * M_PI * G * BH_mass * proton_mass / (epsilon_r * c * sigma_Thomson); /* Should we record this time as the most recent high accretion rate? */ if (Bondi_rate > f_Edd_recording * Eddington_rate) { if (with_cosmology) { bp->last_high_Eddington_fraction_scale_factor = cosmo->a; } else { bp->last_high_Eddington_fraction_time = time; } } /* Limit the accretion rate to a fraction of the Eddington rate */ const double accr_rate = min(Bondi_rate, f_Edd * Eddington_rate); bp->accretion_rate = accr_rate; bp->eddington_fraction = Bondi_rate / Eddington_rate; /* Factor in the radiative efficiency */ const double mass_rate = (1. - epsilon_r) * accr_rate; const double luminosity = epsilon_r * accr_rate * c * c; /* Integrate forward in time */ bp->subgrid_mass += mass_rate * dt; bp->total_accreted_mass += mass_rate * dt; bp->energy_reservoir += luminosity * epsilon_f * dt; if (props->use_nibbling && bp->subgrid_mass < bp->mass) { /* In this case, the BH is still accreting from its (assumed) subgrid gas * mass reservoir left over when it was formed. There is some loss in this * due to radiative losses, so we must decrease the particle mass * in proprtion to its current accretion rate. We do not account for this * in the swallowing approach, however. */ bp->mass -= epsilon_r * accr_rate * dt; if (bp->mass < 0) error("Black hole %lld reached negative mass (%g). Trouble ahead...", bp->id, bp->mass); } /* Increase the subgrid angular momentum according to what we accreted * Note that this is already in physical units, a factors from velocity and * radius cancel each other. Also, the circular velocity contains an extra * smoothing length factor that we undo here. */ bp->accreted_angular_momentum[0] += bp->circular_velocity_gas[0] * mass_rate * dt / bp->h; bp->accreted_angular_momentum[1] += bp->circular_velocity_gas[1] * mass_rate * dt / bp->h; bp->accreted_angular_momentum[2] += bp->circular_velocity_gas[2] * mass_rate * dt / bp->h; /* Below we compute energy required to have a feedback event(s) * Note that we have subtracted the particles we swallowed from the ngb_mass * and num_ngbs accumulators. */ /* Now find the temperature increase for a possible feedback event */ const double delta_T = black_hole_feedback_delta_T(bp, props, cosmo); bp->AGN_delta_T = delta_T; double delta_u = delta_T * props->temp_to_u_factor; const double delta_u_ref = props->AGN_use_nheat_with_fixed_dT ? props->AGN_delta_T_desired * props->temp_to_u_factor : delta_u; /* Energy required to have a feedback event * Note that we have subtracted the particles we swallowed from the ngb_mass * and num_ngbs accumulators. */ const double mean_ngb_mass = bp->ngb_mass / ((double)bp->num_ngbs); const double E_feedback_event = num_ngbs_to_heat * delta_u_ref * mean_ngb_mass; /* Compute and store BH accretion-limited time-step */ if (luminosity > 0.) { const float dt_acc = delta_u * mean_ngb_mass * props->num_ngbs_to_heat / (luminosity * props->epsilon_f); bp->dt_heat = max(dt_acc, props->time_step_min); } else { bp->dt_heat = FLT_MAX; } /* Are we doing some feedback? */ if (bp->energy_reservoir > E_feedback_event) { int number_of_energy_injections; /* How are we doing feedback? */ if (props->AGN_deterministic) { number_of_energy_injections = (int)(bp->energy_reservoir / (delta_u * mean_ngb_mass)); } else { /* Probability of heating. */ const double prob = bp->energy_reservoir / (delta_u * bp->ngb_mass); /* Compute the number of energy injections based on probability */ if (prob < 1.) { /* Initialise counter of energy injections */ number_of_energy_injections = 0; /* How many AGN energy injections will we get? * * Note that we use the particles here to draw random numbers. This does * not mean that the 'lucky' particles here are the ones that will be * heated. If we get N lucky particles, we will use the first N random * ray directions in the isotropic case or the first N closest particles * in the other modes. */ for (int i = 0; i < bp->num_ngbs; i++) { const double rand = random_unit_interval_part_ID_and_index( bp->id, i, ti_begin, random_number_BH_feedback); /* Increase the counter if we are lucky */ if (rand < prob) number_of_energy_injections++; } } else { /* We want to use up all energy avaliable in the reservoir. Therefore, * number_of_energy_injections is > or = props->num_ngbs_to_heat */ number_of_energy_injections = (int)(bp->energy_reservoir / (delta_u * mean_ngb_mass)); } } /* Maximum number of energy injections allowed */ const int N_energy_injections_allowed = min(eagle_blackhole_number_of_rays, bp->num_ngbs); /* If there are more energy-injection events than min(the number of Ngbs in * the kernel, maximum number of rays) then lower the number of events & * proportionally increase the energy per event */ if (number_of_energy_injections > N_energy_injections_allowed) { /* Increase the thermal energy per event */ const double alpha_thermal = (double)number_of_energy_injections / (double)N_energy_injections_allowed; delta_u *= alpha_thermal; /* Lower the maximum number of events to the max allowed value */ number_of_energy_injections = N_energy_injections_allowed; } /* Compute how much energy will be deposited onto the gas */ /* Note that it will in general be different from E_feedback_event if * gas particles are of different mass. */ double Energy_deposited = 0.0; /* Count the number of unsuccessful energy injections (e.g., if the particle * that the BH wants to heat has been swallowed and thus no longer exists) */ int N_unsuccessful_energy_injections = 0; for (int i = 0; i < number_of_energy_injections; i++) { /* If the gas particle that the BH wants to heat has just been swallowed * by the same BH, increment the counter of unsuccessful injections. If * the particle has not been swallowed by the BH, increase the energy that * will later be subtracted from the BH's energy reservoir. */ if (bp->rays[i].id_min_length != -1) Energy_deposited += delta_u * bp->rays[i].mass; else N_unsuccessful_energy_injections++; } /* Store all of this in the black hole for delivery onto the gas. */ bp->to_distribute.AGN_delta_u = delta_u; bp->to_distribute.AGN_number_of_energy_injections = number_of_energy_injections; /* Subtract the deposited energy from the BH energy reservoir. Note * that in the stochastic case, the resulting value might be negative. * This happens when (due to the probabilistic nature of the model) the * BH injects more energy than it actually has in the reservoir. */ bp->energy_reservoir -= Energy_deposited; /* Total number successful energy injections at this time-step. In each * energy injection, a certain gas particle from the BH's kernel gets * heated. (successful = the particle(s) that is going to get heated by * this BH has not been swallowed by the same BH). */ const int N_successful_energy_injections = number_of_energy_injections - N_unsuccessful_energy_injections; /* Increase the number of energy injections the black hole has heated so * far. Note that in the isotropic model, a gas particle may receive AGN * energy several times at the same time-step. In this case, the number of * particles heated at this time-step for this BH will be smaller than the * total number of energy injections for this BH. */ bp->AGN_number_of_energy_injections += N_successful_energy_injections; /* Increase the number of AGN events the black hole has had so far. * If the BH does feedback, the number of AGN events is incremented by one. */ bp->AGN_number_of_AGN_events += N_successful_energy_injections > 0; /* Update the total (cumulative) energy used for gas heating in AGN feedback * by this BH */ bp->AGN_cumulative_energy += Energy_deposited; /* Store the time/scale factor when the BH last did AGN feedback */ if (N_successful_energy_injections) { if (with_cosmology) { bp->last_AGN_event_scale_factor = cosmo->a; } else { bp->last_AGN_event_time = time; } } } else { /* Flag that we don't want to heat anyone */ bp->to_distribute.AGN_number_of_energy_injections = 0; bp->to_distribute.AGN_delta_u = 0.f; } } /** * @brief Computes the (maximal) repositioning speed for a black hole. * * Calculated as upsilon * (m_BH / m_ref) ^ beta_m * (n_H_BH / n_ref) ^ beta_n * where m_BH = BH subgrid mass, n_H_BH = physical gas density around BH * and upsilon, m_ref, beta_m, n_ref, and beta_n are parameters. * * @param bp The #bpart. * @param props The properties of the black hole model. * @param cosmo The current cosmological model. */ __attribute__((always_inline)) INLINE static double black_holes_get_repositioning_speed(const struct bpart* restrict bp, const struct black_holes_props* props, const struct cosmology* cosmo) { const double n_gas_phys = bp->rho_gas * cosmo->a3_inv * props->rho_to_n_cgs; const double v_repos = props->reposition_coefficient_upsilon * pow(bp->subgrid_mass / props->reposition_reference_mass, props->reposition_exponent_mass) * pow(n_gas_phys / props->reposition_reference_n_H, props->reposition_exponent_n_H); /* Make sure the repositioning is not back-firing... */ if (v_repos < 0) error( "BH %lld wants to reposition at negative speed (%g U_V). Do you " "think you are being funny? No-one is laughing.", bp->id, v_repos); return v_repos; } /** * @brief Finish the calculation of the new BH position. * * Here, we check that the BH should indeed be moved in the next drift. * * @param bp The black hole particle. * @param props The properties of the black hole scheme. * @param constants The physical constants (in internal units). * @param cosmo The cosmological model. * @param dt The black hole particle's time step. * @param ti_begin The time at the start of the temp */ __attribute__((always_inline)) INLINE static void black_holes_end_reposition( struct bpart* restrict bp, const struct black_holes_props* props, const struct phys_const* constants, const struct cosmology* cosmo, const double dt, const integertime_t ti_begin) { /* First check: did we find any eligible neighbour particle to jump to? */ if (bp->reposition.min_potential != FLT_MAX) { /* Record that we have a (possible) repositioning situation */ bp->number_of_reposition_attempts++; /* Is the potential lower (i.e. the BH is at the bottom already) * OR is the BH massive enough that we don't reposition? */ const float potential = gravity_get_comoving_potential(bp->gpart); if (potential < bp->reposition.min_potential || bp->subgrid_mass > props->max_reposition_mass) { /* No need to reposition */ bp->reposition.min_potential = FLT_MAX; bp->reposition.delta_x[0] = -FLT_MAX; bp->reposition.delta_x[1] = -FLT_MAX; bp->reposition.delta_x[2] = -FLT_MAX; } else if (props->set_reposition_speed) { /* If we are re-positioning, move the BH a fraction of delta_x, so * that we have a well-defined re-positioning velocity (repos_vel * cannot be negative). */ double repos_vel = black_holes_get_repositioning_speed(bp, props, cosmo); /* Convert target reposition velocity to a fractional reposition * along reposition.delta_x */ const double dx = bp->reposition.delta_x[0]; const double dy = bp->reposition.delta_x[1]; const double dz = bp->reposition.delta_x[2]; const double d = sqrt(dx * dx + dy * dy + dz * dz); /* Exclude the pathological case of repositioning by zero distance */ if (d > 0) { double repos_frac = repos_vel * dt / d; /* We should never get negative repositioning fractions... */ if (repos_frac < 0) error("Wanting to reposition by negative fraction (%g)?", repos_frac); /* ... but fractions > 1 can occur if the target velocity is high. * We do not want this, because it could lead to overshooting the * actual potential minimum. */ if (repos_frac > 1) { repos_frac = 1.; repos_vel = repos_frac * d / dt; } bp->last_repos_vel = (float)repos_vel; bp->reposition.delta_x[0] *= repos_frac; bp->reposition.delta_x[1] *= repos_frac; bp->reposition.delta_x[2] *= repos_frac; } /* ends section for fractional repositioning */ } else { /* We _should_ reposition, but not fractionally. Here, we will * reposition exactly on top of another gas particle - which * could cause issues, so we add on a small fractional offset * of magnitude 0.001 h in the reposition delta. */ /* Generate three random numbers in the interval [-0.5, 0.5[; id, * id**2, and id**3 are required to give unique random numbers (as * random_unit_interval is completely reproducible). */ const float offset_dx = random_unit_interval(bp->id, ti_begin, random_number_BH_reposition) - 0.5f; const float offset_dy = random_unit_interval(bp->id * bp->id, ti_begin, random_number_BH_reposition) - 0.5f; const float offset_dz = random_unit_interval(bp->id * bp->id * bp->id, ti_begin, random_number_BH_reposition) - 0.5f; const float length_inv = 1.0f / sqrtf(offset_dx * offset_dx + offset_dy * offset_dy + offset_dz * offset_dz); const float norm = 0.001f * bp->h * length_inv; bp->reposition.delta_x[0] += offset_dx * norm; bp->reposition.delta_x[1] += offset_dy * norm; bp->reposition.delta_x[2] += offset_dz * norm; } } /* ends section if we found eligible repositioning target(s) */ } /** * @brief Reset acceleration fields of a particle * * This is the equivalent of hydro_reset_acceleration. * We do not compute the acceleration on black hole, therefore no need to use * it. * * @param bp The particle to act upon */ __attribute__((always_inline)) INLINE static void black_holes_reset_feedback( struct bpart* restrict bp) { bp->to_distribute.AGN_delta_u = 0.f; bp->to_distribute.AGN_number_of_energy_injections = 0; #ifdef DEBUG_INTERACTIONS_BLACK_HOLES for (int i = 0; i < MAX_NUM_OF_NEIGHBOURS_STARS; ++i) bp->ids_ngbs_force[i] = -1; bp->num_ngb_force = 0; #endif } /** * @brief Store the gravitational potential of a black hole by copying it from * its #gpart friend. * * @param bp The black hole particle. * @param gp The black hole's #gpart. */ __attribute__((always_inline)) INLINE static void black_holes_store_potential_in_bpart(struct bpart* bp, const struct gpart* gp) { #ifdef SWIFT_DEBUG_CHECKS if (bp->gpart != gp) error("Copying potential to the wrong black hole!"); #endif bp->reposition.potential = gp->potential; } /** * @brief Store the gravitational potential of a particle by copying it from * its #gpart friend. * * @param p_data The black hole data of a gas particle. * @param gp The black hole's #gpart. */ __attribute__((always_inline)) INLINE static void black_holes_store_potential_in_part(struct black_holes_part_data* p_data, const struct gpart* gp) { p_data->potential = gp->potential; } /** * @brief Initialise a BH particle that has just been seeded. * * @param bp The #bpart to initialise. * @param props The properties of the black hole scheme. * @param constants The physical constants in internal units. * @param cosmo The current cosmological model. * @param p The #part that became a black hole. * @param xp The #xpart that became a black hole. * @param ti_current the current time on the time-line. */ INLINE static void black_holes_create_from_gas( struct bpart* bp, const struct black_holes_props* props, const struct phys_const* constants, const struct cosmology* cosmo, const struct part* p, const struct xpart* xp, const integertime_t ti_current) { /* All the non-basic properties of the black hole have been zeroed * in the FOF code. We update them here. * (i.e. position, velocity, mass, time-step have been set) */ /* Birth time and density */ bp->formation_scale_factor = cosmo->a; bp->formation_gas_density = hydro_get_physical_density(p, cosmo); /* Initial seed mass */ bp->subgrid_mass = props->subgrid_seed_mass; /* We haven't accreted anything yet */ bp->total_accreted_mass = 0.f; bp->cumulative_number_seeds = 1; bp->number_of_mergers = 0; bp->number_of_gas_swallows = 0; bp->number_of_direct_gas_swallows = 0; bp->number_of_time_steps = 0; /* Initialise the energy reservoir threshold to the constant default */ bp->num_ngbs_to_heat = props->num_ngbs_to_heat; /* Filler value */ /* We haven't repositioned yet, nor attempted it */ bp->number_of_repositions = 0; bp->number_of_reposition_attempts = 0; bp->last_repos_vel = 0.f; /* Copy over the splitting struct */ bp->split_data = xp->split_data; /* Initial metal masses */ const float gas_mass = hydro_get_mass(p); struct chemistry_bpart_data* bp_chem = &bp->chemistry_data; const struct chemistry_part_data* p_chem = &p->chemistry_data; chemistry_bpart_from_part(bp_chem, p_chem, gas_mass); /* No swallowed angular momentum */ bp->swallowed_angular_momentum[0] = 0.f; bp->swallowed_angular_momentum[1] = 0.f; bp->swallowed_angular_momentum[2] = 0.f; /* Last time this BH had a high Eddington fraction */ bp->last_high_Eddington_fraction_scale_factor = -1.f; /* Last time of mergers */ bp->last_minor_merger_time = -1.; bp->last_major_merger_time = -1.; /* Set the initial targetted heating temperature, used for the * BH time step determination */ bp->AGN_delta_T = props->AGN_delta_T_desired; /* First initialisation */ black_holes_init_bpart(bp); black_holes_mark_bpart_as_not_swallowed(&bp->merger_data); } #endif /* SWIFT_EAGLE_BLACK_HOLES_H */